詳解Spring DeferredResult異步操作使用場景
為什麼使用DeferredResult?
API接口需要在指定時間內將異步操作的結果同步返回給前端時;
Controller處理耗時任務,並且需要耗時任務的返回結果時;
當一個請求到達API接口,如果該API接口的return返回值是DeferredResult,在沒有超時或者DeferredResult對象設置setResult時,接口不會返回,但是Servlet容器線程會結束,DeferredResult另起線程來進行結果處理(即這種操作提升瞭服務短時間的吞吐能力),並setResult,如此以來這個請求不會占用服務連接池太久,如果超時或設置setResult,接口會立即返回。
使用DeferredResult的流程:
- 瀏覽器發起異步請求
- 請求到達服務端被掛起
- 向瀏覽器進行響應,分為兩種情況:
- 調用DeferredResult.setResult(),請求被喚醒,返回結果
- 超時,返回一個你設定的結果
- 瀏覽得到響應,再次重復1,處理此次響應結果
給人一種異步處理業務,但是卻同步返回的感覺。
場景
瀏覽器向A系統發起請求,該請求需要等到B系統(如MQ)給A推送數據時,A才會立刻向瀏覽器返回數據;
如果指定時間內B未給A推送數據,則返回超時。
Demo代碼
接口代碼:
/get是調用A系統的接口返回數據;
/result模擬B系統向A推送數據進行setResult。
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.async.DeferredResult; @RestController @RequestMapping(value = "/deferred-result") public class DeferredResultController { @Autowired private DeferredResultService deferredResultService; /** * 為瞭方便測試,簡單模擬一個 * 多個請求用同一個requestId會出問題 */ private final String requestId = "haha"; @GetMapping(value = "/get") public DeferredResult<DeferredResultResponse> get(@RequestParam(value = "timeout", required = false, defaultValue = "10000") Long timeout) { DeferredResult<DeferredResultResponse> deferredResult = new DeferredResult<>(timeout); deferredResultService.process(requestId, deferredResult); return deferredResult; } /** * 設置DeferredResult對象的result屬性,模擬異步操作 * @param desired * @return */ @GetMapping(value = "/result") public String settingResult(@RequestParam(value = "desired", required = false, defaultValue = "成功") String desired) { DeferredResultResponse deferredResultResponse = new DeferredResultResponse(); if (DeferredResultResponse.Msg.SUCCESS.getDesc().equals(desired)){ deferredResultResponse.setCode(HttpStatus.OK.value()); deferredResultResponse.setMsg(desired); }else{ deferredResultResponse.setCode(HttpStatus.INTERNAL_SERVER_ERROR.value()); deferredResultResponse.setMsg(DeferredResultResponse.Msg.FAILED.getDesc()); } deferredResultService.settingResult(requestId, deferredResultResponse); return "Done"; } }
import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.web.context.request.async.DeferredResult; import java.util.Map; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; @Service public class DeferredResultService { private Map<String, Consumer<DeferredResultResponse>> taskMap; public DeferredResultService() { taskMap = new ConcurrentHashMap<>(); } /** * 將請求id與setResult映射 * * @param requestId * @param deferredResult */ public void process(String requestId, DeferredResult<DeferredResultResponse> deferredResult) { // 請求超時的回調函數 deferredResult.onTimeout(() -> { taskMap.remove(requestId); DeferredResultResponse deferredResultResponse = new DeferredResultResponse(); deferredResultResponse.setCode(HttpStatus.REQUEST_TIMEOUT.value()); deferredResultResponse.setMsg(DeferredResultResponse.Msg.TIMEOUT.getDesc()); deferredResult.setResult(deferredResultResponse); }); Optional.ofNullable(taskMap) .filter(t -> !t.containsKey(requestId)) .orElseThrow(() -> new IllegalArgumentException(String.format("requestId=%s is existing", requestId))); // 這裡的Consumer,就相當於是傳入的DeferredResult對象的地址 // 所以下面settingResult方法中"taskMap.get(requestId)"就是Controller層創建的對象 taskMap.putIfAbsent(requestId, deferredResult::setResult); } /** * 這裡相當於異步的操作方法 * 設置DeferredResult對象的setResult方法 * * @param requestId * @param deferredResultResponse */ public void settingResult(String requestId, DeferredResultResponse deferredResultResponse) { if (taskMap.containsKey(requestId)) { Consumer<DeferredResultResponse> deferredResultResponseConsumer = taskMap.get(requestId); // 這裡相當於DeferredResult對象的setResult方法 deferredResultResponseConsumer.accept(deferredResultResponse); taskMap.remove(requestId); } } }
import lombok.Data; import lombok.Getter; @Data public class DeferredResultResponse { private Integer code; private String msg; public enum Msg { TIMEOUT("超時"), FAILED("失敗"), SUCCESS("成功"); @Getter private String desc; Msg(String desc) { this.desc = desc; } } }
測試
1. 超時
設置超時時間為8s,會一直阻塞8s,如果超時,返回默認超時的結果
8s過去,沒有setResult,返回超時
2. 進行setResult
在阻塞期間調用setResult,我這裡模擬的是B系統推送數據給A時拋異常失敗的情況,會立刻得到返回結果
總結:
當有前端需要一個耗時操作服務時,可以使用DeferredResult異步機制編寫代碼。如果不用此功能,要麼自己實現類似的功能。要麼是前端輪訓去拉處理的結果,開發量比較大。有瞭DeferredResult可以節省很多我們前後端的開發量。
到此這篇關於詳解Spring DeferredResult異步操作使用場景的文章就介紹到這瞭,更多相關Spring DeferredResult異步內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Java SpringMVC異步處理詳解
- springMvc中DeferredResult的long polling應用示例解析
- Java TimedCache 帶時間緩存工具類詳解使用
- SpringBoot 實現動態添加定時任務功能
- SpringBoot整合Redis將對象寫入redis的實現