Spring 5 WebClient

Spring 5 WebClient

2 Comments

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 5: 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.

Spring WebFlux Producer Output from Netty

WebClient in the API Consumer

The API consumer is a Spring Boot project that uses WebFlux. The consumer communicates with two services:

  1. OMDB API to retrieve movie information by name, and ID.
  2. 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 a WebClient using a WebClient.Builder obtained from a call to the builder() method.
  • Line 24 – Line 27 configures the WebClient through method chaining with the base URL and the CONTENT_TYPE and USER_AGENT headers.
  • Line 30 – Line 35 implements the searchMovieByTitle() method to perform a request with the API key and movie title. The retrieve() method returns a WebClient.ResponseSpec whose bodyToMono() extracts the response body to a Mono.
  • 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.

Learn more about WebClient in my Spring Framework 5 Online Course
Online Course – Spring Framework 5: Beginner to Guru

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, not 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.

Testing Spring Boot Online Course
Online Course – Testing Spring Boot: Beginner to Guru

About jt

    You May Also Like