Spring,SpringMVC,SpringBoot中注解讲解
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
当controller
返回值是Callable
的时候,springmvc
就会启动一个线程将Callable
交给TaskExecutor
去处理,然后DispatcherServlet
,还有所有的spring
拦截器都退出主线程,然后把 response
保持打开的状态,当Callable
执行结束之后,springmvc
就会重新启动分配一个request
请求,然后DispatcherServlet
就重新调用和处理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);
};
}
}
Callable
处理过程如下:
- 控制器返回一个
Callable
Spring MVC
调用request.startAsync()
并将Callable
提交给AsyncTaskExecutor
以在单独的线程中进行处理。- 同时,
DispatcherServlet
和所有过滤器退出Servlet
容器线程,但response
保持打开状态。 - 最终
Callable
产生结果,Spring MVC
将请求分派回Servlet
容器以完成处理。 - 再次调用
DispatcherServlet
,并使用Callable
异步生成的返回值继续处理。
Callable
默认使用 SimpleAsyncTaskExecutor
类来执行,这个类非常简单而且没有重用线程。在实践中,需要使用AsyncTaskExecutor类来对线程进行配置。
1.2.2 异步不能回调问题
1.3 WebAsyncTask
Spring
提供了对异步任务 API
, 采用 WebAsyncTask
类即可实现异步任务,对异步任务设置相应的 回调处理, 如当任务超时, 异常抛出 等。异步任务通常非常实用,比如:当一笔订单支付完成之后, 开启异步任务查询订单的支付结果。
简单来说:WebAsyncTask
类是Spring
提供的一步任务处理类。
另外要知道的一点就是:WebAsyncTask
是Callable
的升级版
本质上,和Callable
区别不大,但是由于它额外封装了一些事件的回调,所有,通常都使用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(){
// 1000 为超时设置
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
并将其保存在可以访问的内存队列或列表中。 Spring MVC
调用request.startAsync()
- 同时,
DispatcherServlet
和所有配置的过滤器退出请求处理线程,但响应保持打开状态。 - 应用程序从某个线程设置
DeferredResult
,Spring MVC
将请求分派回Servlet
容器。 - 再次调用
DispatcherServlet
,并使用异步生成的返回值继续处理。
使用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()
得到返回值
使用返回值处理器处理返回值
1.5 什么时候使用异步请求
异步请求能提高吞吐量,这个是建立在相同配置(这里的配置指的是:最大连接数、最大工作线程数
)的情况下。因此并不是说任何接口都可以使用异步请求。比如:一个请求是进行大量的计算(总之就是在处理这个请求的业务方法时CPU是没有休息的),这种情况使用异步请求就没有多大意义了,因为这时的异步请求只是把一个任务从tomcat的工作线程搬到了另一个线程罢了。
直接调大最大工作线程数配置也能到达要求。所以,真正使用异步请求的场景应该是该请求的业务代码中,大量的时间CPU是休息的(比如:在业务代码中请求其他系统的接口,在其他系统响应之前,CPU是阻塞等待的),这个时候使用异步请求,就可以释放tomcat的工作线程,让释放的工作线程可以处理其他的请求,从而提高吞吐量。
由于异步请求增加了更多的线程切换(同步请求是同一个工作线程一直处理),所以理论上会增加接口的耗时。但,这个耗时很短