You are here
Home > java >

WebClient in Spring Boot

WebClient in Spring BootA typical modern web application generally has four well known layers i.e. Presentation Layer, Service Layer and Data Layer, and an Integration Layer. The Integration layer generally works with the web services concept and connects two different applications to exchange data between them. One application refers to producer/provider, whereas other refers to consumers. Here, we will discuss about REST consumer/client API using WebClient in Spring Boot.

The traditional way of implementing REST client API is by using RestTemplate.  However, we have already gone through the RestTemplate in another article where we had developed different operations to consume REST API. Now, we must have a question in mind ‘How to write REST Consumer API using WebClient in Spring Boot?’. Needless to say, WebClient in Spring Boot provides us Consumer methods to consume services provided by producer applications. Accordingly, let’s start discussing on ‘WebClient in Spring Boot’.

Why WebClient?

As aforementioned, RestTemplate is one of the popular REST Client.  As of 5.0 this class is in maintenance mode, with only minor requests for changes and bugs to be accepted going forward. Please, consider using the org.springframework.web.reactive.client.WebClient which has a more modern API and supports sync, async, and streaming scenarios.

As we already know, for non-reactive REST producer (Servlet Stack REST producer), we generally use RestTemplate in order to develop a consumer API. Please also note that Client Application or Consumer Application both we use in the same sense. Here in place of RestTemplate we will use WebClient that supports making request to Reactive RestController and gets final Data either as Mono or as Flux. Moreover WebClient is an interface (DefaultWebClient is an impl class) that is used to define Reactive Client Application.

When To Use WebClient?

WebClient is a reactive, non-blocking REST Client solution that works over the HTTP/1.1 protocol. Please note that, even though the WebClient is a non-blocking client but it offers support for both synchronous and asynchronous operations. Hence, it is acceptable for applications running on a Servlet Stack as well.

If we are working on a non-reactive or servlet stack environment, then we can obtain the results by blocking the operations. In simple words, it can be achieved by calling a block() method. We will be doing this exercise in the later sections of this article. Obviously, this practice is not recommended if we are working on a reactive stack.

About WebClient in Spring Boot

1) Released as part of Spring 5.x as a Spring WebFlux module. As part of this, Spring 5.x introduced the new WebClient API, replacing the existing RestTemplate client.

2) Supports functional style API that takes advantage of Java 8 Lambdas.

3) Synchronous and Asynchronous REST API Client. It is Asynchronous by default.

4) WebClient is a reactive, non-blocking, highly concurrent REST Client solution with less resource intensive framework that works over the HTTP/1.1 protocol.

5) Supports both traditional Servlet stack and Spring reactive stack.

6) We can integrate it directly into our existing Spring configuration easily.

How to add WebClient in Spring Boot?

In order to make use of WebClient APIs in your Spring based project, you need to add a dependency to your project if you don’t have it already. If you are working on Spring Boot, you can add spring-boot-starter-webflux dependency as shown below.

<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-webflux</artifactId>
</dependency>

How to Create a WebClient Instance?

Typically, there are three options to create a WebClient Instance. Let’s discuss them one by one.

1) Creating WebClient object with default settings:

WebClient webClient = WebClient.create(); 

2) Creating a WebClient object with a base URI:

WebClient webClient = WebClient.create("http://localhost:8080");

3) Creating a WebClient object by using the  DefaultWebClientBuilder class:

WebClient webClient = WebClient
          .builder()
          .baseUrl("http://localhost:8080")
          .defaultCookie("Key", "Value")
          .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) 
          .defaultUriVariables(Collections.singletonMap("url", "http://localhost:8080"))
          .build();

The last option is the most advanced one and allows full customization on the client behavior as per our requirement.

retrieve() vs exchange() in WebClient

While using the WebClient in Spring Boot, we have the option to use either retrieve() or exchange() method. Let’s look at the difference between these two methods to understand when to use which one.

1) When we are interested in response body, we should prefer to use the retrieve() method.

2) When we are interested in more control and all the response elements like response status, headers and response body, etc., we should prefer to use exchange() method. The exchange() method returns ClientResponse having the response status and headers. We can get the response body from ClientResponse instance.

3) retrieve() method provides automatic error signal (e.g. 4xx and 5xx), whereas no automatic error signal is available in case of exchange() method and we need to check status code and handle it accordingly.

How to Send a request using WebClient?

Let’s understand the basic programming structure that is most commonly used to send a request using WebClient. Consider the below code:

WebClient webClient = WebClient.create();

WebClient.ResponseSpec responseSpec = webClient.get()
    .uri("//javatechonline.com")
    .retrieve();

Let’s understand what is happening in the code above.

We created a WebClient instance by calling it’s create() method.

Using WebClient instance, we specified the http GET request method and the URI to call.

We called retrieve() method to get a ResponseSpec for the request. Using ResponseSpec, we usually want to read the contents of the response.

Please note that this is an asynchronous operation, which doesn’t block or wait for the request itself, which means that on the following line the request is still pending, and so we can’t yet access any of the response details.

String responseBody = responseSpec.bodyToMono(String.class).block();

In order to read the response body, we need to get a Mono/Flux for the content of the response. Once the response body content is available, we need to unwrap that somehow. To unwrap the response, we will use the simplest form that will provide us a string containing the raw body of the response. However, It’s possible to pass different classes here to parse content automatically into an appropriate format that we will use in the later sections of this article.

How to develop REST Client Application using WebClient in Spring Boot?

There are some guidelines to follow in order to develop REST client Application using WebClient in Spring Boot.

Guidelines to develop Reactive Client Application with WebClient

Please follow below guidelines to create a Client application using WebClient in Spring Boot. These are the most commonly used guidelines to make a WebClient request.

  1. Create WebClient Object using Base URL or by calling uri() method.
  2. Provide Path at Controller method using Request METHOD(GET/POST)
  3. Provide Inputs if exist (Body, Params)
  4. Create Request for data retrieval with Type mono/flux

♥ Note: Model class must exist & be exactly same as in Producer Application.

Step#1: Create Project using STS(Spring Tool Suite)

Create one Spring Boot Starter Project. Let’s name it SpringBoot2ReactiveClientApp. While creating starter project in STS, select starter dependencies as ‘Spring Reactive Web’ and ‘Lombok’. You can also add ‘Spring Boot DevTools’ optionally. If you are new to ‘Lombok’, kindly visit ‘How to configure Lombok‘ and to know all about it in detail.

Step#2 : Update server properties in application.properties file

You need to update the server port and it should be different from the producer application’s server port as you might be running both applications from the same system. Update it something like below.

server.port=9090

Step#3: Create Model class Invoice.java

Please make sure this model class must be the same as it is in the producer application particularly in terms of fields and their datatypes.

//Invoice.java
package com.dev.springboot.reactive.model;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@Data
@AllArgsConstructor
@RequiredArgsConstructor
public class Invoice {
    
	private Integer id;
	@NonNull
	private String name;
	@NonNull
	private String number;
	@NonNull
	private Double amount;
}

Now its time to really test the API and get the results accordingly. Here we are using Runner classes to test each method in a separate Runner class as in step#4 to step#7. The best way to test and get correct results is that keep @Component at one Runner class at a time and comment it on others which are not part of your current testing.

Step#4: Runner class to fetch/retrieve all Invoices

//GetAllInvoicesRunner.java
package com.dev.springboot.reactive.runner;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import com.dev.springboot.reactive.model.Invoice;

import reactor.core.publisher.Flux;

@Component 
public class GetAllInvoicesRunner implements CommandLineRunner {
        
        private static final String baseUrl= "http://localhost:8080/invoice";
	@Override
	public void run(String... args) throws Exception {
		
		WebClient client = WebClient.create(baseUrl);
		Flux<Invoice> flux= client
				.get()
				.uri("/allInvoices")
				.retrieve()
				.bodyToFlux(Invoice.class);
		flux.doOnNext(System.out::println).blockLast();
	}
}

Step#5: Runner class to fetch/retrieve one Invoice

//GetOneInvoiceRunner.java
package com.dev.springboot.reactive.runner;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import com.dev.springboot.reactive.model.Invoice;

import reactor.core.publisher.Mono;

@Component
public class GetOneInvoiceRunner implements CommandLineRunner {
 
	private static final String baseUrl= "http://localhost:8080/invoice";

        @Override
	public void run(String... args) throws Exception {
		
		WebClient client = WebClient.create(baseUrl);
		Mono<Invoice> mono= client
				.get()
				.uri("/get/3")
				.retrieve()
				.bodyToMono(Invoice.class);
		mono.subscribe(System.out::println);
	}

}

Step#6: Runner class to save or update Invoice

//SavrOrUpdateInvoiceRunner.java
package com.dev.springboot.reactive.runner;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import com.dev.springboot.reactive.model.Invoice;

import reactor.core.publisher.Mono;

@Component
public class SavrOrUpdateInvoiceRunner implements CommandLineRunner {

        private static final String baseUrl= "http://localhost:8080/invoice";

	@Override
	public void run(String... args) throws Exception {
		
		WebClient client = WebClient.create(baseUrl);
		Mono<Invoice> mono= client
				.post()
				.uri("/save")
				.body(Mono.just(new Invoice(1, "Invoice1", "INV001", 2345.75)),Invoice.class)
				.retrieve().bodyToMono(Invoice.class);
		mono.subscribe(System.out::println);
	}
}

Step#7: Runner class to delete Invoice

//DeleteInvoiceRunner.java
package com.dev.springboot.reactive.runner;

import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;

import reactor.core.publisher.Mono;

@Component
public class DeleteInvoiceRunner implements CommandLineRunner {

        private static final String baseUrl= "http://localhost:8080/invoice";

	@Override
	public void run(String... args) throws Exception {
		
		WebClient client = WebClient.create(baseUrl);
		Mono<Void> mono= client
				.delete()
				.uri("/delete/3")
				.retrieve()
				.bodyToMono(Void.class);
		mono.subscribe(System.out::println);
		System.out.println("Invoice Deleted!");
	}
}

How to test REST API Using REST Client Application?

Please follow below steps to test results produced by the REST API with the help of REST Client:

1) First of all, don’t forget to run the REST Client Application.

2) Start respective runner classes one by one to test the results.

In this section, let’s write a class that will cover all the methods such as Get, Post, Put, Delete.

Step#1: Create a class that holds all the constants for end points

public class InvoiceConstants {

    public static final String ADD_INVOICE= "/invoice";

    public static final String GET_ALL_INVOICES = "/allInvoices";

    public static final String GET_INVOICE_BY_ID = "/get/3";

    public static final String GET_INVOICE_BY_NAME = "/invoiceName";

}

Step#2: Create a class that holds all the methods for each endpoint

The class below has all the most commonly used methods in the real time project. You may find one thing new in the code which is an annotation @Slf4j on top of the class. In fact, this annotation came from Lombok API. You may find more details on this from the separate article on Lombok API.

import java.util.List;

import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.reactive.function.client.WebClientResponseException;
import org.springframework.web.util.UriComponentsBuilder;

import com.dev.springboot.reactive.model.Invoice;

import lombok.extern.slf4j.Slf4j;

import static com.dev.springboot.reactive.util.InvoiceConstants.ADD_INVOICE;
import static com.dev.springboot.reactive.util.InvoiceConstants.GET_ALL_INVOICES;
import static comp.dev.springboot.reactive.util.InvoiceConstants.GET_INVOICE_BY_ID;
import static com.dev.springboot.reactive.util.InvoiceConstants.GET_INVOICE_BY_NAME;

@Slf4j
public class InvoiceRestClient {

   private WebClient webClient;

   public InvoiceRestClient(WebClient webclient) {
      this.webClient = webclient;
   }

   public List<Invoice> retrieveAllInvoices(){

      try {
          return webClient
           .get()                          //retrieving values, so get()
           .uri(GET_ALL_INVOICES)
           .retrieve()
           .bodyToFlux(Invoice.class)      //returning multiple values, so bodyToFlux
           .collectList()
           .block();                       //to make it synchronous call
      } catch (WebClientResponseException wcre) {
          log.error("Error Response Code is {} and Response Body is {}"
                  ,wcre.getRawStatusCode(), wcre.getResponseBodyAsString());
          log.error("Exception in method retrieveAllInvoices()",wcre);
          throw wcre;
      } catch (Exception ex) {
          log.error("Exception in method retrieveAllInvoices()",ex);
          throw ex;
      }
   }

   public Invoice retrieveInvoiceById(Integer id){

      try {
          return webClient
           .get()                              //retrieving values, so get()
           .uri(GET_INVOICE_BY_ID, id)
           .retrieve()
           .bodyToMono(Invoice.class)          //returning single value, so bodyToMono
           .block();                           // to make it synchronous call
      } catch (WebClientResponseException wcre) {
          log.error("Error Response Code is {} and Response Body is {}"
              ,wcre.getRawStatusCode(), wcre.getResponseBodyAsString());
          log.error("Exception in method retrieveInvoiceById()",wcre);
          throw wcre;
      } catch (Exception ex) {
          log.error("Exception in method retrieveInvoiceById()",ex);
          throw ex;
      }
   }

   public List<Invoice> retrieveInvoiceByName(String invoiceName){

    //URL with Query Parameter: http://localhost:8080/invoice/invoiceName?invoice_name=INV001

      String uri= UriComponentsBuilder
                   .fromUriString(GET_INVOICE_BY_NAME)
                   .queryParam("invoice_name", invoiceName)
                   .build().toUriString();

      try { 
          return webClient.get()
                  .uri(uri)
                  .retrieve()
                  .bodyToFlux(Invoice.class)
                  .collectList() 
                  .block();
      } catch (WebClientResponseException wcre) {
          log.error("Error Response Code is {} and Response Body is {}"
           ,wcre.getRawStatusCode(), wcre.getResponseBodyAsString());
          log.error("Exception in method retrieveInvoiceByName()",wcre);
          throw wcre;
      } catch (Exception ex) {
          log.error("Exception in method retrieveInvoiceByName()",ex);
          throw ex;
      }
   }

   public Invoice addInvoice(Invoice invoice) {

      try { 
          return webClient
                 .post()                    // adding Invoice is a post() request
                 .uri(ADD_INVOICE)
                 .bodyValue(invoice)
                 .retrieve() 
                 .bodyToMono(Invoice.class)
                 .block();
      } catch (WebClientResponseException wcre) {
          log.error("Error Response Code is {} and Response Body is {}"
             ,wcre.getRawStatusCode(), wcre.getResponseBodyAsString());
          log.error("Exception in method addInvoice()",wcre);
          throw wcre;
      } catch (Exception ex) {
          log.error("Exception in method addInvoice()",ex);
          throw ex;
      }
   }

   public Invoice updateInvoice(Invoice invoice, Integer id) {

      try { 
          return webClient
                  .put()
                  .uri(GET_INVOICE_BY_ID, id)
                  .bodyValue(invoice)
                  .retrieve()
                  .bodyToMono(Invoice.class)
                  .block();
      } catch (WebClientResponseException wcre) {
          log.error("Error Response Code is {} and Response Body is {}"
            ,wcre.getRawStatusCode(), wcre.getResponseBodyAsString());
          log.error("Exception in method updateInvoice()",wcre);
          throw wcre;
      } catch (Exception ex) {
          log.error("Exception in method updateInvoice()",ex);
          throw ex;
      } 
   }

   public String deleteInvoiceById(Integer id) {

      try { 
          return webClient
                  .delete() 
                  .uri(GET_INVOICE_BY_ID, id)
                  .retrieve()
                  .bodyToMono(String.class)
                  .block();
      } catch (WebClientResponseException wcre) {
          log.error("Error Response Code is {} and Response Body is {}"
            ,wcre.getRawStatusCode(), wcre.getResponseBodyAsString());
          log.error("Exception in method deleteInvoiceById()",wcre);
          throw wcre;
      } catch (Exception ex) {
          log.error("Exception in method deleteInvoiceById()",ex);
          throw ex;
      } 
   }
}

Here, we assume that the WebClient instance should behave like a synchronous client. Hence, we used block() on each method.

Example Of a Complex POST request Using WebClient in Spring Boot

Sometimes we may have to implement a complex method using WebClient in Spring Boot. For example, below code snippet demonstrates a complex POST request sent by WebClient in Spring Boot. We will only demonstrate the complex part of method, method signature and the return type can be created based on the requirement.

import java.net.URI;
import java.net.URISyntaxException;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.reactive.function.BodyInserters;

public String postFormData() throws URISyntaxException {

   MultiValueMap<String, String> bodyValues = new LinkedMultiValueMap<>();

   bodyValues.add("key1", "value1");
   bodyValues.add("key2", "value2");

   String response = webClient.post()
                     .uri(new URI("https://javatechonline.com/post"))
                     .header("Authorization", "SECRET_TOKEN")
                     .contentType(MediaType.APPLICATION_JSON)
                     .accept(MediaType.APPLICATION_JSON)
                     .body(BodyInserters.fromMultipartData(bodyValues))
                     .retrieve()
                     .bodyToMono(String.class)
                     .block();
   return response;
}

Here in this example we have seen how WebClient allows us to configure headers and a complex body part. We can add a body by using different options. We can call body() with a BodyInserters. We can call body() with a Flux also. Even we can call bodyValue(value) as used it in previous examples.

How to Reset the Memory Limit?

Spring WebFlux module configures the default memory limit for buffering data in-memory to 256KB. If this limit is gone beyond 256KB due to any reason, then we may face DataBufferLimitException error.

In order to readjust the memory limit, we have an option to configure the below property in our application.properties file.

spring.codec.max-in-memory-size=10MB

Conclusion

In this article we have covered all the theoretical and example part of ‘WebClient in Spring Boot’. Finally, you should be able to implement a REST client using WebClient in Spring Boot. Similarly, we expect from you to further extend these examples, as per your requirement. Also try to implement it in your project accordingly. Moreover, Feel free to provide your comments in the comments section below.


  
  
  
close

Leave a Reply

Top