Spring|Spring DeferredResult 异步请求
一、背景
最近在做项目的过程中,有一个支付的场景,前端需要根据支付的结果,跳转到不同的页面中。而我们的支付通知是支付方异步通知回来的,因此在发出支付请求后
无法立即获取到支付结果,此时我们就需要轮训交易结果,判断是否支付成功。
二、分析
要实现后端将支付结果通知给前端,实现的方式有很多种。
- 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
*/
@RestController
public class OrderController {private static final Logger log = LoggerFactory.getLogger(OrderController.class);
private static volatile ConcurrentHashMap> 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 queryOrderPayResult(@RequestParam("orderId") String orderId) {
log.info("订单orderId:[{}]发起了支付", orderId);
ATOMIC_INTEGER.incrementAndGet();
// 3s 超时
DeferredResult 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、超时操作 【Spring|Spring DeferredResult 异步请求】
页面请求
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 再次被调用,并以异步产生的返回值恢复处理 。
@ExceptionHandler
来处理。2、异步过程中的拦截器。 可以通过
DeferredResultProcessingInterceptor
或者 AsyncHandlerInterceptor
来实现。需要注意看拦截器方法上的注释,有些方法,如果调用了setResult
等是不会再次执行的。配置:
/**
* 如果加了 @EnableWebMvc 注解的话, Spring 很多默认的配置就没有了,需要自己进行配置
*
* @author huan.fu 2021/10/14 - 上午10:39
*/
@Configuration
public 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
推荐阅读
- Activiti(一)SpringBoot2集成Activiti6
- SpringBoot调用公共模块的自定义注解失效的解决
- 解决SpringBoot引用别的模块无法注入的问题
- 2018-07-09|2018-07-09 Spring 的DBCP,c3p0
- spring|spring boot项目启动websocket
- Spring|Spring Boot 整合 Activiti6.0.0
- Spring集成|Spring集成 Mina
- springboot使用redis缓存
- Spring|Spring 框架之 AOP 原理剖析已经出炉!!!预定的童鞋可以识别下发二维码去看了
- Spring|Spring Boot之ImportSelector