科技新知
Spring Web 異步 Api
在設計網頁應用時,總會有某些功能,是特別消耗時間的,例如我們的應用要提供報表,或長時間搜索。如果,我們的 Web Api 的連結,要強制客戶端等待結果,那麼中途斷線需要重做的機會就變得很高,客戶端的體驗一定不太好。
面對這些情況,我們最好就把原本一個 API 功能分為三個 API 去做。
- 工作生成 API
- 查詢狀態 API
- 查詢結果 API
如果大家有信心,可以把2和3混合在一起,對於客戶端,也是一件好事。不過,2,3 因為回傳的結構可能不一樣,分開處理,程式碼會更易讀。
以下,筆者就以一個模擬報表生成的應用,去解釋如何設計可以即時回傳的 API。
source code: spring-boot-web-api-async
假設我們有一個 ReportController,它負責處理與報告生成相關的 HTTP 請求,它提供三個核心 API 端點。
- 啟動報告生成端點
@PostMapping("/reportJob/create")
publicResponseEntity<Object>createJob() {
Stringuuid = String.format("%d_%s", (newDate()).getTime(), UUID.randomUUID().toString());
CompletableFuture.runAsync(() -> {
try {
orderStatus.put(uuid, PROCESSING);
Thread.sleep(10000); // 10-second simulated delayreportService.genAndSaveReport(uuid);
orderStatus.put(uuid, COMPLETED);
} catch (InterruptedExceptione) {
Thread.currentThread().interrupt();
}
});
returnResponseEntity
.accepted()
.header(HttpHeaders.LOCATION, "/reportJob/status/" + uuid)
.body(Map.of("uuid", uuid,
"status api",
"/api/reportJob/status/" + uuid,
"download api",
"/api/reportJob/download/" + uuid));
}
運作原理:
- 立即生成唯一的 uuid 來標識這次任務
- 在 CompletableFuture.runAsync 運行長時間的操作。
- API 本身即時回傳了 HTTP 202 (Accepted) 狀態,告訴客戶端請求已被接受但尚未完成
- 在回傳的結果中,還有提示可以查詢狀態和查詢結果的API。 這種設計避免了 HTTP Gateway Timeout,因為回應是即時的 。
- 檢查進度端點
@GetMapping("/reportJob/status/{uuid}")
publicResponseEntity<Object>getStatus(@PathVariable("uuid") Stringuuid) {
Stringstatus = orderStatus.get(uuid);
if (status == null)
returnResponseEntity.notFound().build();
if (COMPLETED.equals(status)) {
// return ResponseEntity.status(HttpStatus.SEE_OTHER)returnResponseEntity.ok()
.header(HttpHeaders.LOCATION, "/api/reportJob/download/" + uuid)
.body(Map.of("status", COMPLETED));
}
returnResponseEntity.status(HttpStatus.ACCEPTED)
.body(Map.of("status", PROCESSING));
}
}
單純以 map orderStatus.get(uuid)
查看狀態結果。這個map 必需是多線程下使用還是安全的 (ConcurrentHashMap
)。
- 下載結果端點
@GetMapping("reportJob/download/{uuid}")
publicResponseEntity<Resource>download(@PathVariable("uuid") Stringuuid) {
Stringstatus = orderStatus.get(uuid);
if (status == null || !COMPLETED.equals(status)) {
returnResponseEntity.notFound().build();
} else {
// 下載檔案
}
}
}
如果大家並不計較是否需要重做失敗的請求,這個例子已經可以簡單地達到即時異步回傳的效果。如果大家還需求考慮請求是否有效完成,就需要用到 message queue 或其他 job server ,這就不是同一個網頁應用的操作範圍。