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<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 */
@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
