Spring Developers, have you ever felt the need for oneasynchronous/non-blockingHTTP-Client mit aflowing functional styleAPI that was easy to use and efficient?
If so, then welcome to this article about WebClient, the new HTTP reactive client introduced in Spring 5.
How to use the web client
WebClient is part of Spring 5's reactive web framework called Spring WebFlux. To use WebClient you must have thespring-webflux
module in your project.
Add dependency in an existing Spring Boot project
If you have an existing Spring Boot project, you can use thespring-webflux
module by adding the following dependency in thepom.xml
file -
<dependency> <The group ID>org.springframework.boot</The group ID> <artifactId>spring-boot-starter-webflux</artifactId></dependency>
Create a new project from Scratch
If you are creating a project from scratch, you can use to generate a starter projectspring-webflux
module from theSpring InitializrWebsite -
- Go tohttp://start.spring.io.
- SentenceArtifactAndgroupTo
Webclient-Demo
. - SentencePackageTo
com.example.webclientdemo
. - Add toResponsive Web, Andvalidationdependencies.
- Clickto generateto generate and download the project.
Use of remote APIs with WebClient
Let's make things interesting and use WebClient to consume a Real World API.
In this article we will consumeGithub's APIswith web client. We use WebClient to perform CRUD operations on the user's Github repositories.
You can read and understand the bits and pieces of WebClient from scratch or download the entire demo project with all the examples fromGitHub.
Create an instance of WebClient
1. WebClient with the createcreate()
Method
You can create an instance of WebClient usingcreate()
factory method -
WebClientwebClient= WebClient.create();
If you are only using APIs from a specific service, you can initialize WebClient with the baseUrl of that service like this:
WebClientwebClient= WebClient.create("https://api.github.com");
2. Create the WebClient with the WebClient Builder
WebClient also includes a builder that gives you a range of customization options including filters, standard headers, cookies, client connectors, etc. -
WebClientwebClient= WebClient.Baumeister() .baseUrl("https://api.github.com") .defaultHeader(HttpHeader.CONTENT TYPE, "application/vnd.github.v3+json") .defaultHeader(HttpHeader.USER-AGENT, "Spring 5 WebClient") .build();
Sending a request with WebClient and getting the response
Here's how you can use WebClient to create aRECEIVE
request toGithubs List Repositories API-
public Flow<GithubRepo> listGithubRepositories(lineusername, lineSign) { to returnwebClient.receive() .uri("/user/repos") .Header("Permit", "Basic " + Base64Utils .encodeToString((username+ ":" +Sign).getBytes(UTF_8))) .recall() .bodyToFlux(GithubRepo.Class);}
See how easy and concise the API calls are!
Suppose we have a class namedGithubRepo
which confirms Github's API response, the above function returns aFlow
vonGithubRepo
objects.
Note that I'm usingGithub's basic authenticationMechanism for calling the APIs. It requires your Github username and a personal access token, which you can generatehttps://github.com/settings/tokens.
Using the exchange() method to get the response
Therecall()
method is the easiest way to get the response body. However, if you want more control over the response, you can use theExchange()
Method that has access to the wholeClientResponse
including all headers and the body -
public Flow<GithubRepo> listGithubRepositories(lineusername, lineSign) { to returnwebClient.receive() .uri("/user/repos") .Header("Permit", "Basic " + Base64Utils .encodeToString((username+ ":" +Sign).getBytes(UTF_8))) .Exchange() .flatMapViele(customer response->customer response.bodyToFlux(GithubRepo.Class));}
Using parameters in the request URI
You can use parameters in the request URI and pass their values separately in theuri()
Function. All parameters are surrounded by curly brackets. The parameters are automatically replaced by the WebClient before the request is made -
public Flow<GithubRepo> listGithubRepositories(lineusername, lineSign) { to returnwebClient.receive() .uri("/user/repos?sort={sortField}&direction={sortDirection}", "Updated", "dismount") .Header("Permit", "Basic " + Base64Utils .encodeToString((username+ ":" +Sign).getBytes(UTF_8))) .recall() .bodyToFlux(GithubRepo.Class);}
Using the URIBuilder to construct the request URI
You can also gain full programmatic control over the request URI by using aUriBuilder
so -
public Flow<GithubRepo> listGithubRepositories(lineusername, lineSign) { to returnwebClient.receive() .uri(uriBuilder->uriBuilder.Away("/user/repos") .QueryParam("Sort by", "Updated") .QueryParam("Direction", "dismount") .build()) .Header("Permit", "Basic " + Base64Utils .encodeToString((username+ ":" +Sign).getBytes(UTF_8))) .recall() .bodyToFlux(GithubRepo.Class);}
Passing the request body in WebClient requests
If you send the request body in the form of aMono
or aFlow
, then you can pass it directly to theBody()
method in the WebClient, otherwise you can just create a Mono/Flux from an object and pass it like this -
public Mono<GithubRepo> createGithubRepository(lineusername, lineSign, RepoRequestcreateRepoRequest) { to returnwebClient.Post() .uri("/user/repos") .Body(Mono.Only(createRepoRequest), RepoRequest.Class) .Header("Permit", "Basic " + Base64Utils .encodeToString((username+ ":" +Sign).getBytes(UTF_8))) .recall() .body to mono(GithubRepo.Class);}
If you have an actual value instead of aeditor
(Flow
/Mono
), you can use the ...syncBody()
Link method to pass the request body -
public Mono<GithubRepo> createGithubRepository(lineusername, lineSign, RepoRequestcreateRepoRequest) { to returnwebClient.Post() .uri("/user/repos") .syncBody(createRepoRequest) .Header("Permit", "Basic " + Base64Utils .encodeToString((username+ ":" +Sign).getBytes(UTF_8))) .recall() .body to mono(GithubRepo.Class);}
Finally, you can use various factory methods provided byBodyInserter
construct class aBodyInserter
object and pass it into theBody()
Method. TheBodyInserter
Class contains methods for creating aBodyInserter
of aObject
,editor
,Resource
,form data
,MultipartData
etc -
public Mono<GithubRepo> createGithubRepository(lineusername, lineSign, RepoRequestcreateRepoRequest) { to returnwebClient.Post() .uri("/user/repos") .Body(BodyInserter.fromObject(createRepoRequest)) .Header("Permit", "Basic " + Base64Utils .encodeToString((username+ ":" +Sign).getBytes(UTF_8))) .recall() .body to mono(GithubRepo.Class);}
Add filter functions
WebClient supports request filtering with aExchangeFilterFunction
. You can use filter functions to intercept the request and modify it as you like. For example, you can use a filter function to add onepermit
Headers for each request or to log the details of each request.
TheExchangeFilterFunction
takes two arguments -
- The
customer request
And - The next
ExchangeFilterFunction
in the filter chain.
It can change thatcustomer request
and call the next oneExchangeFilterFunction
in the filter chain to move on to the next filter or return the changed onecustomer request
directly to block the filter chain.
1. Add basic authentication using a filter function
In all of the above examples, we include apermit
Headers for basic authentication with Github API. Since this is common to all requirements, you can add this logic in a filter function as you create itWebClient
.
TheExchangeFilterFunctions
The API already provides a filter for basic authentication. You can use it like this -
WebClientwebClient= WebClient.Baumeister() .baseUrl(GITHUB_API_BASE_URL) .defaultHeader(HttpHeader.CONTENT TYPE,GITHUB_V3_MIME_TYPE) .Filter(ExchangeFilterFunctions .BasicAuthentication(username,Sign)) .build();
Now you don't have to add thempermit
headers in each request. The filter function intercepts every WebClient request and adds this header.
2. Logging of all inquiries via a filter function
Let's look at an example of a customExchangeFilterFunction
. We'll write a filter function to intercept and log each request -
WebClientwebClient= WebClient.Baumeister() .baseUrl(GITHUB_API_BASE_URL) .defaultHeader(HttpHeader.CONTENT TYPE,GITHUB_V3_MIME_TYPE) .Filter(ExchangeFilterFunctions .BasicAuthentication(username,Sign)) .Filter(logRequest()) .build();
Here is the implementation of thelogRequest()
filter function -
Private ExchangeFilterFunction logRequest() { to return (customer request,next) -> {Logger.die Info("Inquiry: {} {}",customer request.Method(),customer request.URL());customer request.headers() .for every((Name,Values) ->Values.for every(Wert->Logger.die Info("{}={}",Name,Wert))); to returnnext.Exchange(customer request); };}
3. Using the factory methods ofRequestProcessor() and ofResponseProcessor() to create filters
The ExchangeFilterFunction API provides two factory methods namedofRequestProcessor()
AndofResponseProcessor()
for creating filter functions that intercept the request or response.
ThelogRequest()
Filter function that we created in the previous section can be created withofRequestProcessor()
factory method like this -
Private ExchangeFilterFunction logRequest() { ExchangeFilterFunction.ofRequestProcessor(customer request-> {Logger.die Info("Inquiry: {} {}",customer request.Method(),customer request.URL());customer request.headers() .for every((Name,Values) ->Values.for every(Wert->Logger.die Info("{}={}",Name,Wert))); to return Mono.Only(customer request); });}
If you want to intercept the WebClient response, you can use theofResponseProcessor()
Method to create a filter function like this -
Private ExchangeFilterFunction logResponseStatus() { to return ExchangeFilterFunction.ofResponseProcessor(customer response-> {Logger.die Info("response status {}",customer response.Statuscode()); to return Mono.Only(customer response); });}
Handling WebClient errors
Therecall()
Method in WebClient throws aWebClientResponseException
whenever a response with status code 4xx or 5xx is received.
You can customize this withonStatus()
Methods like this -
public Flow<GithubRepo> listGithubRepositories() { to returnwebClient.receive() .uri("/user/repos?sort={sortField}&direction={sortDirection}", "Updated", "dismount") .recall() .onStatus(HTTPStatus::is4xxClientError,customer response-> Mono.Mistake(neu MyCustomClientException()) ) .onStatus(HTTPStatus::is5xxServerError,customer response-> Mono.Mistake(neu MyCustomServerException()) ) .bodyToFlux(GithubRepo.Class);}
Note that differentrecall()
method thatExchange()
method does not throw exceptions on 4xx or 5xx responses. You have to check the status codes yourself and handle them the way you want.
Handling WebClientResponseExceptions with a@ExceptionHandler
within the controller
You can use one@ExceptionHandler
to handle in your controllerWebClientResponseException
and return customers an appropriate response like this -
@ExceptionHandler(WebClientResponseException.Class)public ResponseEntity<line> handleWebClientResponseException(WebClientResponseExceptionex) {Logger.Mistake("Error from WebClient - Status {}, Text {}",ex.getRawStatusCode(),ex.getResponseBodyAsString(),ex); to return ResponseEntity.Status(ex.getRawStatusCode()).Body(ex.getResponseBodyAsString());}
Testing REST APIs with Spring 5 WebTestClient
WebTestClient contains request methods that are similar to WebClient. Additionally, it includes methods to check the response status, header, and body. You can also use assertion libraries likeassertionJ
with WebTestClient.
Check out the example below to learn how to run REST API tests using WebTestClient -
Package com.Example.Web client demo;import com.Example.Web client demo.payload.GithubRepo;import com.Example.Web client demo.payload.RepoRequest;import org.claimj.Kern.API.allegations;import org.junit.Jupiter.API.MethodOrderer;import org.junit.Jupiter.API.command;import org.junit.Jupiter.API.Test;import org.junit.Jupiter.API.TestMethodOrder;import org.spring frame.beans.Fabric.annotation.Automatically wired;import org.spring frame.Stiefel.test.context.SpringBootTest;import org.spring frame.http.media type;import org.spring frame.test.network.reactive.Server.WebTestClient;import Reactor.Kern.editor.Mono;@SpringBootTest(webEnvironment= SpringBootTest.web environment.RANDOM_PORT)@TestMethodOrder(MethodOrderer.order note.Class)public Class WebclientDemoApplicationTests {@AutowiredPrivate WebTestClientwebTestClient;@Test@Command(1)public Empty testCreateGithubRepository() {RepoRequestrepoRequest= neu RepoRequest("test-webclient-repository", "Repository created to test WebClient");webTestClient.Post().uri("/api/repos").content type(media type.APPLICATION_JSON).accept(media type.APPLICATION_JSON).Body(Mono.Only(repoRequest), RepoRequest.Class).Exchange().expectedstatus().isOk().expected header().content type(media type.APPLICATION_JSON).expect body().jsonPfad("$.name").is not empty().jsonPfad("$.name").is equal to("test-webclient-repository");}@Test@Command(2)public Empty testGetAllGithubRepositories() {webTestClient.receive().uri("/api/repos").accept(media type.APPLICATION_JSON).Exchange().expectedstatus().isOk().expected header().content type(media type.APPLICATION_JSON).expectedBodyList(GithubRepo.Class);}@Test@Command(3)public Empty testGetSingleGithubRepository() {webTestClient.receive().uri("/api/repos/{repo}", "test-webclient-repository").Exchange().expectedstatus().isOk().expect body().consumeWith(Answer->allegations.claim that(Answer.getResponseBody()).isNotNull());}@Test@Command(4)public Empty testEditGithubRepository() {RepoRequestnewRepoDetails= neu RepoRequest("updated-webclient-repository", "Updated name and description");webTestClient.Patch().uri("/api/repos/{repo}", "test-webclient-repository").content type(media type.APPLICATION_JSON).accept(media type.APPLICATION_JSON).Body(Mono.Only(newRepoDetails), RepoRequest.Class).Exchange().expectedstatus().isOk().expected header().content type(media type.APPLICATION_JSON).expect body().jsonPfad("$.name").is equal to("updated-webclient-repository");}@Test@Command(5)public Empty testDeleteGithubRepository() {webTestClient.extinguish().uri("/api/repos/{repo}", "updated-webclient-repository").Exchange().expectedstatus().isOk();}}
Diploma
Congratulations! In this article, you learned how to work with the Spring 5 reactive WebClient and WebTestClient APIs.
I hope you find the examples presented in this article helpful. You can download the complete example project with all the examples atmein github-repository.
Thank you for reading. I really enjoyed writing this article for you and I hope to see you in my next article soon.
FAQs
What is the use of WebClient and WebTestClient? ›
Interface WebTestClient. Client for testing web servers that uses WebClient internally to perform requests while also providing a fluent API to verify responses. This client can connect to any server over HTTP, or to a WebFlux application via mock request and response objects.
How do I get JSON response from WebClient? ›You would just use bodyToMono which would return a Mono object. WebClient webClient = WebClient. create(); String responseJson = webClient. get() .
How do I catch exceptions in WebClient? ›As mentioned in the code block, whenever a 5XX/4XX Error occurs, we can throw a user defined exception, and then execute error handling logic based on those user defined exceptions. Once this error Handler is defined, we can add it in the WebClient Initialisation.
Is WebClient synchronous or asynchronous? ›Conversely, WebClient is asynchronous and will not block the executing thread while waiting for the response to come back.
Which is better HttpClient or WebClient? ›WebClient provides a simple but limited wrapper around HttpWebRequest. And HttpClient is the new and improved way of doing HTTP requests and posts, having arrived with . NET Framework 4.5.
What is an example of web client? ›What are the examples of web clients? These are examples that allow users to perform different tasks using the web. The popular web browsers such as Google Chrome, Internet Explorer, Opera, Firefox and Safari are examples that allow users to access any website through the Internet.