科技新知

在設計網頁應用時,總會有某些功能,是特別消耗時間的,例如我們的應用要提供報表,或長時間搜索。如果,我們的 Web Api 的連結,要強制客戶端等待結果,那麼中途斷線需要重做的機會就變得很高,客戶端的體驗一定不太好。

面對這些情況,我們最好就把原本一個 API 功能分為三個 API 去做。

  1. 工作生成 API
  2. 查詢狀態 API
  3. 查詢結果 API

如果大家有信心,可以把2和3混合在一起,對於客戶端,也是一件好事。不過,2,3 因為回傳的結構可能不一樣,分開處理,程式碼會更易讀。

以下,筆者就以一個模擬報表生成的應用,去解釋如何設計可以即時回傳的 API。

source code: spring-boot-web-api-async

ReportController.java 詳細解析

假設我們有一個 ReportController,它負責處理與報告生成相關的 HTTP 請求,它提供三個核心 API 端點。

  1. 啟動報告生成端點
@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,因為回應是即時的 。
  1. 檢查進度端點
@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)。

  1. 下載結果端點
@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 ,這就不是同一個網頁應用的操作範圍。

Reference

馬交野