使用Resilience4J增强Spring WebClient容错性 – Arnold


这次我们将深入探讨如何将 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 上找到基于注解的配置。