使用DeferredResult异步处理SpringMVC请求

前提:SpringBoot + Lombok + spring-boot-starter-web 即可.

  1. 如下图所示,应该是一个最简单的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任务,三个属性:延期返回对象、任务、是否超时

  • 流程:
  1. controller层收到请求,消息入队
  2. 队列监听到消息入队,处理消息
  3. 如果超时任务触发,将超时状态改为true,并直接返回浏览器任务超时
  4. 队列处理完任务后,判断是否超时,如果超时,中断任务,否则将任务加入完成队列
  5. 完成队列监听器监听到消息入队,取出消息,使用延期返回对象返回

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-------------------------------------------

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值