今天我们就来看看SpringDeferredResult,使用起来非常简单,答案可深可浅。浅的是直接回答它的功能,深的是原理,而这个原理其实需要涉及到tomcat(默认的tomcat是web容器)。我们先看看DeferredResult的作用和用法。DeferredResultDeferredResult的用处其实是基于Servlet3.0对异步请求的支持。我们来看这样一个场景:当前controller中有一个方法A,其内部逻辑依赖于redis中的一个值。如果redis中有值,你可以获得返回值。如果没有值,此时没有任何返回,只能返回null,插入redis的值依赖于另外一个后台线程。对于正常的实现,我们肯定可以想到一种轮询的方案,就是浏览器一直轮询方法A,直到有值就停止轮询,但是有时候过于频繁的轮询会给服务器带来压力。而这时候DeferredResult就可以登场了,我们从名字就可以知道:deferred的结果。下面简单看一下使用方法:可以看到,只需要用DeferredResult包裹需要返回的值,然后设置超时时间和超时值即可。该示例设置5s。让我们试验一下。如果后台线程休眠100ms,没有超过设置的超时时间,100ms后立即返回值:如果后台线程休眠10000ms,则超过设置的超时时间,然后等待5秒后,返回是:正如你所看到的,这个功能符合我们的预期。关于DeferredResult还有一个很重要的点:请求处理线程(也就是tomcat线程池中的线程)不会等到调用DeferredResult#setResult()后才释放,而是直接释放。也就是说,tomcat线程在安排好DeferredResult的一些配置后,不会等待逻辑处理完毕(DeferredResult#setResult()调用或超时)。而是直接释放,让tomcat线程回收到线程池,可以响应其他请求,而不用傻傻地阻塞等待DeferredResult#setResult()被调用或超时。我们都知道tomcat的线程池大小是有限制的。如果我们的某些业务逻辑处理慢,会逐渐把tomcat线程占满,导致无法处理新的请求,所以我们会把一些处理慢的业务放到业务线程中。在池中处理,但是如果只是简单地在业务线程池中处理,我们无法知道它是什么时候处理的,也无法将处理后的结果与之前的请求进行匹配,所以常用的方法是轮询。DeferredResult的方法类似于只是把事情安排好,不管事情做没做,tomcat线程就释放了。注意,此时不会给请求者(如浏览器)任何响应,而是将请求存储在一旁,我们先不管它,等后面有结果了,再带上之前的请求,用值响应给请求者。最后是自下而上的处理。如果应该返回的值在超时后还没有被填入,那么请求者将响应自下而上的值。我画个图(注意这不是源码分析图,只是为了更好的理解DeferredResult的逻辑):看到这里,你一定明白了DeferredResult的用法和它的一些特点吧?接下来我们来看一下原理。DeferredResult原理分析这个原理说来话长。如果你真的想完全理解它,你需要同时理解Tomcat和SpringMvc的原理。为避免过度开发而跑题,本文主要讲述DeferredResult的原理。以后有时间再把Tomcat和SpringMvc的原理补全。有需要的可以在评论区留言。如果多了,我马上安排。我打算主要用文字来描述一下整体的流程,后面会跟着一些源码(如果源码都上传了,怕会头晕,毕竟调用链接还是有点长)。我们可以理解。如果你真的对源代码感兴趣,你可以自己研究。开始分析一个请求,首先要经过tomcat的调度,然后才会来到我们的Spring。当我们在Spring中处理完业务逻辑后,tomcat会将相关响应返回给客户端(比如浏览器)。所以,要想完成DeferredResult的功能,就需要tomcat的配合。它需要知道这是一个异步请求。在没有设置结果和没有超时之前,它不能返回响应给客户端。返回给客户端。我再把示例代码贴出来,我们根据这段代码分析一下:如果你用浏览器请求调用这个方法,那么此时tomcat肯定不知道这是一个异步处理请求,只会处理为一个正常的请求。所以正常情况下,tomcat调用SpringMvc定义的DispatcherServlet#doDispatch来处理请求。根据url找到controller处理的方法,也就是上面的实例方法。如果正常处理的话,这个方法应该会直接把我们new的deferredResult返回给浏览器,但是显然刚刚演示的结果不是这样的。中间发生了什么?这就涉及到SpringMvc的内容了。当通过反射调用controller中的方法获取返回值时,需要根据返回值的类型调用不同的returnValueHandlers进行处理。先解释一下为什么需要returnValueHandlers来处理返回值。大家想一想,如果我们的返回类型可能是视图名,也可能是@RequestBody标记的值,那么两者的处理方式肯定是不一样的,所以我们需要对返回值进行处理。而Spring有一个returnValueHandler专门用来处理DeferredResult类型的返回值,即DeferredResultMethodReturnValueHandler。你可以在这里做一些操作。可以看到支持处理的返回类型是DeferredResult:从这里我们可以知道DeferredResult返回类型的前半部分处理其实和正常的返回值是一样的。区别在于方法的返回。值被特殊处理。简单说一下这个returnValueHandler触发的操作:它调用了tomcat中的Request#startAsync方法,也传递了超时时间。这个操作是为了让tomcat明白当前请求是一个异步请求,这样tomcat就不会直接把新的deferredResult返回给客户端,也不会销毁当前的请求和响应。而是会把处理这个请求的Processor暂时保存起来(简单理解就是每个请求都有对应的processor,这是tomcat中的概念),放在waitingProcessors中。然后tomcat线程池就溜走了,经过处理,就无所谓了。至此,前半部分的准备工作就完成了,有两条处理路径:一是超时,二是在超时前调用DeferredResult#setResult成功返回。我们来看看两者的区别:请求超时。从前面我们了解到,异步请求已经放在waitingProcessors中,也设置了超时时间,tomcat会有一个线程每1秒遍历一次。waitingProcessors中的processor检查是否过期:如果发现过期了,就会重新往tomcat中的线程池中丢任务,但是这个任务不一样,可以看到SocketEvent是TIMEOUT。这样当线程池运行到这个任务时,就会知道请求任务超时了。这时,它会在请求中插入超时值(具体是通过之前设置的DeferredResult相关的拦截器中的handleTimeout方法)。这个setResultInteral最终会把值保存到request中(实际上是在请求管理的asyncManager中),重新将request输入到DispatcherServlet#doDispatch()中进行处理。可以看到这次请求又经过了doDispatch,所以DeferredResult类型的方法会经过两次doDispatch逻辑,第二次进来的时候发现请求中的asyncManager已经放入了价值。因为这个值已经被塞进去了,后面处理的时候,会走到这个if,这里controller中原来的方法的反射内容会被替换掉,并且会创建一个新的反射内容。新的反射方法的调用会直接返回插入的结果。看到没有,这就是所谓的embedding,所以还是复用了正常的处理流程,但是插入的超时值是后面执行的时候得到的,既然是普通类型,那么可以这样处理应该是,终于返回给浏览器了。请求没有超时,DeferredResult#setResult其实DeferredResult#setResult和请求超时的处理逻辑是完全一样的,不同的是触发的来源不同。请求超时是tomcat线程池扫描到超时,然后通过handleTimeout调用setResultInternal插入超时默认值,重新触发DispatcherServlet#doDispatch()处理。而DeferredResult#setResult是我们的应用线程主动插入值:其实也是调用setResultInternal,然后重新触发DispatcherServlet#doDispatch()处理。所以两者背后的处理逻辑是一模一样的!补充其实还有很多源码和比较重要的地方没有说到。主要是字太多展开,这里稍微提一下。比如WebAsyncManager,这个东西顾名思义就是一个异步管理器,每一个请求都会配对一个新的,上面说了。一个普通的请求只会进入一次doDispatch方法,但是一个异步的DeferredResult会进入两次。这意味着第二个条目与第一个条目是同一个WebAsyncManager。有些信息需要缓存对齐,也有异步处理的逻辑,比如设置一个值重新触发doDispatch等。tomcat的请求中其实有一个状态机,这里需要了解一下tomcat的相关概念。比如在开始异步处理的时候会调用:这个action底层会调用asyncStateMachine来操作:那么这个状态机最终会通知CoyoteAdapter,这也是tomcat中的一个概念。tomcat连接器通过调用CoyoteAdapter#service来调用容器,其中包含异步请求处理publicvoidservice(org.apache.coyote.Requestreq,org.apache.coyote.Responseres)throwsException{...省略部分代码....try{//解析并设置Catalina和配置特定的//请求参数postParseSuccess=postParseRequest(req,request,res,response);if(postParseSuccess){//调用容器调用容器connector.getService().getContainer().getPipeline().getFirst().invoke(request,response);}if(request.isAsync()){//这里是状态机判断的async=true;.....省略部分代码....}else{//如果不是异步,则结束request.finishRequest();response.finishResponse();}}catch(IOExceptione){//忽略}finally{....省略部分代码....if(!async){updateWrapperErrorCount(请求,响应);//如果不是异步则回收request和responserequest.recycle();响应.回收();}}}最后大概理解到这一层就差不多了,如果想更深入的了解还是需要掌握很多内容来简单的概括一下SpringDeferredResult的话:如果返回值类型是DeferredResult,说明它是异步请求,tomcat线程不会等到应用处理完毕或超时,而是立即释放线程。未处理的请求会暂存,tomcat知道是异步请求,直到tomcat线程扫描到请求超时或应用线程将结果插入DeferredResult时才会响应客户端。主要原理是Spring有一个DeferredResultMethodReturnValueHandler。如果识别到返回值是DeferredResult类型,就会将请求标记为异步请求(这是基于tomcat对servlet3.0异步请求的支持),并暂存该请求。tomcat每秒扫描等待异步请求是否超时触发是否返回默认值,还是应用线程手动将值插入DeferredResult触发返回。具体的返回逻辑其实就是使用了两次Spring的DispatcherServlet#doDispatch来重新分发。使用一个WebAsyncManager对象绑定请求来管理异步操作,比如保存返回值,保存上下文等。DispatcherServlet#doDispatch的处理逻辑根据WebAsyncManager的状态不同,来判断实际的异步请求,以判断是否有异步返回值等,如果有返回值,通过新建反射内容替换之前请求对应的controller方法,通过嵌入花草树木替换反射返回的结果。好吧,就是这样。前面说了,这个其实涉及到tomcat的底层原理,然后是SpringMvc的原理。说实话,是个不错的面试题,可以延展很多~
