This time we’ll dive into how to integrate a Resilience4J CircuitBreaker with a Spring WebClient. Last time I showed the same thing for Spring’s RestTemplate but since WebClient is getting more and more hype, I thought let’s cover it as well.
Similarly to the previous post, I’ll show you 2 ways to integrate Resilience4J with a WebClient. First with annotations and then programmatically. Both are going to be fairly easy.
Again, if you’re not familiar with circuit breaking and what a CircuitBreaker is, check out one of the articles I wrote on this topic: Testing Spring Cloud Feign client resiliency using Resilience4J and even though the title suggests Feign, I explain circuit breaking at the very beginning.
The example app
The app is gonna be simple and quite similar to what I used in the previous article. I generated a project on start.spring.io with Web Reactive, Actuator and Lombok dependencies.
A simple API endpoint for the app:
@RestController @RequiredArgsConstructor public class ApiController { private final ExternalApi externalApi; @GetMapping("/foo") public Mono<String> foo() { return externalApi.callExternalApiFoo(); } }
What’s important here is the fact that the return type from the method is not a simple String
but a Mono<String>
since the app is using Web Reactive.
Let’s create the ExternalApi
class.
@Component @RequiredArgsConstructor public class ExternalApi { private final WebClient webClient; public Mono<String> callExternalApiFoo() { return webClient.get().uri("/external-foo").retrieve().bodyToMono(String.class); } }
The WebClient
is calling the API on the /external-foo
path and resolves the response body into a String
. The returning type will be again a Mono<String>
since we’re in the reactive world.
Now the WebClient
configuration.
@Configuration public class ExternalApiConfig { @Bean public WebClient webClient() { return WebClient.create("http://localhost:9090"); } }
And that’s all for now. The WebClient
will invoke the http://localhost:9090/external-foo
API.
Instead of creating a brand new Spring app to implement the /external-foo
API, I’ll use WireMock to set up a mock server and I’ll write a test-case to verify the scenario we want.
Let’s add the WireMock dependency first into the build.gradle
.
testImplementation "com.github.tomakehurst:wiremock-jre8:2.31.0"
And then the test case:
@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); } } }
The test is going to start up a WireMock server on port 9090. It’ll also start up a Spring Boot web server and then the WireMock server will respond with HTTP 500s on the /external-foo
API endpoint.
Then the first 5 calls for the /foo
API that our Spring app provides should fail with HTTP 500 since there will be uncaught exceptions in the app by the WebClient and by default those get translated into HTTP 500s as well.
After the 5 API calls, the next 5 should fail with HTTP 503 – Service Unavailable, indicating that the Resilience4J CircuitBreaker has opened.
Note: there were a couple tricks you should look out for so I recommend reading the other article because I explain them there: Making your RestTemplate fault tolerant with Resilience4J.
If you run the test case, it’ll obviously fail because we don’t have any CircuitBreaker set up, yet.
Annotation based Resilience4J CircuitBreaker
For annotation driven CircuitBreakers we’ll need a couple dependencies:
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"
The last dependency io.github.resilience4j:resilience4j-reactor:1.7.1
is obviously only needed if you’re running a reactive Spring app.
Now, let’s use the annotation Resilience4J provides:
@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); } }
That’s it. The CircuitBreaker annotation on the method. Due to the fact we have the resilience4j-reactor
dependency, it’ll recognize the Mono return type and will automatically compose circuit breaking into the execution flow. Awesome right?
The externalServiceFoo
in the annotation is the name of our CircuitBreaker that we’ll configure in a second.
Let’s go back to our config class and add a 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)); } }
This will configure the CircuitBreaker to have a COUNT_BASED
sliding window with a size of 10. It won’t evaluate the CircuitBreaker for the first 5 calls (minimumNumberOfCalls
) and will trigger at 50% failure rate; waiting 5 seconds in OPEN state.
Since the test is expecting an HTTP 503 – Service Unavailable response after the CircuitBreaker is open, we have to implement that exception handling first.
We’re creating a new exception handler:
@ControllerAdvice public class ApiExceptionHandler { @ExceptionHandler({CallNotPermittedException.class}) @ResponseStatus(HttpStatus.SERVICE_UNAVAILABLE) public void handle() { } }
This’ll do the job.
Now the test case will still fail if you start it. Why?
It’s because with the CircuitBreakerCustomizer
we can only override existing configuration. In our particular case we’re overriding the externalServiceFoo
configuration which apparently doesn’t exist yet – in the context of Resilience4J.
How to make Resilience4J aware of the CircuitBreaker configuration?
Easy, just go to the application.properties
and add this line:
resilience4j.circuitbreaker.instances.externalServiceFoo.slidingWindowType=COUNT_BASED
This will create the configuration object with default settings in Resilience4J’s CircuitBreaker registry and then the values we provided will override the defaults.
You can restart the test now and it should be green. Great job.
Programmatic Resilience4J CircuitBreaker composition
If you don’t like the magical annotations and the concept of aspect oriented programming, you can use a Resilience4J CircuitBreaker programmatically as well.
First of all, we have to create the CircuitBreaker configuration. It’s stored in a CircuitBreakerRegistry class that’s provided by Resilience4J.
@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) ); } }
Same CircuitBreaker settings, just another way of configuring it. And we can remove the line from the application.properties
, we won’t need that anymore.
How to use it with a WebClient? Easy.
@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)); } }
We autowire the CircuitBreakerRegistry
to get access to the pre-configured CircuitBreaker. In the method, we retrieve the CircuitBreaker instance and then use the CircuitBreakerOperator
from the resilience4j-reactor
module to compose the API call with the CircuitBreaker
instance.
Of course we don’t need to change anything in the test since we only modified the internal implementation how we achieve circuit breaking.
If you run the test case, it should pass.
WebClient with Resilience4J is easy
As you see, it’s quite easy to integrate Resilience4J with a Spring WebClient for resiliency purposes. Using a CircuitBreaker is just the first step on the road; there are much more to Resilience4J that you can use similarly to a CircuitBreaker.
If you liked it, let me know in the comments below. You can reach me on Twitter and Facebook if you have any questions or have something in mind.
You can find the annotation based configuration here on GitHub.
Amazing article! 🙂
Thanks Bartosz. Appreciated. 🙂
Thanks Arnold for this great information.
I have implemented Circuitbreaker with webclient as mention , but i am stuck with one scenario.
when i am calling external POST service and getting response then i am checking is Response contains 4XX or 5XX error , because for 4XX errors CB should not work but for 5XX ( And not for all 5XX but let say some custom HTTPStatus code example 569 , CB should work and also Retry should work for this upto 3 )
code is like :
(onStatus::5XX then ResponseObject -> flatMap ( Then throwing Custom Exception which is configured for CB and Retry Resiliency)
but it stuck with Retry.
Can you please share or create a one tutorial for this scenario where CB and RETRY are part of same method and on 5XX error we are throwing Custom Exception ( This custom Exception is configurable with CB and Retry Resiliency Configuration ) .
Got it. I’ve put this on the list and sorry for the delayed response.
Best,
Arnold
Can you please tell me how can I configure fallback when I user custom configuration bean instead of annotation and yml. I see .transformDeferred(CircuitBreakerOperator.of(circuitBreaker)) is used with WebClient but how will I add fallback method to this WebClient?
I have refered to your blog and created circuitbreaker using programmatically. I have created junit testcase and tried to run junit, got exception at transformDeferred method. While debugging I found NullPointerException in CallNotPermitException class. I got Circuitbreaker mock object but circuitbreaker.getCircuitBreakerConfig() object method returns null. Can you guide me in creating junit code for transformDeferred.