使用Resilience4J增強Spring WebClient容錯性 – Arnold

banq發表於2022-01-27

這次我們將深入探討如何將 Resilience4J CircuitBreaker 與 Spring WebClient 整合。

我將向您展示兩種將 Resilience4J 與 WebClient 整合的方法。首先使用註釋,然後以程式設計方式。兩者都將相當容易。

 

案例:

@RestController
@RequiredArgsConstructor
public class ApiController {
    private final ExternalApi externalApi;

    @GetMapping("/foo")
    public Mono<String> foo() {
        return externalApi.callExternalApiFoo();
    }
}

該應用程式正在使用 Web Reactive:

呼叫外部客戶端:

@Component
@RequiredArgsConstructor
public class ExternalApi {
    private final WebClient webClient;

    public Mono<String> callExternalApiFoo() {
        return webClient.get().uri("/external-foo").retrieve().bodyToMono(String.class);
    }
}

WebClient正在呼叫/external-foo路徑上的API,並將響應體解析為一個字串。返回的型別將再次是Mono<String>,因為我們是在反應式世界中。

WebClient的配置:

@Configuration
public class ExternalApiConfig {
    @Bean
    public WebClient webClient() {
        return WebClient.create("http://localhost:9090");
    }
}

這就是目前的全部內容。WebClient將呼叫http://localhost:9090/external-foo API。

我沒有建立一個全新的Spring應用來實現/external-foo API,而是使用WireMock來建立一個模擬伺服器,並編寫一個測試案例來驗證我們想要的場景。

讓我們先在build.gradle中新增WireMock的依賴項。

testImplementation "com.github.tomakehurst:wiremock-jre8:2.31.0"

測試程式碼:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
public class ApiControllerTest {
    @RegisterExtension
    static WireMockExtension EXTERNAL_SERVICE = WireMockExtension.newInstance()
            .options(WireMockConfiguration.wireMockConfig().port(9090))
            .build();

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void testFoo() throws Exception {
        EXTERNAL_SERVICE.stubFor(get("/external-foo").willReturn(serverError()));

        for (int i = 0; i < 5; i++) {
            ResponseEntity<String> response = restTemplate.getForEntity("/foo", String.class);
            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR);
        }
        for (int i = 0; i < 5; i++) {
            ResponseEntity<String> response = restTemplate.getForEntity("/foo", String.class);
            assertThat(response.getStatusCode()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE);
        }
    }
}

該測試將在9090埠啟動一個WireMock伺服器。它還將啟動一個Spring Boot網路伺服器,然後WireMock伺服器將在/external-foo API端點上以HTTP 500s響應。

然後,我們的Spring應用程式對/foo API的前5次呼叫應該以HTTP 500失敗,因為WebClient在應用程式中會有未捕獲的異常,預設情況下,這些異常也會被翻譯成HTTP 500。

在這5次API呼叫之後,接下來的5次呼叫應該以HTTP 503 - Service Unavailable失敗,表明Resilience4J CircuitBreaker已經開啟。

如果你執行這個測試案例,它顯然會失敗,因為我們還沒有設定任何CircuitBreaker。

 

基於註解的Resilience4J CircuitBreaker

對於註解驅動的CircuitBreakers,我們需要一些依賴性。

    implementation 'org.springframework.boot:spring-boot-starter-aop'
    implementation 'io.github.resilience4j:resilience4j-spring-boot2:1.7.1'
    implementation "io.github.resilience4j:resilience4j-reactor:1.7.1"

最後一個依賴 io.github.resilience4j:resilience4j-reactor:1.7.1 顯然只有在你執行一個反應式Spring應用時才需要。

現在,讓我們使用Resilience4J提供的註解。

@Component
@RequiredArgsConstructor
public class ExternalApi {
    private final WebClient webClient;

    @CircuitBreaker(name = "externalServiceFoo")
    public Mono<String> callExternalApiFoo() {
        return webClient.get().uri("/external-foo").retrieve().bodyToMono(String.class);
    }
}

這就是了。方法上的CircuitBreaker註解。由於我們有resilience4j-reactor的依賴,它將識別Mono的返回型別,並自動將斷路寫入執行流程。很棒吧?

註解中的externalServiceFoo是我們的CircuitBreaker的名字,我們將在一秒鐘內進行配置。

讓我們回到我們的配置類中,新增一個CircuitBreakerCustomizer。

@Configuration
public class ExternalApiConfig {
    @Bean
    public WebClient webClient() {
        return WebClient.create("http://localhost:9090");
    }

    @Bean
    public CircuitBreakerConfigCustomizer externalServiceFooCircuitBreakerConfig() {
        return CircuitBreakerConfigCustomizer
                .of("externalServiceFoo",
                        builder -> builder.slidingWindowSize(10)
                                .slidingWindowType(COUNT_BASED)
                                .waitDurationInOpenState(Duration.ofSeconds(5))
                                .minimumNumberOfCalls(5)
                                .failureRateThreshold(50.0f));
    }
}

這將配置CircuitBreaker有一個COUNT_BASED的滑動視窗,大小為10。它不會對前5次呼叫(minimumNumberOfCalls)的CircuitBreaker進行評估,並將在50%的失敗率時觸發;在開放狀態下等待5秒。

由於測試期望在CircuitBreaker開啟後有一個HTTP 503 - Service Unavailable的響應,我們必須先實現這個異常處理。

我們要建立一個新的異常處理程式。

@ControllerAdvice
public class ApiExceptionHandler {
    @ExceptionHandler({CallNotPermittedException.class})
    @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE)
    public void handle() {
    }
}

這將完成工作。

現在,如果你啟動這個測試案例,它仍然會失敗。為什麼呢?

這是因為在CircuitBreakerCustomizer中,我們只能覆蓋現有的配置。在我們的案例中,我們要覆蓋externalServiceFoo的配置,而在Resilience4J的上下文中,這個配置顯然還不存在。

如何讓Resilience4J知道CircuitBreaker的配置?

很簡單,只要到application.properties中新增這一行。

resilience4j.circuitbreaker.instances.externalServiceFoo.slidingWindowType=COUNT_BASED

這將在Resilience4J的CircuitBreaker登錄檔中用預設設定建立配置物件,然後我們提供的值將覆蓋預設值。

你現在可以重新啟動測試,它應該是綠色的。

 

程式化的Resilience4J CircuitBreaker構成

如果你不喜歡神奇的註釋和麵向方面的程式設計概念,你也可以以程式設計方式使用Resilience4J CircuitBreaker。

首先,我們必須建立CircuitBreaker配置。它被儲存在Resilience4J提供的CircuitBreakerRegistry類中。

@Configuration
public class ExternalApiConfig {
    @Bean
    public WebClient webClient() {
        return WebClient.create("http://localhost:9090");
    }

    @Bean
    public CircuitBreakerRegistry circuitBreakerRegistry() {
        CircuitBreakerConfig externalServiceFooConfig = CircuitBreakerConfig.custom()
                .slidingWindowSize(10)
                .slidingWindowType(COUNT_BASED)
                .waitDurationInOpenState(Duration.ofSeconds(5))
                .minimumNumberOfCalls(5)
                .failureRateThreshold(50.0f)
                .build();
        return CircuitBreakerRegistry.of(
                Map.of("externalServiceFoo", externalServiceFooConfig)
        );
    }
}

同樣的CircuitBreaker設定,只是用另一種方式來配置。我們可以從application.properties中刪除這一行,我們不再需要它了。

如何在WebClient中使用它?很簡單。

@Component
@RequiredArgsConstructor
public class ExternalApi {
    private final CircuitBreakerRegistry circuitBreakerRegistry;
    private final WebClient webClient;

    public Mono<String> callExternalApiFoo() {
        CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("externalServiceFoo", "externalServiceFoo");
        return webClient.get()
               .uri("/external-foo")
               .retrieve()
               .bodyToMono(String.class)
               .transformDeferred(CircuitBreakerOperator.of(circuitBreaker));
    }
}

我們自動連線CircuitBreakerRegistry以獲得對預先配置的CircuitBreaker的訪問。在該方法中,我們檢索CircuitBreaker例項,然後使用resilience4j-reactor模組中的CircuitBreakerOperator,將API呼叫與CircuitBreaker例項組成。

當然,我們不需要在測試中改變任何東西,因為我們只是修改了內部實現如何實現斷路。

如果你執行這個測試用例,它應該通過。

總結

使用 Resilience4J 的 WebClient 很簡單:

如您所見,將 Resilience4J 與 Spring WebClient 整合以實現彈性目的非常容易。使用斷路器只是道路上的第一步;Resilience4J 還有很多其他功能,您可以像使用 CircuitBreaker 一樣使用它們。

你可以在 GitHub 上找到基於註解的配置。

 

相關文章