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