eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

eBook – Reactive – NPI(cat= Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

1. Overview

WebClient is an interface that simplifies the process of performing HTTP requests. Unlike RestTemplate, it’s a reactive and non-blocking client that can consume and manipulate HTTP responses. Though it’s designed to be non-blocking it can also be used in a blocking scenario.

In this tutorial, we’ll dive into key methods from the WebClient interface, including retrieve(), exchangeToMono(), and exchangeToFlux(). Also, we’ll explore the differences and similarities between these methods, and look at examples to showcase different use cases. Additionally, we’ll use the JSONPlaceholder API to fetch user data.

2. Example Setup

To begin, let’s bootstrap a Spring Boot application and add the spring-boot-starter-webflux dependency to the pom.xml:

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

This dependency provides the WebClient interface, enabling us to perform HTTP requests.

Also, let’s see a sample GET response from a request to https://jsonplaceholder.typicode.com/users/1:

{
  "id": 1,
  "name": "Leanne Graham",
// ...
}

Furthermore, let’s create a POJO class named User:

class User {

    private int id;
    private String name;

   // standard constructor,getter, and setter

}

The JSON response from the JSONPlaceholder API will be deserialized and mapped to an instance of a User class.

Finally, let’s create an instance of WebClient with the base URL:

WebClient client = WebClient.create("https://jsonplaceholder.typicode.com/users");

Here, we define the base URL for HTTP requests.

3. The exchange() Method

The exchange() method returns ClientResponse directly, thereby providing access to the HTTP status code, headers, and response body. Simply put, the ClientResponse represents an HTTP response returned by WebClient.

However, this method was deprecated since Spring version 5.3 and has been replaced by the exchangeToMono() or exchangeToFlux() method, based on what we emit. The two methods allow us to decode responses based on the response status.

3.1. Emitting a Mono

Let’s see an example that uses exchangeToMono() to emit a Mono:

@GetMapping("/user/exchange-mono/{id}")
Mono<User> retrieveUsersWithExchangeAndError(@PathVariable int id) {
    return client.get()
      .uri("/{id}", id)
      .exchangeToMono(res -> {
          if (res.statusCode().is2xxSuccessful()) {
              return res.bodyToMono(User.class);
          } else if (res.statusCode().is4xxClientError()) {
              return Mono.error(new RuntimeException("Client Error: can't fetch user"));
          } else if (res.statusCode().is5xxServerError()) {
              return Mono.error(new RuntimeException("Server Error: can't fetch user"));
          } else {
              return res.createError();
           }
     });
}

In the code above, we retrieve a user and decode responses based on the HTTP status code.

3.2. Emitting a Flux

Furthermore, let’s use exchangeToFlux() to fetch a collection of users:

@GetMapping("/user-exchange-flux")
Flux<User> retrieveUsersWithExchange() {
   return client.get()
     .exchangeToFlux(res -> {
         if (res.statusCode().is2xxSuccessful()) {
             return res.bodyToFlux(User.class);
         } else {
             return Flux.error(new RuntimeException("Error while fetching users"));
         }
    });
}

Here, we use the exchangeToFlux() method to map the response body to a Flux of User objects and return a custom error message if the request fails.

3.3. Retrieving Response Body Directly

Notably, exchangeToMono() or exchangeToFlux() can be used without specifying the response status code:

@GetMapping("/user-exchange")
Flux<User> retrieveAllUserWithExchange(@PathVariable int id) {
    return client.get().exchangeToFlux(res -> res.bodyToFlux(User.class))
      .onErrorResume(Flux::error);
}

Here, we retrieve the user without specifying the status code.

3.4. Altering Response Body

Furthermore, let’s see an example that alters the response body:

@GetMapping("/user/exchange-alter/{id}")
Mono<User> retrieveOneUserWithExchange(@PathVariable int id) {
    return client.get()
      .uri("/{id}", id)
      .exchangeToMono(res -> res.bodyToMono(User.class))
      .map(user -> {
          user.setName(user.getName().toUpperCase());
          user.setId(user.getId() + 100);
          return user;
      });
}

In the code above, after mapping the response body to the POJO class, we alter the response body by adding 100 to the id and capitalizing the name.

Notably, we can also alter the response body with the retrieve() method.

3.5. Extracting Response Headers

Also, we can extract the response headers:

@GetMapping("/user/exchange-header/{id}")
Mono<User> retrieveUsersWithExchangeAndHeader(@PathVariable int id) {
  return client.get()
    .uri("/{id}", id)
    .exchangeToMono(res -> {
        if (res.statusCode().is2xxSuccessful()) {
            logger.info("Status code: " + res.headers().asHttpHeaders());
            logger.info("Content-type" + res.headers().contentType());
            return res.bodyToMono(User.class);
        } else if (res.statusCode().is4xxClientError()) {
            return Mono.error(new RuntimeException("Client Error: can't fetch user"));
        } else if (res.statusCode().is5xxServerError()) {
            return Mono.error(new RuntimeException("Server Error: can't fetch user"));
        } else {
            return res.createError();
        }
    });
}

Here, we log the HTTP header and the content type to the console. Unlike the retrieve() method that needs to return ResponseEntity to access the headers and the response code, exchangeToMono() gives us access directly because it returns ClientResponse.

4. The retrieve() Method

The retrieve() method simplifies the extraction of a response body from an HTTP request. It returns ResponseSpec, which allows us to specify how the response body should be processed without the need to access the complete ClientResponse.

The ClientResponse includes the response code, headers, and body. Therefore, the ResponseSpec includes the response body without the response code and header.

4.1. Emitting a Mono

Here’s an example code that retrieves an HTTP response body:

@GetMapping("/user/{id}")
Mono<User> retrieveOneUser(@PathVariable int id) {
    return client.get()
      .uri("/{id}", id)
      .retrieve()
      .bodyToMono(User.class)
      .onErrorResume(Mono::error);
}

In the code above, we retrieve JSON from the base URL by making an HTTP call to the /users endpoint with a specific id. Then, we mapped the response body to the User object.

4.2. Emitting a Flux

Additionally, let’s see an example that makes a GET request to the /users endpoint:

@GetMapping("/users")
Flux<User> retrieveAllUsers() {
    return client.get()
      .retrieve()
      .bodyToFlux(User.class)
      .onResumeError(Flux::error);
}

Here, the method emits a Flux of User objects when it maps the HTTP response to the POJO class.

4.3. Returning ResponseEntity

In the case where we intend to access the response status and headers with the retrieve() method, we can return ResponseEntity:

@GetMapping("/user-id/{id}")
Mono<ResponseEntity<User>> retrieveOneUserWithResponseEntity(@PathVariable int id) {
    return client.get()
      .uri("/{id}", id)
      .accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .toEntity(User.class)
      .onErrorResume(Mono::error);
}

The response obtained using the toEntity() method contains the HTTP headers, status code, and response body.

4.4. Custom Error With onStatus() Handler

Also, when there is a 400 or 500 HTTP error, it returns the WebClientResponseException error by default. However, we can customize the exception to give a custom error response using the onStatus() handler:

@GetMapping("/user-status/{id}")
Mono<User> retrieveOneUserAndHandleErrorBasedOnStatus(@PathVariable int id) {
    return client.get()
      .uri("/{id}", id)
      .retrieve()
      .onStatus(HttpStatusCode::is4xxClientError, 
        response -> Mono.error(new RuntimeException("Client Error: can't fetch user")))
      .onStatus(HttpStatusCode::is5xxServerError, 
        response -> Mono.error(new RuntimeException("Server Error: can't fetch user")))
      .bodyToMono(User.class);
}

Here, we check the HTTP status code and use the onStatus() handler to define a custom error response.

5. Performance Comparison

Next, let’s write a performance test to compare the execution times of retrieve() and exchangeToFlux() using Java Microbench Harness (JMH)

First, let’s create a class named RetrieveAndExchangeBenchmarkTest:

@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@Warmup(iterations = 3, time = 10, timeUnit = TimeUnit.MICROSECONDS)
@Measurement(iterations = 3, time = 10, timeUnit = TimeUnit.MICROSECONDS)
public class RetrieveAndExchangeBenchmarkTest {
  
    private WebClient client;

    @Setup
    public void setup() {
        this.client = WebClient.create("https://jsonplaceholder.typicode.com/users");
    }
}

Here, we set the benchmark mode to AverageTime, which means it measures the average time for the test to execute. Also, we define the number of iterations and the time to run each iteration.

Next, we create an instance of WebClient and use the @Setup annotation to make it run before each benchmark.

Let’s write a benchmark method that retrieves a collection of users using the retrieve() method:

@Benchmark
Flux<User> retrieveManyUserUsingRetrieveMethod() {
    return client.get()
      .retrieve()
      .bodyToFlux(User.class)
      .onErrorResume(Flux::error);;
}

Finally, let’s define a method that emits a Flux of User objects using the exchangeToFlux() method:

@Benchmark
Flux<User> retrieveManyUserUsingExchangeToFlux() {
    return client.get()
      .exchangeToFlux(res -> res.bodyToFlux(User.class))
      .onErrorResume(Flux::error);
}

Here’s the benchmark result:

Benchmark                             Mode  Cnt   Score    Error  Units
retrieveManyUserUsingExchangeToFlux   avgt   15  ≈ 10⁻⁴            s/op
retrieveManyUserUsingRetrieveMethod   avgt   15  ≈ 10⁻³            s/op

Both methods demonstrate efficient performance. However, the exchangeToFlux() is slightly faster when retrieving a collection of users than the retrieve() method.

6. Key Differences and Similarities

Both retrieve() and exchangeToMono() or exchangeToFlux() can be used to make HTTP requests and extract the HTTP response.

The retrieve() method only allows us to consume the HTTP body and emit a Mono or Flux because it returns ResponseSpec. However, if we want to access the status code and headers, we can use the retrieve() method with ResponseEntity. Also, it allows us to report errors based on the HTTP status code using the onStatus() handler.

Unlike the retrieve() method, the exchangeToMono() and exchnageToFlux() allow us to consume HTTP response and access the headers and response code directly because they return ClientResponse. Furthermore, they provides more control over error handling because we can decode responses based on the HTTP status code.

Notably, in the case where the intention is to consume only the response body, the retrieve() method is advised.

However, if we need more control over the response, the exchangeToMono() or exchangeToFlux() may be the better choice.

7. Conclusion

In this article, we learned how to use the retrieve(), exchangeToMono(), and exchangeToFlux() methods to handle HTTP response and further map the response to a POJO class. Additionally, we compared the performance between the retrieve() and exchangeToFlux() methods.

The retrieve() method is good for scenarios where we only need to consume the response body, and don’t need access to the status code or headers. It simplifies the process by returning ResponseSpec, which provides a straightforward way to handle the response body.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)