Spring REST API設置請求超時

    1.概述

    在本教程中,我們將探討幾種可能的方法來實現Spring REST API的請求超時。

    我們將討論每種方法的優缺點。請求超時對於防止不良的用戶體驗很有用,尤其是在存在資源佔用時間過長的情況下,我們可以默認使用其他方法時。這種設計模式稱為“斷路器”模式,但在此不再贅述。

    2. @ @Transactional超時

    我們可以對數據庫調用實現請求超時的一種方法是利用Spring的@Transactional註釋。它具有我們可以設置的timeout屬性。此屬性的默認值為-1,這等效於根本沒有任何超時。對於超時值的外部配置,必須使用另一個屬性timeoutString代替。

    例如,假設我們將此超時設置為30。如果帶註釋的方法的執行時間超過了此秒數,則將引發異常。這對於回滾長時間運行的數據庫查詢可能很有用。

    為了了解這一點,讓我們編寫一個非常簡單的JPA存儲庫層,該層將代表一個外部服務,該外部服務需要很長時間才能完成,並且會導致超時。此JpaRepository擴展中有一個耗時的方法:

    public interface BookRepository extends JpaRepository<Book, String> {
    
    
    
     default int wasteTime() {
    
     int i = Integer.MIN_VALUE;
    
     while(i < Integer.MAX_VALUE) {
    
     i++;
    
     }
    
     return 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>1.6.1</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.");
    
     }));
    
     }

    @Transactional解決方案相比, TimeLimiter許多優點。即,它支持亞秒精度和超時響應的立即通知。但是,它仍然必須手動包含在所有需要超時的端點中,它需要一些冗長的包裝代碼,並且它產生的錯誤仍然是通用的500 HTTP錯誤。另外,它需要返回Callable<String>而不是原始String.

    TimeLimiter僅包含Resilience4j的功能的子集,並且與Circuit Breaker模式很好地接口。

    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. WebClient超時

    與其為整個端點設置超時,不如我們只想為單個外部呼叫設置超時。 WebClient是Spring的反應式Web客戶端,允許我們配置響應超時。

    也可以在Spring較舊的RestTemplate對像上配置超時。但是,大多數開發人員現在更喜歡WebClient不是RestTemplate

    要使用WebClient,我們必須首先將Spring的WebFlux依賴項添加到我們的項目中:

    <dependency>
    
     <groupId>org.springframework.boot</groupId>
    
     <artifactId>spring-boot-starter-webflux</artifactId>
    
     <version>2.4.2</version>
    
     </dependency>

    讓我們定義一個響應時間為250毫秒的WebClient ,我們可以使用它在其基本URL中通過localhost進行調用:

    @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();
    
     }

    調用此終結點後,我們看到確實以500 HTTP錯誤響應的形式收到WebClient的超時。我們還可以檢查日誌以查看下游@Transactional超時。但是,當然,如果我們調用外部服務而不是本地主機,則它的超時將被遠程打印。

    為不同的後端服務配置不同的請求超時可能是必需的,並且使用此解決方案是可能的。此外, WebClient返回的MonoFlux響應發布者包含許多錯誤處理方法,用於處理通用超時錯誤響應。

    六,結論

    在本文中,我們剛剛探討了幾種用於實現請求超時的解決方案。在決定使用哪一個時,要考慮幾個因素。

    如果我們想對數據庫請求設置超時,則可能要使用Spring的@Transactional方法及其timeout屬性。如果我們試圖與更廣泛的斷路器模式集成,則使用Resilience4j的TimeLimiter會很有意義。使用Spring MVC request-timeout屬性最適合為所有請求設置全局超時,但是我們可以使用WebClient輕鬆為每個資源定義更精細的超時。