How to Use the Spring Reactive WebClient
5 CommentsLast Updated on October 21, 2024 by jt
Spring Framework 5 introduces WebClient, a component in the new Web Reactive framework that helps build reactive and non-blocking web applications.
In web applications, a common requirement is to make HTTP calls to other services.
Prior to Spring 5, there was RestTemplate
for client-side HTTP access. RestTemplate
, which is part of the Spring MVC project, enables communication with HTTP servers and enforces RESTful principles.
Other options to perform HTTP operations from Spring Boot applications include the Apache HttpClient library. These options are based upon the Java Servlet API, which is blocking (aka not reactive).
With Spring Framework 5, you now have a new reactive WebClient that provides a higher level, common API over HTTP client libraries.
This post assumes you have basic knowledge of Spring 5 Reactive Programming.
If you are new to reactive programming, checkout my course, Spring Framework 6: Beginner to Guru which covers reactive programming with Spring Framework 5.
In this post, I will explain how to use WebClient along with WebClientTest.
Overview of WebClient
WebClient is a non-blocking, reactive client for performing HTTP requests with Reactive Streams back pressure. WebClient provides a functional API that takes advantage of Java 8 Lambdas.
By default, WebClient uses Reactor Netty as the HTTP client library. But others can be plugged in through a custom ClientHttpConnector
.
To start using WebClient with remote Rest APIs, you need Spring WebFlux as your project dependency.
You can create a WebClient using one of the static factory methods create()
or the overloaded create(String)
. Another approach is to obtain a builder()
to create and configure an instance.
In this post, we’ll look at both the approaches.
The Application
For this post, I have a Spring 5 reactive RESTful service that acts as a Producer. It continuously emits streams of data wrapped in a Flux
. We will access the producer from a second service using WebClient.
We will also use WebClient to access the OMDB API, a free REST API to query movie information.
The existing Spring 5 Reactive RESTful service (Producer) is comprised of a controller and a MovieEvent
domain object that models an event. The service layer produces a stream of MovieEvent
with a delay of 1 second continuously.
As this post is on WebClient, I won’t go into the Producer side. The Producer is a Maven project that you can download from the link provided at the end of this post. You need to clone it, import it to your IDE, and run.
I have imported the producer as a Maven Project to IntelliJ and got it running on an embedded Netty server, as shown in this Figure.
WebClient in the API Consumer
The API consumer is a Spring Boot project that uses WebFlux. The consumer communicates with two services:
- OMDB API to retrieve movie information by name, and ID.
- Our local Producer to consume event streams.
To access the OMDB API, get your free API access key here.
The Maven POM of the consumer is this.
pom.xml
// 4.0.0 springframework.guru webclient-movie-api 0.0.1-SNAPSHOT jar webclient-movie-api Demo project for WebClient org.springframework.boot spring-boot-starter-parent 2.0.2.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter-webflux org.springframework.boot spring-boot-starter-test test io.projectreactor reactor-test test org.springframework.boot spring-boot-maven-plugin
The Domain Models
Our domain model is a Movie
POJO with fields to hold movie information returned by the OMDB API.
The Movie
POJO is this.
Movie.java
//package springframework.guru.webclientdemo.domain; import com.fasterxml.jackson.annotation.JsonProperty; public class Movie { @JsonProperty("Title") private String movieTitle; @JsonProperty("Year") private String releaseYear; @JsonProperty("Type") private String type; @JsonProperty("Poster") private String posterUrl; // /getter and setters public String getMovieTitle() { return movieTitle; } public void setMovieTitle(String movieTitle) { this.movieTitle = movieTitle; } public String getReleaseYear() { return releaseYear; } public void setReleaseYear(String releaseYear) { this.releaseYear = releaseYear; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getPosterUrl() { return posterUrl; } public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; } }
Our second domain model is MovieEvent
that models an event to be received from the Producer.
The MovieEvent
POJO is this.
MovieEvent.java
//package springframework.guru.webclientdemo.domain; import java.util.Date; public class MovieEvent { private String eventMessage; private Date date; public MovieEvent() { } public MovieEvent(String eventMessage, Date date) { this.eventMessage = eventMessage; this.date = date; } public String getEventMessage() { return eventMessage; } public void setEventMessage(String eventMessage) { this.eventMessage = eventMessage; } public Date getDate() { return date; } public void setDate(Date date) { this.date = date; } }
The Service Interfaces
The service layer is composed of two service interfaces – MovieClientService
and MovieClientEventService
.
The service interfaces are as follows.
MovieClientService.java
//package springframework.guru.webclientdemo.service; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import reactor.core.publisher.Mono; import springframework.guru.webclientdemo.domain.Movie; public interface MovieClientService { public Mono searchMovieByTitle(String apiKey, String title); public Mono searchMovieById(String apiKey, String imdbId); }
MovieClientEventService.java
//package springframework.guru.webclientdemo.service; import reactor.core.publisher.Flux; import springframework.guru.webclientdemo.domain.MovieEvent; public interface MovieClientEventService { public Flux getMovieEvents(); }
The Service Implementations
The MovieClientServiceImplementation
class implements the MovieClientService
interface. In this class, we will use WebClient to send requests to the OMDB API to search a movie by ID and title.
For this example, I have specified the OMDB API access key in the application.properties
file, like this.
app.api.key=MY_API_KEY_VALUE
The code of the MovieClientServiceImplementation
class is this.
MovieClientServiceImplementation.java
//package springframework.guru.webclientdemo.service; import springframework.guru.webclientdemo.domain.Movie; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; @Service public class MovieClientServiceImpl implements MovieClientService{ private static final String OMDB_MIME_TYPE = "application/json"; private static final String OMDB_API_BASE_URL = "http://omdbapi.com"; private static final String USER_AGENT = "Spring 5 WebClient"; private static final Logger logger = LoggerFactory.getLogger(MovieClientServiceImpl.class); private final WebClient webClient; public MovieClientServiceImpl() { this.webClient = WebClient.builder() .baseUrl(OMDB_API_BASE_URL) .defaultHeader(HttpHeaders.CONTENT_TYPE, OMDB_MIME_TYPE) .defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT) .build(); } @Override public Mono searchMovieByTitle(String apiKey, String title) { return webClient.post() .uri("/?apikey="+apiKey+"&t=+"+title) .retrieve() .bodyToMono(Movie.class); } @Override public Mono searchMovieById(String apiKey, String imdbId) { return webClient.post() .uri("/?apikey="+apiKey+"&i="+imdbId) .retrieve() .bodyToMono(Movie.class); } }
In the preceding code:
- The constructor of the
MovieClientServiceImplementation
creates aWebClient
using aWebClient.Builder
obtained from a call to thebuilder()
method. - Line 24 – Line 27 configures the WebClient through method chaining with the base URL and the
CONTENT_TYPE
andUSER_AGENT
headers. - Line 30 – Line 35 implements the
searchMovieByTitle()
method to perform a request with the API key and movie title. Theretrieve()
method returns aWebClient.ResponseSpec
whosebodyToMono()
extracts the response body to aMono
. - Line 38 -Line 43 implements the
searchMovieById()
method in the same way, but by passing the movie ID instead of the title in the URL.
The MovieClientEventServiceImpl
class implements the MovieClientEventService
interface to communicate with our producer of MovieEvent
stream.
The code MovieClientEventServiceImpl
service implementation is this.
MovieClientEventServiceImpl.java
//package springframework.guru.webclientdemo.service; import reactor.core.publisher.Flux; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.ExchangeFilterFunction; import org.springframework.web.reactive.function.client.WebClient; import springframework.guru.webclientdemo.domain.MovieEvent; @Service public class MovieClientEventServiceImpl implements MovieClientEventService { private static final String API_MIME_TYPE = "application/json"; private static final String API_BASE_URL = "http://localhost:8082"; private static final String USER_AGENT = "Spring 5 WebClient"; private static final Logger logger = LoggerFactory.getLogger(MovieClientServiceImpl.class); private final WebClient webClient; public MovieClientEventServiceImpl() { this.webClient = WebClient.builder() .baseUrl(API_BASE_URL) .defaultHeader(HttpHeaders.CONTENT_TYPE, API_MIME_TYPE) .defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT) .build(); } @Override public Flux getMovieEvents() { return webClient.get() .uri("/api/v1/movies/events") .exchange() .flatMapMany(clientResponse -> clientResponse.bodyToFlux(MovieEvent.class)); } }
Note that Line 32 calls the exchange()
method instead of retrieve()
to receive the response. The exchange()
method returns a Mono
that represents the response body along with other information, such as status and headers. On the other hand, the retrieve()
method we used earlier is a lightweight way to access the response body directly.
The Controller
The REST controller of the Consumer application define endpoints for clients to query for movies and subscribe to events.
The MovieController
class is this.
MovieController.java
//package springframework.guru.webclientdemo.controller; import org.springframework.core.env.Environment; import org.springframework.http.MediaType; import reactor.core.publisher.Flux; import springframework.guru.webclientdemo.domain.Movie; import springframework.guru.webclientdemo.domain.MovieEvent; import springframework.guru.webclientdemo.service.MovieClientEventService; import springframework.guru.webclientdemo.service.MovieClientService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; import org.springframework.web.reactive.function.client.WebClientResponseException; import reactor.core.publisher.Mono; @RestController @RequestMapping("/api/v1") public class MovieController { private static final Logger logger = LoggerFactory.getLogger(MovieController.class); private MovieClientService movieClientService; private MovieClientEventService movieClientEventService; private Environment env; @Autowired public MovieController(MovieClientService movieClientService, MovieClientEventService movieClientEventService, Environment env){ this.movieClientService=movieClientService; this.movieClientEventService=movieClientEventService; this.env=env; } @GetMapping("/movies/title/{name}") public Mono getMovieByTitle(@PathVariable String name) { String apiKey = env.getProperty("app.api.key"); return movieClientService.searchMovieByTitle(apiKey, name); } @GetMapping("/movies/id/{imdbId}") public Mono getMovieById(@PathVariable String imdbId) { return movieClientService.searchMovieById(env.getProperty("app.api.key"), imdbId); } @GetMapping(value = "/movies/events",produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux getEvents() { return movieClientEventService.getMovieEvents(); } @ExceptionHandler(WebClientResponseException.class) public ResponseEntity handleWebClientResponseException(WebClientResponseException ex) { logger.error("Error from WebClient - Status {}, Body {}", ex.getRawStatusCode(), ex.getResponseBodyAsString(), ex); return ResponseEntity.status(ex.getRawStatusCode()).body(ex.getResponseBodyAsString()); } }
Testing Endpoints with WebTestClient
To test endpoints, Spring 5 WebFlux framework comes with a WebTestClient
class. WebTestClient
is a thin shell around WebClient. You can use it to perform requests and verify responses.
WebTestClient
binds to a WebFlux application using a mock request and response, or it can test any web server over an HTTP connection.
Our first test uses WebTestClient
to test the movie search endpoints exposed by out Producer RESTful service.
The code of the MovieClientServiceImplTest
is this.
MovieClientServiceImplTest.java
//package springframework.guru.webclientdemo.service; import org.junit.Before; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import springframework.guru.webclientdemo.domain.Movie; import org.assertj.core.api.Assertions; import org.junit.FixMethodOrder; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.MethodSorters; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Mono; import java.time.Duration; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @FixMethodOrder(MethodSorters.NAME_ASCENDING) //@AutoConfigureWebTestClient(timeout = "36000") public class MovieClientServiceImplTest { @Autowired private WebTestClient webTestClient; @Before public void setUp() { webTestClient = webTestClient .mutate() .responseTimeout(Duration.ofMillis(36000)) .build(); } @Test public void testGetMovieById() { webTestClient.get() .uri("/api/v1/movies/id/{imdbId}","tt3896198" ) .accept(MediaType.APPLICATION_JSON) .exchange() .expectStatus().isOk() .expectBody() .consumeWith(response -> Assertions.assertThat(response.getResponseBody()).isNotNull()); } @Test public void testGetMovieByName() { webTestClient.get() .uri("/api/v1/movies/title/{name}", "Superman") .exchange() .expectStatus().isOk() .expectBody() .consumeWith(response -> Assertions.assertThat(response.getResponseBody()).isNotNull()); } }
In the preceding code:
- Line 27 autowires in
WebTestClient
to the test class. - Line 31 – Line 36 mutates the response timeout property of
WebTestClient
and builds it. - Line 38 – Line 42 of the first test case sets up a GET request and performs the request through
exchange()
- Line 43- Line 46 after
exchange()
is a chained API workflow to verify responses. - Line 49 – Line 58 tests the endpoint that accepts search requests of movies by title.
Our second test uses WebTestClient
to test the event source endpoint exposed by out Producer RESTful service.
The code of the MovieClientServiceImplTest
is this.
MovieClientEventServiceImplTest.java
//package springframework.guru.webclientdemo.service; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.http.MediaType; import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.web.reactive.server.FluxExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; import reactor.core.publisher.Flux; import reactor.test.StepVerifier; import springframework.guru.webclientdemo.domain.MovieEvent; @RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureWebTestClient(timeout = "36000") public class MovieClientEventServiceImplTest { @Autowired private WebTestClient webTestClient; @Test public void getMovieEvents() { FluxExchangeResult result = webTestClient.get().uri("/api/v1/movies/events" ) .accept(MediaType.TEXT_EVENT_STREAM) .exchange() .expectStatus().isOk() .returnResult(MovieEvent.class); } }
Summary
One common question is whether WebClient is replacing the traditional RestTemplate, n
ot at this time. RestTemplate
will continue to exist within the Spring Framework for the foreseeable future.
The primary differentiating factor is that RestTemplate
continues to use the Java Servlet API and is synchronous blocking. This means, a call done using RestTemplate
needs to wait till the response comes back to proceed further.
On the other hand, as WebClient is asynchronous, the rest call need not wait till response comes back. Instead when there is a response, a notification will be provided.
Get The Source!
Like all of my tutorials, the source code for this post is available on GitHub here.
Eduardo Alfonso Sanchez
Hello, thx for the post, very useful!!!.
I have a doubt. WebClient is thread safe? for example: can i use one WebClient instance in whole application? Because i supose that is thread safe by the documentation say that but i am not sure because several thread can set the uri at the same time
webTestClient.get()
.uri(“Diferent threads can change the uri at same time” )
Thank you
Manny
Excellent article. Wondering if there is an alternative to make SOAP client from Spring reactive library. Please reply
Arun
Did you find the alternative for SOAP client from Sprint Reactive Library?
Chao
I dont understand how request an HTTPS serveur, how initialize the contextSSL of my WebClient
Bhagawant
i tried your application.Getting the WebClientResponseException: 411 Length Required. Added content_length as given below.
this.webClient = WebClient.builder()
.baseUrl(OMDB_API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, OMDB_MIME_TYPE)
.defaultHeader(HttpHeaders.CONTENT_LENGTH, “0”)
.defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT)
.build();