前提:SpringBoot + Lombok + spring-boot-starter-web 即可.
- 如下图所示,应该是一个最简单的controller的web请求处理方法:
同步
2. 但假设某个请求需要耗费大量的时间,那么,该请求的线程就会一直同步等待该次请求完成,才能被返回线程池,分配给下一个新的请求.
那么,如果我们能在controller层的方法中异步执行,另起一个线程去处理耗时任务,让该线程先返回线程池,那么它就可以继续处理下一个请求,从而增加web程序的吞吐量.
3. 一个简单的异步处理请求方法如下,使用了Callable对象:
Callable异步
该方法中,输出"主线程开始"的和输出"子线程开始"的将是两个线程,但Callable<String>被先行返回了.
对于浏览器访问者来说,该方法无论是否使用异步处理,效果就是请求了/order,然后加载了5秒后,获取了数据.
而如果使用了异步,在加载的5秒过程中,主线程已经去处理下一个请求了.
4. 上述方法虽然可以异步处理大部分请求,但如果是下面这种情况,则不适用:
A项目的收到请求后,将请求提交的数据放入消息队列.
B项目从消息队列取出数据后进行处理,处理完成后,将返回消息放回消息队列.
A项目的另一个线程一直从消息队列中获取数据,如果获取到了,就返回给之前的请求者.
那么,就可以使用DeferredResult对象来进行异步处理.
5. 我们先模拟下消息对列的环境:
创建Task任务对象类:
任务对象
该类封装了DeferredResult对象和收到的消息对象,以及一个是否超时的标志属性.
主要是为了在任务完成后方便取出每个请求消息对应的DeferredResult对象,返回消息给客户端.
创建MockQueue模拟队列类:
模拟队列
在该队列中,使用LinkedBlockQueue类来作为队列(因为阻塞队列的阻塞获取和阻塞放入方法比较适合该场景,并且LinkedBlockQueue的存取效率比ArrayBlockingQueue高),分为 接收请求队列 和 完成队列.
然后有对应的将任务放入请求队列和从完成队列中获取任务的方法,供其他类调用.
然后在该类的构造方法中,启动execute()方法.
execute()方法执行了一个新的线程,不断的从接收队列中取出数据,然后处理若干秒后,将成功数据放入成功队列.
此处,有一个超时标志判断,如果任务超时,可以中断该任务的进行,在正常的service中,可以替换为数据库回滚等操作.
此处,从任务队列中获取新请求任务时,如果队列为空,会释放掉锁,一直阻塞,等到队列中有了数据,才会被唤醒.
创建Controller方法:
该方法中,创建一个新的DeferredResult对象(该构造函数中可以设置超时时间/超时返回对象).
然后将DeferredResult/请求消息(此处直接写了个"test任务")/是否超时 封装为一个Task对象.
然后设置DeferredResult对象的超时处理线程,当处理超时后,就会执行onTimeout()方法中传入的Runnable线程,处理超时.
此处,在超时任务中,我将该任务的超时标志设置为true,表示该任务已经超时;那么在处理方法(模拟队列的put()方法)中就会判断到,然后中断该任务的处理.注意,该标志对象的设置是需要加锁的.
然后将该对象放入模拟任务队列,并直接返回DeferredResult.
当然,此时,浏览器并不会获得任何消息,还是加载中的状态.
而当模拟队列收到数据后,execute()方法中的阻塞队列获取方法,将不再被阻塞,将会处理该任务,并将完成消息放入完成队列.
创建完成队列监听器:
完成队列监听器
该类实现了spring的监听器,监听了容器启动完毕事件.
当容器启动完毕后,我们就在方法中开启了一个无限循环的线程,不断地从完成队列中阻塞地获取数据.
如果成功获取到,则将对应的数据,使用DeferredResult对象的setResult()方法,返回给客户端.
此时,客户端就会加载完毕,收到消息.
6. 至此,一个最简单的DeferredResultDemo已经全部走通.
- 组件:
- controller层方法,接收请求、新建延期返回对象、设置超时方法、任务入队、返回
- MockQueue队列,任务入队,处理任务、判断是否超时、放入完成队列
- QueueListener完成队列监听器,监听完成队列,获取到完成的数据后,使用延期对象返回
- Task任务,三个属性:延期返回对象、任务、是否超时
- 流程:
- controller层收到请求,消息入队
- 队列监听到消息入队,处理消息
- 如果超时任务触发,将超时状态改为true,并直接返回浏览器任务超时
- 队列处理完任务后,判断是否超时,如果超时,中断任务,否则将任务加入完成队列
- 完成队列监听器监听到消息入队,取出消息,使用延期返回对象返回
7. 接下去是DeferredResult对象的源码简析:
该对象的属性:
DeferredResult对象的属性
RESULT_NONE:默认的返回对象,一个空的Object,无任何意义,只是一个空的标识.
Logger:日志对象.
timeout:异步处理超时时间,毫秒.
timeoutResult:超时后返回给浏览器的对象.
timeoutCallback:超时后调用的线程.
complectionCallBack:完成后调用的线程.
resultHandler:对返回值进行处理的一个策略接口.该接口就一个handleResult(Object result)方法,可以对该返回值进行处理.
result:真正的返回值,其默认就是RESULT_NONE这个空对象.
expired:是否失效,默认为false.
该对象的构造方法:
DeferredResult构造方法
DeferredResult():一个空的方法,调用了下面的一个重载的构造方法 ,将空对象同样赋值给了timeoutResult.
DeferredResult(Long timeout):传入超时时间,对timeoutResult相同的处理.
DeferredResult(Long timeout,Object timeoutResult):超时时间/超时后返回对象
还有一些简单的方法:
一些简单方法
isSetOrExpired():判断该对象是否设置了result的值或失效;
hasResult():判断是否自己设置了result的值;
getResult():返回result的值,如果该对象没有自己设置过result,则为null.
又一些简单方法:
又一些简单方法
getTimeoutValue():获取设置的超时时间;
onTimeout(Runnable callback):设置当超时时执行的Runnable线程;
onCompletion(Runnable callback):设置处理完成时执行的Runnable线程.
setResultHandler方法:
setResultHandler方法
该方法的参数DeferredResultHandler对象是DeferredResult对象的内部接口如下:
DeferredResultHandler
该方法首先断言参数不为空,然后如果对象已经失效,则不处理;其判断用了和单例模式中相同的双重检查,保证线程安全.
如果还未设置result,就暂时将该resultHandler赋值给自己;
如果已经自己调用setResult或setErrorResult方法设置了DeferredResult的返回值,就直接调用该策略接口的handleResult(Object result)方法;
setResult和setErrorResult方法,其实都是执行了setResultInternal方法,没有任何区别:
setResultInternal
如果已经设置了result或失效,返回false,同样的双重检查;
否则,设置result,并当resultHandler不为空,执行给策略接口;
此外,还有一个getInterceptor()方法,涉及到其对应的拦截器,容后再提;
8. springWeb的异步支持配置
在SpringBoot中,只需要新建一个类,继承WebMvcConfigurerAdapter类,并注解上@Configuration即可:
异步支持配置
该说的都在图片的注解中了,其中,注册拦截器的方法传参为...,也就是可以传多个拦截器链;
9. 异步拦截器的具体实现
可以实现DeferredResultProcessingInterceptor接口,也可以直接继承DeferredResultProcessingInterceptorAdapter适配器类来使用,如果使用适配器类,可以不重写不想要的方法,其他都是一样的.
此外,有一个TimeoutDeferredResultProcessingInterceptor类,只是重写了超时方法,setErrorResult了一个异常进去.
拦截器的一些方法:
其执行顺序和图片顺序相同;
10. DeferredResult对象的最后一个getInterceptor()方法:
超时处理方法:
如果DeferredResult对象的timeoutCallback不为空,执行超时处理线程;
如果timeoutResult对象不为空,设置该对象为真正的返回值,并且,以此为依据,判断已经处理过了,就返回false,表示不再执行后续的拦截器链了.
如果为空,继续执行下个拦截器
处理完成后执行的方法:
将该对象设置为失效,并在completionCallback线程不为空时,运行.
在提一句,该过程还有很多类参与,具体可以运行后debug自行查看.
-----------------------------------------END-------------------------------------------