1.SpringMvc 异步请求能解决什么问题
1.1 在霖久我写过一篇关于Apollo配置中心实现原理的文档,其中关于管理界面发布配置后客户端拉取配置是这样描述的:
其中的核心是通过DeferredResult来实现的,而DeferredResult就是Springmvc实现异步请求的一种放式.
1.2 开发中我们也会遇到这样的问题,我们需要将接口的逻辑处理完后,将结果返回给页面,但接口逻辑太复杂,响应时间太长,或则接口需要调用其他系统的接口,而其他系统的接口又不给力,这样导致Tomcat线程连接无法被释放,如果连接占用过多,服务器就很可能无法及时响应每个请求;极端情况下如果将线程池中的所有连接耗尽,服务器将长时间无法向外提供服务.
1.3 开发中我们可能也会遇到这样的问题,我们想实时向页面展示服务器变动的数据,也既实时推送
以上种种问题,都可以通过Springmvc提供的异步请求来解决
2.1 Servlet 3.0 之前,一个普通 Servlet 的主要工作流程大致如下:
2.1.1 Servlet 接收到请求之后,可能需要对请求携带的数据进行一些预处理;
2.1.2 调用业务接口的某些方法,以完成业务处理;
2.1.3 根据处理的结果提交响应,Servlet 线程结束。
2.2 早期Servlet请求流程如下:
从上图可以看出:请求过来后,从主线程池获取一个线程,处理业务,响应请求,然后将线程还回线程池,整个过程都是由同一个主线程在执行。
这里存在一个问题,通常 web 容器中的主线程数量是有限的,若执行业务的比较耗时,大量请求过来之后,主线程被耗光,新来的请求就会处于等待状态。
3 在servlet3.0之后做了调整,引入了异步Servlet的概念。
2.3.1 Servlet 接收到请求之后,可能需要对请求携带的数据进行一些预处理;
2.3.2 调用业务接口的某些方法过程中request.startAsync()请求,获取一个AsyncContext
2.3.3 紧接着servlet线程退出(回收到线程池),但是响应response对象仍旧保持打开状态,新增线程会使用AsyncContext处理并响应结果。
2.3.4 AsyncContext处理完成触发某些监听通知结果
2.4 Servlet 3.0异步处理流程
2.4.1 在主线程中开启异步处理,主线程将请求交给其他线程去处理,主线程就结束了,被放回了主线程池,由其他线程继续处理请求。
2.5 值得注意的是, 对于单次访问来讲,同步的servlet相比异步的servlet在响应时长上并不会带来变化,但对于高并发的服务而言异步servlet能增加服务端的吞吐量因为释放的是请求处理线程, 当前的请求线程被释放了,它可以接着去处理别的请求.但对于浏览器而言页面此时并没有得到响应,响应结果是通过异步线程执行,当后端得到返回结构后,将结果重新Dispatcher到浏览器,这就是异步的过程.
2.6 下面通过一段代码,展示Servlet是如何实现异步请求的:
@RequestMapping("/test8")
public String test8(HttpServletRequest request){
AsyncContext asyncContext = request.startAsync();
asyncContext.start(new TestTask(asyncContext));
asyncContext.addListener(new AsyncListener() {
@Override
public void onComplete(AsyncEvent event) throws IOException {
System.out.println("测试任务完成");
}
@Override
public void onTimeout(AsyncEvent event) throws IOException {
}
@Override
public void onError(AsyncEvent event) throws IOException {
}
@Override
public void onStartAsync(AsyncEvent event) throws IOException {
}
});
return "成功";
}
public static class TestTask implements Runnable {
private AsyncContext asyncContext;
public TestTask(AsyncContext asyncContext) {
this.asyncContext = asyncContext;
}
@SneakyThrows
@Override
public void run() {
Thread.sleep(6000);
System.out.println("run完成");
}
}
2.6.1 这个请求的结果是立即返回结果,而浏览器不会等待6s中才得到响应.
2.6.2 看到这里的同学可能会说,这个很简单,我们在请求中使用一个异步线程不也能达到同样的效果吗,因为这里并没有说要拿到异步处理的结果.
2.6.3 展示这个代码我只是想让大家知道Servlet3.0是通过AsyncContext类来实现异步的,而Springmvc底层实现异步是依赖AsyncContext类的,Springmvc在此基础上进行了封装,他会将异步线程的结果重新Dispathcer浏览器
2.6.4 如果对异步Servlet没有了解,那么接下来的源码时刻大家可能就难以理解.
3.1 Springmvc实现异步请求的方式有多种,我主要讲解DefferedResult的实现方式, 因为只有了解了其中一种方式的实现原理,其他方式也就很好理解了, 主要从以下几个方面讲解
3.1.1 DefferedResult的使用
3.1.2 DefferdResult实现的源码分析
3.13 在分析源码时会说明使用了java的哪些设计模式
3.1.4总结DefferResult实现的过程
3.1.5 我会在讲解DefferResult实现原理的时候,举一反三, 扩展其他异步方式的实现原理.
3.1.6 总结其他异步请求的实现过程, 为大家分析几种异步请求的区别
3.2 展示一段DefferResult使用的代码
@RequestMapping("/test3")
public DeferredResult test3(){
final DeferredResult dr = new DeferredResult();
executorService.execute(()->{
try {
Thread.sleep(6000);
dr.setResult("成功");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
return dr;
}
3.2.1 这个请求结果会在6s后响应给客户端,但Servlet主线程并不是在6s后释放的,而是在请求刚开始进行的时候就已经释放,这样提高了吞吐量
3.2.2 注意在线程池的任务中,我调用了setResult()方法
3.2.3 当然setResult也可以在其他请求或线程中调用,比如Apollo的配置实现就是在另一个请求中调用的.
3.3 DefferedResult的源码分析:
3.3.1 Springmvc的请求源码从DispatcherServlet#doDispatch()
开始分析, 这是基于Springmvc基础了解和Servlet特性得出的,不知道的请自行研究Tomcat是初始化Servlet的.
3.3.2 我们先看 WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
从servletRequest中获取asyncManager对象,没有的话先创建再存入servletRequest对象中.
3.3.3 ha.handle(), 这是核心代码, springmvc请求参数和返回参数的解析,requestMapping的映射, adapter的适配, web的configer, view的渲染, dispatcher等都是在这个方法中.
3.3.4 实际会调用RequestMappingHandlerAdapter类中的handleInternal,这其实是springmvc实现流程中一步,根据requestMapping先走适配器Adapter,适配器适配对应的RequestMappingHandler, 这其中用到了适配器设计模式.
3.3.5 WebAsyncUtils.getAsyncManager(request);取出本次请求的WebAsyncWebRequest对象,这个对象是将本次请求的资源属性做统一管理,其中有一个AsyncWebRequest属性,保存了请求url等信息,在之后中会使用.
asyncManager.hasConcurrentResult(),获取请求的异步结果,由于本次请求没有设置异步返回结果,所以不会进入这部分代码,只有dr.setResult方法执行完成后,此时已经有异步结果了会走这部分代码
3.3.6 其中的invocableMethod.wrapConcurrentResult(result)很重要,是将异步result通过构造器,传递给Callable的call方法,大家要先记住这部分代码,我会在之后分析如何通过反射执行Callable的call方法的。
3.3.7 invokeForRequest会得到请求的返回值,由于请求中返回的是DefferedResult
3.3.8通过handler处理返回值,大家仔细看这个handler是一个composite组合对象,我在讲Apollo配置时候说过对于配置资源Apollo就使用了composite将多个资源处理handler组合到commposite中,这里用到了组合者设计模式
3.3.9 由于我们返回的是DefferedResult对象,所以是被DeferredResultMethodReturnValueHandler处理,我们直接进入这个Handler
3.3.10 这里有一段转换类型的代码,是将ListenableFuture和CompletionStage都转成DeferredResult对象,大家先记住这段代码,我会在扩展中为大家解析这段代码。
3.3.11 开始进行DefferedResult的处理,核心处理来了,仔细看
3.3.12 这里其实展示了DefferedResult其他API的用法,比如设置超时时间,错误回调,完成回调,我会在扩展中为大家解析。
3.3.13 startAsyncProcessing是开启Servlet异步,将Sevlet主线程回收,以便接收其他请求,这是核心代码,接下来我们对这个方法进行深入研究
3.3.14deferredResult.setResultHandler方法由于本次调用还没有返回结果,所以会走红框中的逻辑重要一步,
this.resultHandler=resultHander很重要,先是赋值,将当前的DefferedResult对象和resultHandler建立关系,当DefferedResult对象有异步结果时,根据lamdba表达式会执行对应的resultHandler.
3.3.15 我们先解析startAsyncProcessing, 由于目前没有调用setResult, 所以有这段代码this.concurrentResult = RESULT_NONE;是为了调用3.1.22中的deferredResult.setResultHandle方法中不走lambda表达式中的逻辑,因为业务逻辑没有处理完,不需要响应浏览器。
3.3.16 这里通过 getRequest().startAsync开启了异步Servlet,同时返回了AsyncContext对象,大家还记得我在2.1.6异步Servlet实现的代码中,就是通过AsyncContext实现的异步Servlet。 其中this.asyncContext.addListener典型的使用了观察者模式,将当前事件源添加到监听器中。
3.3.17 之后进入RequestFacade中的startAsync,这里用到了门面模式。
3.3.18 进入Request的startAsync,大家想一下为什么先用门面模式进入门面对象,然后再进入request,为什么不是直接进入Request,这样设计的好处是什么。
3.3.19 action方法是真正执行异步Servlet。留个小问题,下面红框中用到了什么设计模式?
3.3.20 由于本次请求没有参数,所以会走param==null中的action
3.3.21由于本次并没有setResult,只是开启异步,而无需执行dispatch,所以这段代码不会执行,那么此时异步Servlet已经开启,主线程已经回收,服务器Servlet连接得到了释放。注意此时DispatcherServlet里的拦截器、Filter等等都马上退出主线程,但是response仍然保持打开的状态
3.3.22 到这里,本次请求在没有执行setResult的所有逻辑结束,这里只是实现了异步,下面我们解析当setReslut时是如何将异步结果返回给浏览器的。
3.4.1 当dr.setResult执行时,会调用setResultIntenal,大家还记得3.3.13中的deferredResult.setResultHandler方法吗,这两个方法名字虽然是一样的,但参数不同,是在DefferedResult两个不同的方法,这点注意
3.4.2 由于此时已经有返回结果,resultToHandle == RESULT_NONE为false,就会调用resultHandler.handleResult, 而resultHandler是一个函数式接口,所以就会执行3.3.13中的lamdba表达式,
所以我们回到3.3.13中进行源码分析。
3.4.3 核心是setConcurrentResultAndDispatch方法中的asyncWebRequest.dispatch方法,大家还记得这个asyncWebRequest吗,他是请求的资源管理器,包含本次请求的所有信息。
3.4.4 还记得这个asyncContext吗,通过asyncContext.dispatch()进行重定向
3.4.5 有子类AsyncContextImpl具体实现,path = sr.getRequestURI();就是获取请求的url,由于sr是来源于本次的请求asyncWebRequest的,所以这个path就是本次请求的路径/banner/test3,这个很好理解。给大家留个小问题,Apollo配置中心的setResult是在另一个请求中设置的,那这个path是什么呢,这个大家要好好理解,只有这样,才能真正理解3.1.13中的lamdba表达式,这个很重要。
3.4.6 在AsyncRunnable中执行dispatcher
3.4.7 由于AsyncRunable实现了Runable接口,而上面已经开启了异步线程,所以我们直接看run方法
3.4.8 action方法我们在上面已经分析过了,我们直接看applicationDispatcher.dispatch,将本次请求的参数进行跳转,然后invoke,这部分其实是进入tomcat的源码了。
3.4.9 基于Tomcate的特性,我们核心关注这个方法,因为ApplicationFilterChain会最终调用各个Servlet子类
3.4.10 看到servlet.service方法了没,看多Tomcat源码的人都应该知道这个方法意味着什么,对的,我们就会进入各个实现了HttpServet类的servlet中,而Springmvc的入口就是DispatcherServlet,而DispatcherSevlet就是继承了HttpServlet了
3.4.11 而DispatcherServlet中的service方法调用了doDispatch, 所以我们又回到了开始3.3.1中的源码
3.4.12 至此Springmvc进入了重定向,大家仔细看是内部重定向,是将setResult结果拿到的重定向,就像我们请求了返回值是【成功】的接口,这点很重要,绝不是重新的RequestMapping原来的请求,大家想想如果是这样,那不是陷入死循环了吗,接下来我们看在重定向过程中是如何将异步结果返回给浏览器的。
3.4.13 所以我们直接进入3.3.7 中的invokeForRequest,此时的returnValue就是【成功】,恭喜我们现在拿到了返回值
3.4.14 大家还记得我在3.3.6中说到,返回值本来是放在call方法中的,那是如何得到返回值的呢,请大家务必自己深入研究一下,因为如果我们出去面试说对Springmvc源码有深入研究,那大概率会被问道Springmvc是如何解析参数和返回结果的,那这个方法就是核心。
3.4.15 由于我在接口中添加了@RestController注解,所以会走RequestResponseBodyMethodProcessor,他是处理响应json的处理器。留个小问题,我们平时写controller时,都会加一个@RestController注解返回json对象,如果只写@Controller会怎么样
3.4.16 到这里,我们就相当于是进行了一次【成功】为结果的请求,path是 /banner/test3, 那么RequestResponseBodyMethodProcessor处理器会走正常请求流程,将结果返回给浏览器,完成了异步转同步操作,完美收关。
3.5 DefferResult的其他API的使用
3.5.1 onTimeOut()
3.5.1.1说明一下, 这个API是防止出现AsyncRequestTimeoutException异常的超时时间。其实我们在上面的源码中有提到这个超时时间。
3.5.1.2 我们先看一下早期实现推送http是如何轮询的。
3.5.1.3 大家可以看到,整个超时时间完全由前端控制,而服务器的资源响应本来只有服务器知道,能不能有一种方式,有服务器定义异步请求的响应时间呢,所以有了这个onTimeOut.
3.5.1.4 当然还有其他场景,比如Apollo,后端设置了异步请求的超时时间是60s,如果60S没有给客户端响应配置修改的信息,那么客户端就认为配置没有修改,那么会重新发起一次请求。
3.5.2 onCompletion方法
如果我们想在异步结果返回后做一些操作,那这个api是很好的被调用者,他由对应的处理器处理,上面代码中其实也有提到,想深入研究者,请自行研究,这个不是本次的重点,我打字也很累啊。
4.1 ListenableFutureTask 方式实现异步
4.1.1 下面通过一段代码展示ListenableFutureTask 方式实现异步
@RequestMapping("/test5")
public ListenableFutureTask test5(){
ListenableFutureTask listenableFutureTask=new ListenableFutureTask(new Callable() {
@Override
public Object call() throws Exception {
Thread.sleep(6000);
return "张三";
}
});
executorService.execute(listenableFutureTask);
return listenableFutureTask;
}
4.1.2 源码解析
4.1.2.1大家还记得我在3.3.10中有段代码是关于的,ListenableFutureTask 所以我们回到3.3.10,我们看到如果返回值是ListenableFutureTask 类型会被转成DefferResult处理,所以他们两者的实现原理是一样的
4.1.2.2 但有个问题,他们之间的数据是如何交互的呢,我们看下面这段代码,
ListenableFutureTask 是通过自己的回调监听器Listener,将value值传递给DefferedResult的。
4.1.2.3 还有一种CompletionStage类型实现异步,大家自己研究,你可以的。
4.2 Callable实现异步请求
4.2.1 下面通过一段代码展示Callable方式实现异步
@RequestMapping("/test6")
public Callable test6(){
Callable callable=new Callable() {
@Override
public Object call() throws Exception {
Thread.sleep(6000);
return "张三";
}
};
return callable;
}
4.2.2 源码分析
4.2.2.1 大家还记得3.3.9中的源码中,说了请求的返回结果是由不同的handler处理的,由于我们返回的是Callable类型,所以我们进入CallableMethodReturnValueHandler的源码
4.2.2.2 将Callable用WebAsyncTask包装
4.2.2.3 先开启异步Servlet,然后将Callable交给线程池taskExecutor处理,注意这个taskExecutor其实是SimpleAsyncTaskExecutor线程池,这是Spring默认的线程池
4.2.2.4 然后将result结果重定向,大家看这段代码的核心和之前的源码分析是不是一样,所以我不继续了。注意,这个重定向就是在任务中进行的,所以我们在使用Callable的时候不需要走自定义异步方法。
4.2.2.5 WebAsyncTask
底层就是通过Callable实现的。实际使用中,我并不建议直接使用Callable ,而是使用Spring提供的WebAsyncTask
代替,因为它包装了Callable,和DefferedResult一样拥有更丰富的API供使用。
4.3 WebAsyncTask 实现异步请求
4.3.1 下面通过一段代码展示WebAsyncTask 方式实现异步
@RequestMapping("/test7")
public WebAsyncTask test7(){
Callable callable=new Callable() {
@Override
public Object call() throws Exception {
Thread.sleep(6000);
return "张三";
}
};
WebAsyncTask webAsyncTask=new WebAsyncTask(callable);
return webAsyncTask;
}
4.3.2 源码分析
4.3.2.1 经过之前的分析,所以我们直接进入AsyncTaskMethodReturnValueHandler的源码
4.3.2.2 webAsyncTask由于包装了Callable,这里又get出来了
4.3.2.3之后的逻辑是这样的,是不是和Callable的处理方式是一样的,我不继续说了。
5.1 Springmvc实现异步请求的方式可以用下面的图表示,可以分成两个大类,因为根据源码分析,其他几种都可以归到这两个类中。
5.2 DefferedResult异步请求的流程图如下:
5.2.1从图中可以看出,异步结果是在自定义线程或接口中获取的
5.2.2 异步请求中间相比同步而言多了一次dispatcher重定向请求。
5.3 Callable异步请求的流程图如下:
5.3.1 从图中可以看出DefferedResult和Callable的区别在于他们使用的线程池不同
5.4 留个思考题,开发中我们希望项目中的线程进行统一的管理,我们可以让他们都使用自定义线程池吗,
如果可以,要怎么做呢?
6.1 异步回调
6.2 实时推送
6.3 请求线程间通信