SpringMVC
Servlet3 API ,无法使用 SpringMVC 为我们提供的特性,我们需要自己处理响应信息,处理方式相对繁琐。
SpringMVC 3.2 基于 Servelt3 引入异步请求处理方式,我们可以跟使用同步请求一样,方便使用异步请求。
SpringMVC 提供有两种异步方式,只要将 Controller
方法返回值修改下述类即可:
DeferredResult
Callable
DeferredResult
DeferredResult
是 SpringMVC 3.2 之后引入新的类,只要让请求方法返回 DeferredResult
,就可以快速使用异步请求,示例代码如下:
ExecutorService executorService = Executors.newFixedThreadPool(10); @RequestMapping("/hello_v1") public DeferredResult<String> hello_v1() { // 设置超时时间 DeferredResult<String> deferredResult = new DeferredResult<>(7000L); // 异步线程处理结束,将会执行该回调方法 deferredResult.onCompletion(() -> { log.info("异步线程处理结束"); }); // 如果异步线程执行时间超过设置超时时间,将会执行该回调方法 deferredResult.onTimeout(() -> { log.info("异步线程超时"); // 设置返回结果 deferredResult.setErrorResult("timeout error"); }); deferredResult.onError(throwable -> { log.error("异常", throwable); // 设置返回结果 deferredResult.setErrorResult("other error"); }); executorService.submit(() -> { try { TimeUnit.SECONDS.sleep(5); deferredResult.setResult("hello_v1"); // 设置返回结果 } catch (Exception e) { e.printStackTrace(); // 若异步方法内部异常 deferredResult.setErrorResult("error"); } }); log.info("servlet 线程处理结束"); return deferredResult; }
创建 DeferredResult
实例时可以传入特定超时时间。另外我们可以设置默认超时时间:
# 异步请求超时时间 spring.mvc.async.request-timeout=2000
如果异步程序执行完成,可以调用 DeferredResult#setResult
返回响应结果。此时若有设置 DeferredResult#onCompletion
回调方法,将会触发该回调方法。
同时我们还可以设置超时回调方法 DeferredResult#onTimeout
,一旦异步线程执行超时,将会触发该回调方法。
最后 DeferredResult
还提供其他异常的回调方法 onError
,起初阿粉以为只要异步线程内发生异常,就会触发该回调方法。尝试在异步线程内抛出异常,但是无法成功触发。
后续阿粉查看这个方法的 doc,当 web 容器线程处理异步请求时发生异常,才能成功触发。
image-20200326195610915
阿粉不知道如何才能发生这个异常,有经验的小伙伴们的可以留言告知下。
Callable
Spring 另外还提供一种异步请求使用方式,直接使用 JDK Callable
。示例代码如下:
@RequestMapping("/hello_v2") public Callable<String> hello_v2() { returnnew Callable<String>() { @Override public String call() throws Exception { TimeUnit.SECONDS.sleep(5); log.info("异步方法结束"); return"hello_v2"; } }; }
默认情况下,直接执行将会输出 WARN 日志:
image-20200326213122894
这是因为默认情况使用 SimpleAsyncTaskExecutor
执行异步请求,每次调用执行都将会新建线程。由于这种方式不复用线程,生产不推荐使用这种方式,所以我们需要使用线程池代替。
我们可以使用如下方式自定义线程池:
@Bean(TaskExecutionAutoConfiguration.APPLICATION_TASK_EXECUTOR_BEAN_NAME) public AsyncTaskExecutor executor() { ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor(); threadPoolTaskExecutor.setThreadNamePrefix("test-"); threadPoolTaskExecutor.setCorePoolSize(10); threadPoolTaskExecutor.setMaxPoolSize(20); return threadPoolTaskExecutor; }
注意 Bean 名称一定要是 applicationTaskExecutor
,若不一致, Spring 将不会使用自定义线程池。
或者可以直接使用 SpringBoot 配置文件方式配置代替:
# 核心线程数 spring.task.execution.pool.core-size=10 # 最大线程数 spring.task.execution.pool.max-size=20 # 线程名前缀 spring.task.execution.thread-name-prefix=test # 还有另外一些配置,读者们可以自行配置
这种方式异步请求的超时时间只能通过配置文件方式配置。
spring.mvc.async.request-timeout=10000
如果需要为单独请求的配置特定的超时时间,我们需要使用 WebAsyncTask
包装 Callable
。
@RequestMapping("/hello_v3") public WebAsyncTask<String> hello_v3() { System.out.println("asdas"); Callable<String> callable=new Callable<String>() { @Override public String call() throws Exception { TimeUnit.SECONDS.sleep(5); log.info("异步方法结束"); return"hello_v3"; } }; // 单位 ms WebAsyncTask<String> webAsyncTask=new WebAsyncTask<>(10000,callable); return webAsyncTask; }
总结
SpringMVC 两种异步请求方式,本质上就是帮我们包装 Servlet3 API ,让我们不用关心具体实现细节。虽然日常使用我们一般会选择使用 SpringMVC 两种异步请求方式,但是我们还是需要了解异步请求实际原理。所以大家如果在使用之前,可以先尝试使用 Servlet3 API 练习,后续再使用 SpringMVC。