一、背景
最近在做我的项目的过程中,有一个领取的场景,前端须要依据领取的后果,跳转到不同的页面中。而咱们的领取告诉是支付方异步告诉回来的,因而在收回领取申请后
无奈立刻获取到领取后果,此时咱们就须要轮训交易后果,判断是否领取胜利。
二、剖析
要实现后端将领取后果告诉给前端,实现的形式有很多种。
- ajax 轮训
- 长轮训
- websocket
- sse
......
通过思考,最终决定应用 长轮训
来实现。 而 Spring 的 DeferredResult
是一个异步申请,正好能够用来实现长轮训。而这个异步是基于 Servlet3
的异步来实现的,在Spring中DeferredResult后果会另起线程来解决,并不会占用容器(Tomcat)的线程,因而还能进步程序的吞吐量。
三、实现要求
前端申请 查问交易办法(queryOrderPayResult
),后端将申请阻塞住 3s
,如果在3s之内,领取告诉回调(payNotify
)过去了,那么之前查问交易
的办法立刻返回领取后果,否则返回超时了。
四、后端代码实现
package com.huan.study.controller;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import org.springframework.web.bind.annotation.GetMapping;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;import org.springframework.web.context.request.async.DeferredResult;import javax.annotation.PostConstruct;import java.util.Optional;import java.util.concurrent.ConcurrentHashMap;import java.util.concurrent.Executors;import java.util.concurrent.TimeUnit;import java.util.concurrent.atomic.AtomicInteger;/** * 订单控制器 * * @author huan.fu 2021/10/14 - 上午9:34 */@RestControllerpublic class OrderController { private static final Logger log = LoggerFactory.getLogger(OrderController.class); private static volatile ConcurrentHashMap<String, DeferredResult<String>> DEFERRED_RESULT = new ConcurrentHashMap<>(20000); private static volatile AtomicInteger ATOMIC_INTEGER = new AtomicInteger(0); @PostConstruct public void printRequestCount() { Executors.newSingleThreadScheduledExecutor() .scheduleAtFixedRate(() -> { log.error("" + ATOMIC_INTEGER.get()); }, 10, 1, TimeUnit.SECONDS); } /** * 查问订单领取后果 * * @param orderId 订单编号 * @return DeferredResult */ @GetMapping("queryOrderPayResult") public DeferredResult<String> queryOrderPayResult(@RequestParam("orderId") String orderId) { log.info("订单orderId:[{}]发动了领取", orderId); ATOMIC_INTEGER.incrementAndGet(); // 3s 超时 DeferredResult<String> result = new DeferredResult<>(3000L); // 超时操作 result.onTimeout(() -> { DEFERRED_RESULT.get(orderId).setResult("超时了"); log.info("订单orderId:[{}]发动领取,获取后果超时了.", orderId); }); // 实现操作 result.onCompletion(() -> { log.info("订单orderId:[{}]实现.", orderId); DEFERRED_RESULT.remove(orderId); }); // 保留此 DeferredResult 的后果 DEFERRED_RESULT.put(orderId, result); return result; } /** * 领取回调 * * @param orderId 订单id * @return 领取回调后果 */ @GetMapping("payNotify") public String payNotify(@RequestParam("orderId") String orderId) { log.info("订单orderId:[{}]领取实现回调", orderId); // 默认后果产生了异样 if ("123".equals(orderId)) { DEFERRED_RESULT.get(orderId).setErrorResult(new RuntimeException("订单产生了异样")); return "回调解决失败"; } if (DEFERRED_RESULT.containsKey(orderId)) { Optional.ofNullable(DEFERRED_RESULT.get(orderId)).ifPresent(result -> result.setResult("实现领取")); // 设置之前orderId toPay申请的后果 return "回调解决胜利"; } return "回调解决失败"; }}
五、运行后果
1、超时操作
页面申请 http://localhost:8080/queryOrderPayResult?orderId=12345
办法,在3s之内没有DeferredResult#setResult没有设置后果,间接返回超时了。
2、失常操作
页面申请 http://localhost:8080/queryOrderPayResult?orderId=12345
办法之后,并立刻申请http://localhost:8080/payNotify?orderId=12345
办法,失去了正确的后果。
六、DeferredResult运行原理
- Controller 返回一个 DeferredResult 对象,并且把它保留在一个能够拜访的内存队列或列表中。
- Spring Mvc 开始异步解决。
- 同时,DispatcherServlet 和所有配置的过滤器退出申请解决线程,但Response(响应)放弃关上状态。
- 应用程序从某个线程设置 DeferredResult,Spring MVC 将申请分派回 Servlet 容器。
- DispatcherServlet 再次被调用,并以异步产生的返回值复原解决 。
六、注意事项
1、异样的解决
能够通过 @ExceptionHandler
来解决。
2、异步过程中的拦截器。
能够通过 DeferredResultProcessingInterceptor
或者 AsyncHandlerInterceptor
来实现。须要留神看拦截器办法上的正文,有些办法,如果调用了setResult
等是不会再次执行的。
配置:
/** * 如果加了 @EnableWebMvc 注解的话, Spring 很多默认的配置就没有了,须要本人进行配置 * * @author huan.fu 2021/10/14 - 上午10:39 */@Configurationpublic class WebConfig implements WebMvcConfigurer { @Override public void configureAsyncSupport(AsyncSupportConfigurer configurer) { // 默认超时工夫 60s configurer.setDefaultTimeout(60000); // 注册 deferred result 拦截器 configurer.registerDeferredResultInterceptors(new CustomDeferredResultProcessingInterceptor()); } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new CustomAsyncHandlerInterceptor()).addPathPatterns("/**"); }}
七、残缺代码
https://gitee.com/huan1993/spring-cloud-parent/tree/master/springboot/spring-deferred-result
八、参考链接
- https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async-deferredresult