Eric Standley
What is Spring WebClient?
The Spring WebClient is a reactive HTTP library; It is the successor to the Spring RestTemplate which is now in maintenance mode. While RestTemplate was a synchronous blocking library, WebClient is an asynchronous non-blocking library. This guide also includes some information on using a Mono object from the Spring Reactive project, as this is key to how the WebClient works.
before you start
Things you must do before you start
- JDK 11 (JDK 17 if you want to use the records in the GitHub sample code)
- Springboot 2
- Basic understanding of how Spring Boot apps work
This code and a sample test service to invokeat the top of GitHub.
Using WebClient
Set up the project and import dependencies
You can create a java project by going tostart.spring.io/, generating a new Java project and including theSpring Reactive Web
Project. Or you can manually set up a Java project and add the following to your pom.xml.
<parent><Group ID>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><Version>2.5.6</Version><relativerPfad/> <!-- Find parent from repository --></parent><dependencies> <dependency> <Group ID>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <Group ID>io. Project reactor</groupId> <artifactId>Reactor test</artifactId> <area>test</scope> </dependency></Dependencies>
Then create the following classes, which we will use throughout this guide:
ReactiveWebClient.java
@Componentspublic Class ReactiveWebClient { Private Finale WebClient webClient; public ReactiveWebClient() { }}
ReactiveService.java
@Servicepublic Class ReactiveService { Private Finale ReactiveWebClient reactive web client; public ReactiveService(ReactiveWebClient reactive web client) { The.reactive web client = reactive web client; }}
ReactiveExamplesController.java
@RestControllerpublic Class ReactiveExamplesController { Private Finale ReactiveService reactive service; Private Finale ReactiveWebClient reactive web client; public ReactiveExamples(ReactiveService reactive service, ReactiveWebClient reactive web client) { The.reactive service = reactive service; The.reactive web client = reactive web client; }}
Set up the web client
There are two ways to create a WebClient, the first using the create method, which has two versions: either an empty argument to set up a default WebClient, or one that takes the base URL that that WebClient calls ( this example uses the localhost url of the wait app in the code sample; you can use that or any other REST app you might have).
The.webClient = WebClient.create();//orThe.webClient = WebClient.create("http://localhost:12345");
There is also a more comprehensive builder method that you can use to set more default settings for the WebClient if you wish. For example, if you know you will only be using JSON and want to ensure that your Accept and Content headers are always set.
The.webClient = WebClient.Baumeister() .baseUrl(Characteristics.getHost()) .defaultHeader("Accept", media type.APPLICATION_JSON_VALUE, "content type", media type.APPLICATION_JSON_VALUE ).build();
For this guide just putthis.webClient = WebClient.create("URL des Dienstes");
in the constructor ofReactiveWebClient
Class.
make calls
Let's look at how to make a simple phone callwebClient
we just created. Place this method in yourReactiveWebClient
Class.
public Mono<Returned item> callOne() { to return webClient.receive() .uri("wait1") .recall() .body to mono(Returned item.Class);}public record Returned item(UUID ID, line die Info) {}
Here you can see that the first method we call on our WebClient is the REST action we want to perform, in this case it's a.receive()
. All common REST calls - GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS - have their own helper methods that can be called. The next method in the chain is this.uri()
, which specifies what path we want to append to the base URL we used to set up this WebClient earlier. Next we have.recall()
, where in the chain the WebClient actually makes the call, and we convert from methods that set up the call to methods that handle the response. I want to point this out instead of calling.recall()
, there are also those.exchangeToMono()
And.exchangeToFlux()
methods. I won't go through these, but they are alternatives you can look up. Last is the.bodyToMono()
method that gives us the payload we expect from our call. In this case, the invocation is expected to return a JSON payload that can be mapped toReturned item
record.
Before we go any further, we need to deal with this last part, as it is key to using the WebClient and is very different from how the old RestTemplate (or any synchronous HTTP client library) was handled. The best way to think about this is to view each invocation of the WebClient as a threaded process. So to compare the old to the new, look at the following code:
War Article = remainderTemplate.getForEntity(Characteristics.getHost() + "/" + end point, Returned item.Class);//Execution waits here until the call returns to the remote systemSystem.out of.println("ReturnedItem from the call: " + Article.toString());
vs
War Article = webClient.receive().uri(API Properties.A_ENDPOINT).recall().body to mono(Returned item.Class);//The call to the remote system is still being processed, so the following line will be printed before the call has returnedSystem.out of.println("This is the mono that will return a ReturnedItem at some point in the future: " + Article.toString());
Instead of having our returned data from our REST call, we now get a Mono object that wraps our returned data and we still process without having our data yet. You may be wondering how to use the data from this call. Well, if you're used to the streaming API introduced in Java 8, you should notice the familiar method.Map()
that exists on mono. If you've never used this at a basic level, it takes an object of one type and converts it to another. For example, take a code base that we want to convert theReturned item
that comes from our call back in areturn object
to return to our customer. We can take the above method call and apply our logic to it. Place this method in theReactiveService
Class.
public Mono<return object> simpleCall(){ to return reactive web client.callOne().Map(Returned item -> { War ids = List.von(Returned item.ID().toString()); to return neu return object(ids, Null); });}
Here you can see that the.Map()
afunction<? Super T, ? expands R>
Parameter that takes the ID from theReturned item
to create a new onereturn object
Object that has a list of IDs. Now that we have our object to return, you may be wondering how to get thatreturn object
from mono. Although there are several ways to get the value out of mono, for now we'll just leave it to Spring, and so our controller method can look like this. Place this method in theReactiveExamplesController
Class.
@GetMapping("/defaultStatus")public Mono<return object> defaultStatus(){ to return reactive service.simpleCall();}
At this point, if we launch the dummy wait app and our sample reactive app, we should be able to make a simple call to our service that will return us a list of UUIDs. This should contain a single UUID as that is all we coded above.
Handling custom returns and errors
Well, we got the basic call right, but if you've been developing for a while you'll find that there are many instances where we want to return some custom headers or handle errors to return something useful to our callers. Let's start with how to customize our return status and headers.
Custom statuses and headers
There are two ways to return a success status other than 200, which is Spring's default. The first and easiest way to do this would be using the Spring annotation@ResponseStatus()
. Add this method to theReactiveExamplesController
Class.
@GetMapping("/annotation status")@responsestatus(HTTPStatus.ACCEPTED)public Mono<return object> annotation status(){ to return reactive service.simpleCall();}
If we call this endpoint now, we can see that our return status has changed from the default of 200 OK to 202 ACCEPTED
The second way will once again reap the benefits.Map()
method on the Mono class to return theResponseEntity
Object. The bonus effect of this method is that we can also add additional REST data to the returned message. In this case, the following code adds a custom header in addition to switching up the status. Add this method to the againReactiveExamplesController
Class.
@GetMapping("/responseStatus")public Mono<ResponseEntity<return object>> response status(){ War Mono = reactive service.simpleCall(); to return Mono.Map(return object -> { HttpHeader headers = neu HttpHeader(); headers.add to ("ETag", „ab435e9c88dcfbc355fe9a56061712f45b5d60b5a74b1ae8c5a13a6b04c3f834“); to return neu ResponseEntity<>(return object, headers, HTTPStatus.ACCEPTED); });}
Here you can see that we are adding themETag
Add a header to our reply and change the status to Accepted. There are many other options you can read about when creating your own response entity objectHere. Now if we name oursresponse status
endpoint, you'll see that we now have our ETag header as well.
To deal with errors, let's add the following to oursReactiveWebClient
Class
public Mono<Returned item> errorStatus(line end point){ to return webClient.receive().uri(end point).recall() .body to mono(Returned item.Class);}
And add the followingReactiveExamplesController
:
Private static Finale line endpoint400 = "400Errors";@GetMapping("/defaultHandle")public Mono<Returned item> defaultHandle(){ to return reactive web client.errorStatus(endpoint400);}
Now when we call our endpoint, you'll see that we get back an ugly 500 error, which doesn't tell us anything about our call to another service being a bad request.
To correct this, to give us something useful, we need to set up two different parts. The first is the@ExceptionHandler()
Annotation. This is a spring web class and while not specific to using the WebClient class, it makes life much easier when using the WebClient class. To use it, let's add the followingReactiveWebClient
Class.
@ExceptionHandler(Exception.Class)public ResponseEntity<line> Others(Exception Exception){ to return ResponseEntity.Status(HTTPStatus.I_AM_A_TEAPOT.Wert()).build();}
Basically, this method tells Spring Boot that this controller is throwing a type exceptionexception.class
Catch that and send back the instead of the standard error responseResponseEntity
that this method returns. In this case, since this is soughtexception.class
, it acts as the default error handler. Now, if we run our code again, we should get back an empty body and a status of 418 I AM A TEAPOT.
Here we can see that our 500 error with the default Spring error text has been replaced with our empty 418 error text. While this is a better way of handling things, you probably want to send back more informative errors. To do this we need to create our own exception, add the right oneexception handler
to our controller and update the WebClient's method chain to properly handle errors. First create theBadRequestException
Class.
public Class BadRequestException expanded Exception{ public BadRequestException() { } public BadRequestException(line News) { super(News); }}
Then add a new oneexception handler
for theReactiveExamplesController
Great for handling our new exception.
@ExceptionHandler(BadRequestException.Class)public ResponseEntity<line> onBadRequest(BadRequestException BadRequestException){ to return ResponseEntity.Status(HTTPStatus.BAD REQUEST.Wert()).build();}
Now let's modify our WebClient call to handle our 400 error more cleanly. An important note here is that if our WebClient receives an HTTP status that isn't in the 200's, it will throw an exception, so you should always make sure you handle these exceptions.
public Mono<Returned item> errorStatus(line end point){ to return webClient.receive().uri(end point).recall() .onStatus(HTTPStatus::iserror, Answer -> switch (Answer.rawStatusCode()){ Fall 400 -> Mono.Mistake(neu BadRequestException("bad request made")); Fall 401, 403 -> Mono.Mistake(neu Exception("Authentication Error")); Fall 404 -> Mono.Mistake(neu Exception("Maybe not a mistake?")); Fall 500 -> Mono.Mistake(neu Exception("Server Error")); Standard -> Mono.Mistake(neu Exception("something went wrong")); }) .body to mono(Returned item.Class);}
Here we can apply the new method.onStatus()
. This method takes a predicate that evaluates the response status code and, if it returns true, executes the function that is also sent to the method. In this example ours.onStatus()
Handler runs when we get a status that is not in the 200s. As you can see we will return our new one when we get a 400 backBadRequestException
Class, otherwise we just return a generic exception with some custom labels. You'll also notice that we had to wrap our exceptions in aMono.error()
. This is just a wrapper so the reactive library can propagate the error to any chain of Mono methods. Now when we call our 400 returning endpoint, we end up with:
Now that we have error handling for failed requests, what about non-stateful exceptions? For example, suppose the request timed out and we don't have a status issue. In that case, there is one last method that can be used on the WebClient to handle these issues. First, let's add a new endpointReactiveExamplesController
class so we can set up a non-state error method.
@GetMapping("/nonStatusError")public Mono<Returned item> nonStatusError(){ to return reactive web client.nonStatusError("unknown url", 9000, "/wooden path");}
Then add the following method in theReactiveWebClient
Class.
public Mono<Returned item> nonStatusError(line host, int Harbor, line Away){ to return webClient.receive().uri(uriBuilder -> uriBuilder.host(host).Harbor(Harbor).Away(Away).build()).recall() .onStatus(HTTPStatus::iserror, Answer -> switch (Answer.rawStatusCode()){ Fall 400 -> Mono.Mistake(neu BadRequestException("bad request made")); Fall 401, 403 -> Mono.Mistake(neu Exception("Authentication Error")); Fall 404 -> Mono.Mistake(neu Exception("Maybe not a mistake?")); Fall 500 -> Mono.Mistake(neu Exception("Server Error")); Standard -> Mono.Mistake(neu Exception("something went wrong")); }) .body to mono(Returned item.Class) .onErrorMap(throwable.Class, throwable -> neu Exception("Simple Exception"));}
We added those.onErrorMap()
Method here, which in this example grabs everything of this typeThrowable.class
and wrap that in whateverthrowable
object we want. Here we just wrap it in a normal exception but throw our specific error message fromsimple exception
. If we change the default setting slightlyexception handler
In the controller, we should be able to see this message in our return body.
@ExceptionHandler(Exception.Class)public ResponseEntity<line> Others(Exception Exception){ to return ResponseEntity.Status(HTTPStatus.I_AM_A_TEAPOT.Wert()).Body(Exception.getLocalizedMessage());}
So now the error we get from the call is sent back to us in the response to our request.
And here we go. Instead of our timeout returning a generic Spring error, we were able to turn it into an error with the text "simple exception". But what happens if we change the call to return a 400 to us? I won't go into depth here, but you can play around with the WebClient call and see how you can change some of the call parameters to make this work.
Uh oh, you can see here that while trying to catch the non-state errors, this solution caught ours as wellBadRequestError
and changed it to the "simple exception" error that we set up to catch the non-state errors. Luckily there is another version of the.onErrorMap()
Function that allows us to set up filtering to not change previous error handling.
public Mono<Returned item> nonStatusError(line host, int Harbor, line Away){ to return webClient.receive().uri(uriBuilder -> uriBuilder.host(host).Harbor(Harbor).Away(Away).build()).recall() .onStatus(HTTPStatus::iserror, Answer -> switch (Answer.rawStatusCode()){ Fall 400 -> Mono.Mistake(neu BadRequestException("bad request made")); Fall 401, 403 -> Mono.Mistake(neu Exception("Authentication Error")); Fall 404 -> Mono.Mistake(neu Exception("Maybe not a mistake?")); Fall 500 -> Mono.Mistake(neu Exception("Server Error")); Standard -> Mono.Mistake(neu Exception("something went wrong")); }) .body to mono(Returned item.Class) .onErrorMap(predicate.not(BadRequestException.Class::isInstance), other exception -> neu Exception("other exception"));}
This version of.onErrorMap()
first takes a predicate for the kind of bugs the map function is working on. This is a simple case when the current error is not aBadRequestException
Then convert the current error to a simple exception with the message "other exception".
Then add a final endpoint to oursReactiveExamplesController
Class calling this new method with our 400 error endpoint and we should see that our specific error handling for 400 errors in our error map should not change at the end.
@GetMapping("/statusErrorWithExceptionMapping") public Mono<ExtendedReturnedItem> statusErrorWithExceptionMapping(){ to return reactive web client.AvoidErrorSwallow("premises Host", "12345", endpoint400); }
And here's what we get:
As expected, although at the end of our call we have an error map, oursBadRequestException
made it out of the call chain unchanged.
Wrap up
You should now have a good understanding of how to use the Spring WebClient class to make REST calls in your services. From here you should play around with the methods in the WebClient to see how to make either a PUT or POST call (hint: hit the.bodyValue()
Method). Also, due to the move away from the normal synchronous style of yore, I'd recommend working with the Mono class to get a feel for how this works. It's a different way of thinking and requires new ways of testing components and understanding how to ensure you're getting the most out of using non-blocking code.