1. 概述
在本教程中,我們將探討幾種實現 Spring REST API 請求超時時間的可能方法。
然後,我們將討論每種方法的優缺點。請求超時對於防止用户體驗不佳非常有用,尤其是在我們能夠提供默認選項以應對資源耗時過長的情況下。這種設計模式被稱為斷路器模式,但此處我們將不會對其進行詳細説明。
2. <em @Transactional> 時間超期
通過利用 Spring 的 > 註解,我們可以實現數據庫調用上的時間超期。它具有 屬性,我們可以設置其值。該屬性的默認值為 -1,等效於沒有設置任何時間超期。對於時間超期值的外部配置,必須使用不同的屬性,>,而不是它。
例如,假設我們將時間超期設置為 30 秒。如果被註解的方法的執行時間超過這個數字秒數,將會拋出異常。這對於回滾長時間運行的數據庫查詢可能很有用。
為了在實踐中看到這一點,我們將編寫一個非常簡單的 JPA 存儲庫層,它將代表一個花費過多的時間並導致時間超期發生的外接服務。這個 > 擴展包含一個耗時的方法:
public interface BookRepository extends JpaRepository<Book, String> {
default int wasteTime() {
Stopwatch watch = Stopwatch.createStarted();
// delay for 2 seconds
while (watch.elapsed(SECONDS) < 2) {
int i = Integer.MIN_VALUE;
while (i < Integer.MAX_VALUE) {
i++;
}
}
}
}如果在帶有 1 秒超時時間的事務中調用我們的 wasteTime() 方法,則超時時間將在方法執行完成之前到期:
@GetMapping("/author/transactional")
@Transactional(timeout = 1)
public String getWithTransactionTimeout(@RequestParam String title) {
bookRepository.wasteTime();
return bookRepository.findById(title)
.map(Book::getAuthor)
.orElse("No book found for this title.");
}調用此端點會導致 500 HTTP 錯誤,我們可以將其轉換為更有意義的響應。它也需要很少設置即可實現。
然而,此超時解決方案存在一些缺點:
- 依賴於具有 Spring 事務管理的數據庫
- 不適用於整個項目,因為該註解必須存在於每個方法或類中
- 在超時時間到達時,不會中斷請求,因此實體仍需等待完整的處理時間
讓我們考慮一些替代方案。
3. Resilience4j TimeLimiter
Resilience4j 是一個主要用於管理遠程通信故障容錯的庫。其 TimeLimiter 模塊是我們關注的重點。
首先,我們需要在項目中包含 resilience4j-timelimiter 依賴:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-timelimiter</artifactId>
<version>2.1.0</version>
</dependency>接下來,我們將定義一個簡單的 TimeLimiter,其超時時間為 500 毫秒:
private TimeLimiter ourTimeLimiter = TimeLimiter.of(TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofMillis(500)).build());我們可以輕鬆地進行外部配置。
我們可以使用我們的 TimeLimiter 來封裝與我們 @Transactional 示例中相同的邏輯:
@GetMapping("/author/resilience4j")
public Callable<String> getWithResilience4jTimeLimiter(@RequestParam String title) {
return TimeLimiter.decorateFutureSupplier(ourTimeLimiter, () ->
CompletableFuture.supplyAsync(() -> {
bookRepository.wasteTime();
return bookRepository.findById(title)
.map(Book::getAuthor)
.orElse("No book found for this title.");
}));
}TimeLimiter 相較於 @Transactional 解決方案,提供了諸多優勢。主要體現在它支持亞秒級精度以及對超時響應的即時通知。然而,我們仍然需要在所有需要超時的端點中手動包含它。它還要求大量的封裝代碼,並且產生的錯誤仍然是一個通用的 500 HTTP 錯誤。最後,它需要返回一個 Callable<String> 而不是一個原始的 String。
TimeLimiter 僅包含 Resilience4j 的一部分功能,並能很好地與斷路器模式集成。
4. Spring MVC request-timeout
Spring 提供了名為 spring.mvc.async.request-timeout 的屬性。該屬性允許我們以毫秒級精度定義請求超時時間。
以下是如何定義具有 750 毫秒超時時間的屬性:
spring.mvc.async.request-timeout=750這個屬性是全局的並且可以外部配置,但和 TimeLimiter 解決方案一樣,它只適用於返回 Callable 類型的端點。 讓我們定義一個類似於 TimeLimiter 示例的端點,但不需要將邏輯封裝在 Futures 中,也不需要提供 TimeLimiter:
@GetMapping("/author/mvc-request-timeout")
public Callable<String> getWithMvcRequestTimeout(@RequestParam String title) {
return () -> {
bookRepository.wasteTime();
return bookRepository.findById(title)
.map(Book::getAuthor)
.orElse("No book found for this title.");
};
}我們能看到代碼更加簡潔,並且 Spring 在我們定義應用程序屬性時會自動實現配置。 一旦超時時間到達,響應會立即返回,並且還會返回更詳細的 503 HTTP 錯誤,而不是泛化的 500 錯誤。 項目中的每個端點都會自動繼承這個超時配置。
現在,讓我們考慮另一種選項,它將允許我們使用更精細的粒度定義超時時間。
5. 配置 HTTP 客户端的超時時間
與其為整個端點設置超時時間,我們可能只想為單個外部調用設置超時時間。 WebClient 是 Spring 的響應式 Web 客户端,允許我們配置響應超時。
Needless to say,所有流行的 HTTP 客户端庫都允許為發出的請求配置自定義超時。例如,Spring 的舊版 RestTemplate 和 WebClient 的非響應式等效項——RestClient,都支持此功能。
5.1. WebClient 超時設置
為了使用 WebClient,我們首先需要在項目中添加 Spring WebFlux 依賴:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
<version>2.4.2</version>
</dependency>讓我們定義一個具有 250 毫秒響應時間的 WebClient,以便我們通過其基本 URL 在本地主機上調用自身:
@Bean
public WebClient webClient() {
return WebClient.builder()
.baseUrl("http://localhost:8080")
.clientConnector(new ReactorClientHttpConnector(
HttpClient.create().responseTimeout(Duration.ofMillis(250))
))
.build();
}顯然,我們可以輕鬆地通過外部配置設置這個超時值。我們還可以外部配置基本 URL,以及其他若干可選屬性。
現在,我們可以將我們的 WebClient 注入到我們的控制器中,並使用它來調用我們自己的 /transactional 端點,該端點仍然具有 1 秒的超時時間。由於我們已將 WebClient 配置為在 250 毫秒內超時,因此我們應該比 1 秒快得多地看到它失敗。
這是我們新的端點:
@GetMapping("/author/webclient")
public String getWithWebClient(@RequestParam String title) {
return webClient.get()
.uri(uriBuilder -> uriBuilder
.path("/author/transactional")
.queryParam("title", title)
.build())
.retrieve()
.bodyToMono(String.class)
.block();
}調用此端點後,我們能看到收到的是 WebClient 的超時時間,以 500 HTTP 錯誤響應的形式呈現。 此外,我們還可以查看日誌以檢查下游的 @Transactional 超時時間,但如果調用外部服務而不是 localhost,則超時時間將遠程打印。
配置不同後端服務用於不同的請求超時時間可能需要,並且可以使用此解決方案實現。 此外,由 WebClient 返回的 Mono 或 Flux 響應包含大量的錯誤處理方法,用於處理通用的超時錯誤響應。
5.2. RestClient 超時設置
Spring 的 RestClient 自 Spring Framework 6 和 Spring Boot 3 引入以來,作為一種更簡單、非反應式的 WebClient 的替代方案。它提供了一種直接、同步的方法,同時仍提供現代且流暢的 API 設計。
首先,如果尚未添加,請添加 spring-boot-starter-web 依賴項:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>同樣地,我們首先將使用構建器模式創建一個 RestClient Bean:
@Bean
public RestClient restClient() {
return RestClient.builder()
.baseUrl("http://localhost:" + serverPort)
.requestFactory(customRequestFactory())
.build();
}
ClientHttpRequestFactory customRequestFactory() {
ClientHttpRequestFactorySettings settings = ClientHttpRequestFactorySettings.DEFAULTS
.withConnectTimeout(Duration.ofMillis(200))
.withReadTimeout(Duration.ofMillis(200));
return ClientHttpRequestFactories.get(settings);
}如我們所見,我們可以通過在 ClientHtttpRequestFactory 中定義 RestClient 的構建器來配置默認超時值。
現在,我們可以使用 HTTP 客户端發出請求,如果在設定的 200 毫秒閾值內未收到響應,則會拋出異常:
@GetMapping("/author/restclient")
public String getWithRestClient(@RequestParam String title) {
return restClient.get()
.uri(uriBuilder -> uriBuilder
.path("/author/transactional")
.queryParam("title", title)
.build())
.retrieve()
.body(String.class);
}如我們所見,語法與反應式方法非常相似,這使得我們能夠輕鬆地在兩者之間切換,而無需進行大量的代碼修改。
6. 結論
在本文中,我們探討了多種實現請求超時解決方案。 需要考慮的因素有很多。
如果我們想為數據庫請求設置超時,可以使用 Spring 的 @Transactional 方法及其 timeout 屬性。 如果我們嘗試與更廣泛的熔斷器模式集成,使用 Resilience4j 的 TimeLimiter 會更有意義。 使用 Spring MVC 的 request-timeout 屬性是為所有請求設置全局超時的一種方法,但我們也可以輕鬆地在諸如 WebClient 和 RestClient 這樣的 HTTP 客户端中定義更細粒度的資源級別的超時。