1 SpringMVC异步
1.1 引言
spring mvc 同步接口在请求处理过程中一直处于阻塞状态,而异步接口可以启用后台线程去处理耗时任务。简单来说适用场景:
- 高并发;
- 高IO耗时操作。
Spring MVC3.2 之后支持异步请求,能够在controller 中返回一个Callable 或者DeferredResult
WebAsyncTask 是对Callable 的封装,提供了一些事件回调的处理,本质上区别不大。 DeferredResult 使用方式与Callable 类似,重点在于跨线程之间的通信。 @Async 也是替换Runable 的一种方式,可以代替我们自己创建线程。而且适用的范围更广,并不局限于Controller 层,而可以是任何层的方法上。
Servlet3.0 提供了AsyncContext 支持异步处理。Spring DeferredResult 在AsyncContext 进行了优化,实现了更简单的异步的实现。 Callable 是并发编程提供的支持有返回值的异步处理方式。 WebAsyncTask 在Callable 的基础上进行了包装,提供了更强大的功能,比如:处理超时回调、错误回调、完成回调等。 @Async 提供了更优为Runnable 的实现方式。
至于在实际的代码中,我们可能还需要借助其它的类配合实现以此来达到更好的效果。
1.2 Callable
1.2.1 Callable实例
@Controller
public class CallableController {
@RequestMapping(path = "/async1", method = RequestMethod.GET)
@ResponseBody
public Callable<String> asyncRequest() {
return () -> {
final long currentThread = Thread.currentThread().getId();
final Date requestProcessingStarted = new Date();
Thread.sleep(6000L);
final Date requestProcessingFinished = new Date();
return String.format(
"request: [threadId: %s, started: %s - finished: %s]"
, currentThread, requestProcessingStarted, requestProcessingFinished);
};
}
}
1.2.2 异步不能回调问题
使用了异步但是执行异步的方法,原因是在方法上加了@Async 注解,之所以加这个注解是因为报错:
There was an unexpected error (type=Internal Server Error, status=500). Async support must be enabled on a servlet and for all filters involved in async request processing. This is done in Java code using the Servlet API or by adding <async-supported>true</async-supported> to servlet and filter declarations in web.xml
异步测试时一直报这个错误,提示我在web.xml 开启异步支持,但是我是SpringBoot 项目,于是开始网上查找 错误 :加@Async 注解,会更加异步,不能获取异步结果 正确 :根本原因是容器注册问题,在springboot 启动类的注解@SpringBootApplication 旁边添加了@ServletComponentScan ,才导致上面的报错和不能回调,有三种解决方法:
- 去掉注解
@ServletComponentScan - 添加容器注册(springboot项目)
@Bean
public ServletRegistrationBean dispatcherServlet() {
ServletRegistrationBean registration = new ServletRegistrationBean(
new DispatcherServlet(), "/");
registration.setAsyncSupported(true);
return registration;
}
@Bean
DispatcherServlet dispatcherServlet(){
return new DispatcherServlet();
}
在过滤器那里添加asyncSupported = true 的支持
@WebFilter(urlPatterns="/*",asyncSupported = true)
- 修改
web.xml (传统xml项目) 需要在 web.xml 文件中的 servlet 定义中添加:"<async-supported>true</async-supported>"
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>Archetype Created Web Application</display-name>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mybatis.xml</param-value>
</context-param>
<context-param>
<param-name>spring.profiles.active</param-name>
<param-value>dev</param-value>
</context-param>
<context-param>
<param-name>spring.profiles.default</param-name>
<param-value>dev</param-value>
</context-param>
<filter>
<filter-name>encodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<async-supported>true</async-supported>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
1.3 WebAsyncTask
Spring 提供了对 异步任务 API , 采用 WebAsyncTask 类即可实现 异步任务. 对异步任务设置相应的 回调处理, 如当 任务超时, 异常抛出 等. 异步任务通常非常实用, 比如: 当一笔订单支付完成之后, 开启异步任务查询订单的支付结果。 简单来说:WebAsyncTask 类是Spring 提供的一步任务处理类。 另外要知道的一点就是:WebAsyncTask 是Callable 的升级版
1.3.1 使用例子及说明
WebAsyncTask : 在构造时写入Callable 主要业务逻辑 WebAsyncTask.onCompletion(Runnable) :在当前任务执行结束以后,无论是执行成功还是异常中止,onCompletion的回调最终都会被调用 WebAsyncTask.onError(Callable>) :当异步任务抛出异常的时候,onError() 方法即会被调用 WebAsyncTask.onTimeout(Callable>) :当异步任务发生超时的时候,onTimeout() 方法即会被调用
@RequestMapping("/async")
@ResponseBody
public WebAsyncTask<String> asyncTask(){
WebAsyncTask<String> webAsyncTask = new WebAsyncTask<String>(1000,new Callable<String>(){
@Override
public String call() throws Exception {
Thread.sleep(5000);
String message = "username:wangbinghua";
return message;
}
});
webAsyncTask.onCompletion(new Runnable() {
@Override
public void run() {
System.out.println("调用完成");
}
});
webAsyncTask.onTimeout(new Callable<String>() {
@Override
public String call() throws Exception {
System.out.println("业务处理超时");
return "<h1>Time Out</h1>";
}
});
return webAsyncTask;
}
1.4 DeferredResult
1.4.1 DeferredResult简介
DeferredResult 和Callable 实现功能类型,都是异步返回,只不过Callable 不能直接设置超时时间,还需要和FutureTask 配合使用,DeferredResult 可以直接超时时间 点击此处了解Callable相关使用
使用DeferredResult 目的:
API 接口需要在指定时间内将异步操作的结果同步返回给前端时;Controller 处理耗时任务,并且需要耗时任务的返回结果时;- 当一个请求到达
API 接口,如果该API 接口的return 返回值是DeferredResult ,在没有超时或者DeferredResult 对象没有设置setResult 时,接口不会返回,但是Servlet 容器线程会结束,DeferredResult 另起线程来进行结果处理(即这种操作提升了服务短时间的吞吐能力),并setResult ,如此以来这个请求不会占用服务连接池太久,如果超时或设置setResult ,接口会立即返回
使用DeferredResult 的流程:
- 浏览器发起异步请求
- 请求到达服务端被挂起
- 向浏览器进行响应,分为两种情况:
调用DeferredResult.setResult() ,请求被唤醒,返回结果 超时,返回一个设定的结果 - 浏览得到响应,再次重复1,处理此次响应结果
给人一种异步处理业务,但是却同步返回的感觉,和前端Promise对象非常类似(点击了解JavaScript中异步回调之Promise使用)
1.4.2 DeferredResult使用
创建实例对象
DeferredResult<ResponseEntity<List<User>>> deferredResult
= new DeferredResult<>(20000L, new ResponseEntity<>(HttpStatus.NOT_MODIFIED));
设置回调 DeferedResult 两个监听器(onCompletion & onTimeout ) 当DeferedResult 对象调用setResult 之后,响应完毕客户端,则直接调用onCompletion 对应的方法。 当业务处理相当耗时,则响应客户端超时,也会调用onCompletion 对应的方法以及onTimeout 方法。 此时,响应客户端的内容为deferedResult.setErrorResult 的内容,否则500错误。 发生异常,调用onCompletion 方法,此时,响应客户端的内容为deferedResult.setErrorResult 的内容,否则500错误。
deferredResult.onTimeout(() -> {
log.info("调用超时");
deferredResult.setResult("调用超时");
});
deferredResult.onCompletion(() -> {
log.info("调用完成");
});
设置结果
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(10);
deferredResult.setResult(new ResponseEntity<>(userService.listUser(), HttpStatus.OK));
} catch (InterruptedException e) {
e.printStackTrace();
}}).start();
1.4.3 完整示例
@GetMapping("/deferredResultUser")
public DeferredResult<String> deferredResultListUser() {
DeferredResult<String> deferredResult
= new DeferredResult<>(20000L, "失败");
deferredResult.onTimeout(() -> {
log.info("调用超时");
deferredResult.setResult("调用超时");
});
deferredResult.onCompletion(() -> {
log.info("调用完成");
});
new Thread(() -> {
try {
TimeUnit.SECONDS.sleep(10);
deferredResult.setResult("OK"));
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
return deferredResult;
}
客户端请求映射到控制器方法返回值为DeferredResult 时,会立即释放Tomcat 线程并将请求挂起,直到调用setResult() 方法或者超时,才会响应客户端请求
1.4.4 DeferredResult总结
控制器中定义方法返回值为DeferredResult ,会立即释放Tomcat 线程,使用业务线程处理业务 由DeferredResultMethodReturnValueHandler 处理返回结果,开启异步处理并设置DeferredResultHandler 业务执行完成后调用setResult() 方法,紧接着回调DeferredResultHandler 的handleResult() 设置结果并调度请求 创建Callable 对象并设置调用方法为call() 通过反射方式调用call() 得到返回值 使用返回值处理器处理返回值
|