在Servlet 3.0
之前,Servlet
采用Thread-Per-Request
的方式处理请求,即每一次Http
请求都由某一个线程从头到尾负责处理。如果一个请求需要进行IO操作,比如访问数据库、调用第三方服务接口等,那么其所对应的线程将同步地等待IO操作完成, 而IO操作是非常慢的,所以此时的线程并不能及时地释放回线程池以供后续使用,在并发量越来越大的情况下,这将带来严重的性能问题。其请求流程大致为:
而在Servlet3.0
发布后,提供了一个新特性:异步处理请求。可以先释放容器分配给请求的线程与相关资源,减轻系统负担,释放了容器所分配线程的请求,其响应将被延后,可以在耗时处理完成(例如长时间的运算)时再对客户端进行响应。其请求流程为:
在Servlet 3.0
后,我们可以从HttpServletRequest
对象中获得一个AsyncContext
对象,该对象构成了异步处理的上下文,Request
和Response
对象都可从中获取。AsyncContext
可以从当前线程传给另外的线程,并在新的线程中完成对请求的处理并返回结果给客户端,初始线程便可以还回给容器线程池以处理更多的请求。如此,通过将请求从一个线程传给另一个线程处理的过程便构成了Servlet 3.0
中的异步处理。
多说几句:
随着Spring5
发布,提供了一个响应式Web框架:Spring WebFlux
。之后可能就不需要Servlet
容器的支持了。以下是先后对比图:
左侧是传统的基于Servlet
的Spring Web MVC
框架,右侧是5.0版本新引入的基于Reactive Streams
的Spring WebFlux
框架,从上到下依次是Router Functions,WebFlux,Reactive Streams三个新组件。对于其发展前景还是拭目以待吧,有时间也该去了解下Spring5
了。
在编写实际代码之前,我们来了解下一些关于异步请求的api的调用说明。
HttpServletRequest
对象获取。AsyncContext asyncContext = request.startAsync();
其监听器的接口代码:
public interface AsyncListener extends EventListener {
void onComplete(AsyncEvent event) throws IOException;
void onTimeout(AsyncEvent event) throws IOException;
void onError(AsyncEvent event) throws IOException;
void onStartAsync(AsyncEvent event) throws IOException;
}
说明:
一般上,我们在超时或者异常时,会返回给前端相应的提示,比如说超时了,请再次请求等等,根据各业务进行自定义返回。同时,在异步调用完成时,一般需要执行一些清理工作或者其他相关操作。
需要注意的是只有在调用request.startAsync
前将监听器添加到AsyncContext
,监听器的onStartAsync
方法才会起作用,而调用startAsync
前AsyncContext
还不存在,所以第一次调用startAsync
是不会被监听器中的onStartAsync
方法捕获的,只有在超时后又重新开始的情况下onStartAsync
方法才会起作用。
setTimeout
方法设置,单位:毫秒。一定要设置超时时间,不能无限等待下去,不然和正常的请求就一样了。
Servlet方式实现异步请求
前面已经提到,可通过HttpServletRequest
对象中获得一个AsyncContext
对象,该对象构成了异步处理的上下文。所以,我们来实际操作下。
0.编写一个简单控制层
package com.bo.springboot.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.AsyncContext;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description: 使用servlet方式进行异步请求
* @Author: wangboc
* @Date: 2019/1/7 16:44
*/
@Slf4j
@RestController
@RequestMapping("/servlet")
public class ServletController {
@RequestMapping("/sync")
public void todo(HttpServletRequest request, HttpServletResponse response){
try {
// 休眠3秒
Thread.sleep(3000);
response.getWriter().println("This is a normal sync response.");
} catch (Exception e) {
e.printStackTrace();
}
}
@RequestMapping("/async")
public void todoAsync(HttpServletRequest request, HttpServletResponse response){
AsyncContext asyncContext = request.startAsync();
// 设置异步监听器
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent asyncEvent) throws IOException {
// 这里可以做一些清理资源的操作
log.info("Async execution completed.");
}
@Override
public void onTimeout(AsyncEvent asyncEvent) throws IOException {
// 做一些超时后的相关操作
log.info("Async execution timeout.");
}
@Override
public void onError(AsyncEvent asyncEvent) throws IOException {
log.info("Async execution error.");
}
@Override
public void onStartAsync(AsyncEvent asyncEvent) throws IOException {
log.info("Async execution started.");
}
});
// 设置10秒超时
asyncContext.setTimeout(10000);
// 异步处理请求
asyncContext.start(new Runnable() {
@Override
public void run() {
try {
// 休眠3秒
Thread.sleep(3000);
log.info("异步线程:" + Thread.currentThread().getName());
asyncContext.getResponse().setCharacterEncoding("utf-8");
asyncContext.getResponse().setContentType("text/html;charset=UTF-8");
asyncContext.getResponse().getWriter().println("This is a async response.");
// 通知异步上下文请求已完成并真正将响应信息返回给客户端
// 如果最后不调用该方法则异步线程将一直阻塞直到给定的超时后触发complete()回调
// 其实可以利用此特性 把连接挂起 进行多条消息的推送
asyncContext.complete();
} catch (Exception e) {
log.error("Async execution exception", e);
}
}
});
//也可以不使用asyncContext而使用线程池等进行异步调用
/* new Thread(new Runnable() {
@Override
public void run() {
// To Do ..
// Finally
asyncContext.complete();
}
}).start();*/
// 此时request的线程连接已经被释放了
log.info("请求线程:" + Thread.currentThread().getName());
}
}
注意:异步请求时,可以利用ThreadPoolExecutor
自定义个线程池。
1.启动下应用,查看控制台输出就可以获悉是否在同一个线程里面了。同时,可设置下等待时间,之后就会调用超时回调方法了,大家可自己试试。
2019-01-07 17:14:32.844 INFO 5172 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 4 ms
2019-01-07 17:14:32.864 INFO 5172 --- [nio-8080-exec-1] c.b.s.controller.ServletController : 请求线程:http-nio-8080-exec-1
2019-01-07 17:14:35.864 INFO 5172 --- [nio-8080-exec-2] c.b.s.controller.ServletController : 异步线程:http-nio-8080-exec-2
2019-01-07 17:14:35.872 INFO 5172 --- [nio-8080-exec-3] c.b.s.controller.ServletController : Async execution completed.
使用过滤器时,需要加入asyncSupported
为true
配置,开启异步请求支持。
@WebServlet(urlPatterns = "/test", asyncSupported = true )
public class AsyncServlet extends HttpServlet ...
题外话:其实我们可以利用在未执行asyncContext.complete()
方法时请求未结束这特性,可以做个简单的文件上传进度条之类的功能。但注意请求是会超时的,需要设置超时的时间下。
在Spring
中,有多种方式实现异步请求,比如callable
、DeferredResult
或者WebAsyncTask
。每个的用法略有不同,可根据不同的业务场景选择不同的方式。以下主要介绍一些常用的用法。
使用很简单,直接返回的参数包裹一层callable
即可。
@Slf4j
@RestController
@RequestMapping("/spring")
public class SpringController {
@RequestMapping("/callable")
public Callable callable(){
log.info("请求线程:" + Thread.currentThread().getName());
return new Callable() {
@Override
public String call() throws Exception {
log.info("异步线程:" + Thread.currentThread().getName());
Thread.sleep(3000);
return "callable";
}
};
}
}
控制台输出:
2019-01-08 13:17:04.273 INFO 9716 --- [nio-8080-exec-1] c.b.s.controller.SpringController : 请求线程:http-nio-8080-exec-1
2019-01-08 13:17:04.279 INFO 9716 --- [ task-1] c.b.s.controller.SpringController : 异步线程:task-1
超时、自定义线程设置
从控制台可以看见,异步响应的线程使用的是名为:task-1
的线程。第一次再访问时,就是task-2
了。若采用默认设置,会无限的创建新线程去处理异步请求,所以正常都需要配置一个线程池及超时时间。
编写一个配置类:CustomAsyncPool.java
@Configuration
public class CustomAsyncPool implements WebMvcConfigurer {
public ThreadPoolTaskExecutor taskExecutor(){
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(10);
taskExecutor.setMaxPoolSize(20);
taskExecutor.setQueueCapacity(500);
taskExecutor.setKeepAliveSeconds(60);
taskExecutor.setThreadNamePrefix("callable-");
taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
taskExecutor.setAwaitTerminationSeconds(60);
taskExecutor.initialize();
return taskExecutor;
}
@Override
public void configureAsyncSupport(AsyncSupportConfigurer configurer) {
configurer.setDefaultTimeout(3*1000);
configurer.setTaskExecutor(taskExecutor());
configurer.registerCallableInterceptors(new CustomCallableProcessingInterceptor());
// configurer.registerCallableInterceptors(new TimeoutCallableProcessingInterceptor());
}
public class CustomCallableProcessingInterceptor implements CallableProcessingInterceptor {
@Override
public Object handleTimeout(NativeWebRequest request, Callable task) throws Exception {
HttpServletRequest httpRequest = request.getNativeRequest(HttpServletRequest.class);
return new CustomAsyncRequestTimeoutException(httpRequest.getRequestURI());
}
}
}
自定义一个超时异常处理类:CustomAsyncRequestTimeoutException.java
public class CustomAsyncRequestTimeoutException extends RuntimeException {
public CustomAsyncRequestTimeoutException(String url){
super(url);
}
public CustomAsyncRequestTimeoutException(String url, Throwable cause){
super(url, cause);
}
}
同时,在统一异常处理加入对CustomAsyncRequestTimeoutException类的处理:
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomAsyncRequestTimeoutException.class)
@ResponseBody
public String asyncTimeout(HttpServletRequest request, Exception exp){
log.error("Async request timed out: " + exp.getMessage());
return "system busy!!";
}
}
之后,再运行就可以看见使用了自定义的线程池了,超时模拟如下:
2019-01-08 15:45:53.299 INFO 8580 --- [nio-8080-exec-1] c.b.s.controller.SpringController : 请求线程:http-nio-8080-exec-1
2019-01-08 15:45:53.303 INFO 8580 --- [ callable-1] c.b.s.controller.SpringController : 异步线程:callable-1
2019-01-08 15:45:56.957 ERROR 8580 --- [nio-8080-exec-2] c.b.s.exception.GlobalExceptionHandler : Async request timed out: /spring/callable
相比于callable
,DeferredResult
可以处理一些相对复杂一些的业务逻辑,最主要还是可以在另一个线程里面进行业务处理及返回,即可在两个完全不相干的线程间的通信。
public static ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
@RequestMapping("/deferred-result")
public DeferredResult deferredResult(){
log.info("请求线程:" + Thread.currentThread().getName());
// 初始化时指定超时时间
DeferredResult result = new DeferredResult<>(3*1000L);
// 处理超时任务采用委托机制
result.onTimeout(new Runnable() {
@Override
public void run() {
log.info("Async deferred result timed out");
// 返回超时结果
result.setErrorResult("task timeout");
}
});
// setResult完毕之后,调用该方法
result.onCompletion(new Runnable() {
@Override
public void run() {
log.info("Async deferred result completed");
}
});
// spring5.0新增 暂不知何时回调
result.onError(exp -> {
log.error("Async deferred result error", exp);
});
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
log.info("异步线程:" + Thread.currentThread().getName());
try {
// 模拟IO处理
Thread.sleep(1000);
// 返回结果
result.setResult("task finished");
} catch (Exception e) {
// 返回异常结果
log.error("async task exception", e);
result.setErrorResult("task error");
}
}
});
return result;
}
控制台输出:
2019-01-08 17:25:13.240 INFO 8072 --- [nio-8080-exec-1] c.b.s.controller.SpringController : 请求线程:http-nio-8080-exec-1
2019-01-08 17:25:13.242 INFO 8072 --- [pool-1-thread-1] c.b.s.controller.SpringController : 异步线程:pool-1-thread-1
2019-01-08 17:25:14.300 INFO 8072 --- [nio-8080-exec-2] c.b.s.controller.SpringController : Async deferred result completed
注意:返回结果时记得调用下setResult
方法。另外,利用DeferredResult
可实现一些长连接的功能,比如当某个操作是异步时,我们可以保存这个DeferredResult
对象,当异步通知回来时,我们再找回这个DeferredResult
对象,setResult返回
结果,提高性能。
使用方法与DeferedResult
类似,只是WebAsyncTask
是直接返回了。
@RequestMapping("/webAsyncTask")
public WebAsyncTask webAsyncTask(){
log.info("请求线程:" + Thread.currentThread().getName());
// 初始化时指定超时时间
WebAsyncTask result = new WebAsyncTask<>(3 * 1000L, new Callable() {
@Override
public String call() throws Exception {
log.info("异步线程:" + Thread.currentThread().getName());
try {
Thread.sleep(1000);
// int t = 1/0;
} catch (Exception e) {
log.error("task exception", e);
return "task error";
}
return "task finished";
}
});
// 任务超时后回调
result.onTimeout(new Callable() {
@Override
public String call() throws Exception {
log.info("Web async task timed out");
return "task timeout";
}
});
// 超时后也会执行该回调
result.onCompletion(new Runnable() {
@Override
public void run() {
log.info("Web async task completed");
}
});
// // spring5.0新增 暂不知如何回调
result.onError(new Callable() {
@Override
public String call() throws Exception {
log.error("Web async task error");
return "task error";
}
});
return result;
}
控制台输出:
2019-01-08 17:54:17.303 INFO 4116 --- [nio-8080-exec-1] c.b.s.controller.SpringController : 请求线程:http-nio-8080-exec-1
2019-01-08 17:54:17.308 INFO 4116 --- [ callable-1] c.b.s.controller.SpringController : 异步线程:callable-1
2019-01-08 17:54:18.360 INFO 4116 --- [nio-8080-exec-2] c.b.s.controller.SpringController : Web async task completed
本节主要是讲解了异步请求的使用及相关配置,如超时,异常等处理。设置异步请求时,不要忘记设置超时时间。异步请求只是提高了服务的吞吐量,提高单位时间内处理的请求数,并不会加快处理效率,这点需要注意。
慕课手记:异步开发之异步请求
docs.spring.io#mvc-ann-async