Spring Boot Webclient Example

Okay, let’s break down Spring Boot WebClient configuration. Here’s a comprehensive guide covering various aspects, from basic setup to advanced customization:

1. Basic Setup (Simplest Configuration):

  • Dependency: First, make sure you have the spring-boot-starter-webflux dependency in your pom.xml (Maven) or build.gradle (Gradle) file. This starter includes WebClient and Reactive Streams support.

    <!-- Maven -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    
    // Gradle
    implementation 'org.springframework.boot:spring-boot-starter-webflux'
    
  • Basic Usage (Without Custom Configuration): Spring Boot auto-configures a WebClient.Builder bean, which you can inject into your components.

    import org.springframework.web.reactive.function.client.WebClient;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class MyApiClient {
    
        private final WebClient.Builder webClientBuilder;
    
        @Autowired
        public MyApiClient(WebClient.Builder webClientBuilder) {
            this.webClientBuilder = webClientBuilder;
        }
    
        public String fetchData(String url) {
            WebClient webClient = webClientBuilder.build();  // Create a WebClient instance
            return webClient.get()
                            .uri(url)
                            .retrieve()
                            .bodyToMono(String.class) // Example: retrieve as String
                            .block(); //  Block for result (for demonstration only - avoid in reactive flows)
        }
    }
    

    Explanation:

    • @Autowired WebClient.Builder webClientBuilder: Injects the auto-configured WebClient.Builder. This is the foundation for creating WebClient instances.
    • webClientBuilder.build(): Creates a WebClient instance using the default configuration.
    • webClient.get().uri(url): Specifies a GET request to the provided URL.
    • retrieve(): Retrieves the response body. You can use exchange() for more control over the response, including access to headers and status codes, but it’s generally more complex.
    • bodyToMono(String.class): Converts the response body to a Mono<String>. Mono represents a reactive stream that emits zero or one element.
    • .block(): This is a blocking operation and should generally be avoided in reactive applications. It’s used here for simplicity in the example. In a real reactive application, you would subscribe to the Mono or Flux and handle the result asynchronously. Prefer using subscribe() or operators like map() or flatMap().

2. Custom Configuration (Recommended for most real-world scenarios):

  • Creating a Custom WebClient Bean: Define a @Bean method to configure your WebClient. This provides maximum flexibility.

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.reactive.function.client.WebClient;
    import io.netty.channel.ChannelOption;
    import io.netty.handler.timeout.ReadTimeoutHandler;
    import io.netty.handler.timeout.WriteTimeoutHandler;
    import org.springframework.http.client.reactive.ReactorClientHttpConnector;
    import reactor.netty.http.client.HttpClient;
    import java.time.Duration;
    import java.util.concurrent.TimeUnit;
    
    @Configuration
    public class WebClientConfig {
    
        @Bean
        public WebClient webClient() {
            HttpClient httpClient = HttpClient.create()
                    .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) // Connection Timeout
                    .responseTimeout(Duration.ofSeconds(5)) // Response Timeout
                    .doOnConnected(conn ->
                            conn.addHandlerLast(new ReadTimeoutHandler(5, TimeUnit.SECONDS))  // Read Timeout
                                    .addHandlerLast(new WriteTimeoutHandler(5, TimeUnit.SECONDS))); // Write Timeout
    
            return WebClient.builder()
                    .baseUrl("https://api.example.com") // Optional: Base URL for all requests
                    .clientConnector(new ReactorClientHttpConnector(httpClient))
                    .defaultHeader("Content-Type", "application/json") // Example: Default header
                    //.filter(logRequest()) // Example: Request logging filter
                    .build();
        }
    }
    

    Key Customization Options:

    • baseUrl(String baseUrl): Sets a base URL. Subsequent requests will be relative to this URL. This is very useful if your API client interacts with a single service.

    • defaultHeader(String headerName, String headerValue): Adds a default header to all requests made by this WebClient. Common headers are Content-Type, Accept, Authorization, etc. You can add multiple default headers.

    • filter(ExchangeFilterFunction filter): Adds a filter to intercept and modify requests and responses. This is a powerful mechanism for things like:

      • Logging: Log request and response details.
      • Authentication: Add authorization headers dynamically.
      • Retry Logic: Implement retry mechanisms for failed requests.
      • Error Handling: Intercept errors and handle them globally.
    • clientConnector(ClientHttpConnector connector): Configures the underlying HTTP client. The most common connector is ReactorClientHttpConnector, which uses Reactor Netty (non-blocking I/O). This allows you to configure low-level HTTP client settings.

      • Timeouts: Important for preventing your application from hanging indefinitely when a remote service is slow or unavailable. You can configure:
        • connectTimeout: The maximum time to establish a connection.
        • responseTimeout: The maximum time to wait for the first byte of the response.
        • readTimeout: The maximum time of inactivity between two data packets when reading data from server.
        • writeTimeout: The maximum time of inactivity between two data packets when sending data to server.
  • Using the Custom WebClient Bean:

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    import org.springframework.web.reactive.function.client.WebClient;
    
    @Service
    public class MyApiClient {
    
        private final WebClient webClient;
    
        @Autowired
        public MyApiClient(WebClient webClient) {
            this.webClient = webClient;
        }
    
        public String fetchData(String path) {
            return webClient.get()
                    .uri(path) // Append path to the base URL defined in the configuration
                    .retrieve()
                    .bodyToMono(String.class)
                    .block(); // Avoid block in production code
        }
    }
    

    Now you’re injecting the custom WebClient bean that you configured in WebClientConfig. The baseUrl from the configuration will be used, so you only need to provide the path in your service method.

3. Advanced Configuration Options:

  • Exchange Filters (Request and Response Interceptors):

    import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import reactor.core.publisher.Mono;
    
    public class WebClientFilters {
    
        private static final Logger log = LoggerFactory.getLogger(WebClientFilters.class);
    
        public static ExchangeFilterFunction logRequest() {
            return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
                log.info("Request: {} {}", clientRequest.method(), clientRequest.url());
                clientRequest.headers().forEach((name, values) -> values.forEach(value -> log.info("{}={}", name, value)));
                return Mono.just(clientRequest);
            });
        }
    
        public static ExchangeFilterFunction logResponse() {
            return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
                log.info("Response: {}", clientResponse.statusCode());
                clientResponse.headers().asHttpHeaders().forEach((name, values) -> values.forEach(value -> log.info("{}={}", name, value)));
                return Mono.just(clientResponse);
            });
        }
    }
    

    Then, in your WebClientConfig:

    @Configuration
    public class WebClientConfig {
    
        @Bean
        public WebClient webClient() {
            return WebClient.builder()
                    .baseUrl("https://api.example.com")
                    .filter(WebClientFilters.logRequest())
                    .filter(WebClientFilters.logResponse())
                    .build();
        }
    }
    
  • Error Handling (Using onStatus() and onErrorResume()):

    import org.springframework.web.reactive.function.client.WebClient;
    import org.springframework.http.HttpStatus;
    import reactor.core.publisher.Mono;
    import org.springframework.web.server.ResponseStatusException;
    
    public class WebClientErrorHandling {
    
        public static void main(String[] args) {
            WebClient webClient = WebClient.create("https://example.com");
    
            Mono<String> result = webClient.get()
                    .uri("/nonexistent-resource")
                    .retrieve()
                    .onStatus(HttpStatus::is4xxClientError, clientResponse -> {
                        // Handle 4xx errors
                        return Mono.error(new ResponseStatusException(clientResponse.statusCode(), "Client error"));
                    })
                    .onStatus(HttpStatus::is5xxServerError, clientResponse -> {
                        // Handle 5xx errors
                        return Mono.error(new ResponseStatusException(clientResponse.statusCode(), "Server error"));
                    })
                    .bodyToMono(String.class)
                    .onErrorResume(ResponseStatusException.class, ex -> {
                        // Custom error handling for ResponseStatusException
                        System.err.println("Error: " + ex.getMessage());
                        return Mono.just("Error occurred: " + ex.getStatus()); // Return a default value
                    });
    
            String data = result.block();
            System.out.println(data);
        }
    }
    

    Explanation:

    • onStatus(Predicate<HttpStatus> statusPredicate, Function<ClientResponse, Mono<? extends Throwable>> errorFunction): Executes the errorFunction if the response status code matches the statusPredicate. You can handle different status code ranges (e.g., 4xx, 5xx) separately. Inside the errorFunction, you should return a Mono that emits an error.
    • onErrorResume(Class<? extends Throwable> exceptionType, Function<Throwable, ? extends Mono<? extends T>> fallback): Catches exceptions of the specified type (exceptionType) and executes the fallback function. This allows you to provide alternative behavior when an error occurs (e.g., return a default value, retry the request, log the error, etc.).
  • Authentication:

    • Basic Authentication:

      import org.springframework.http.HttpHeaders;
      import org.springframework.util.Base64Utils;
      import java.nio.charset.StandardCharsets;
      
      public class WebClientAuthentication {
      
          public static HttpHeaders createBasicAuthHeaders(String username, String password) {
              String auth = username + ":" + password;
              byte[] encodedAuth = Base64Utils.encode(auth.getBytes(StandardCharsets.UTF_8));
              String authHeader = "Basic " + new String(encodedAuth);
              HttpHeaders headers = new HttpHeaders();
              headers.set(HttpHeaders.AUTHORIZATION, authHeader);
              return headers;
          }
      
          public static void main(String[] args) {
              HttpHeaders headers = createBasicAuthHeaders("your_username", "your_password");
      
              WebClient webClient = WebClient.builder()
                      .defaultHeaders(httpHeaders -> httpHeaders.addAll(headers))
                      .build();
      
              // Now use the webClient to make requests that require basic authentication.
          }
      }
      

      Or, you can use an ExchangeFilterFunction for more dynamic authentication:

      import org.springframework.web.reactive.function.client.ExchangeFilterFunction;
      import org.springframework.http.HttpHeaders;
      import org.springframework.util.Base64Utils;
      import java.nio.charset.StandardCharsets;
      import reactor.core.publisher.Mono;
      
      public class WebClientAuthentication {
      
          public static ExchangeFilterFunction basicAuthentication(String username, String password) {
              return ExchangeFilterFunction.ofRequestProcessor(clientRequest -> {
                  String auth = username + ":" + password;
                  byte[] encodedAuth = Base64Utils.encode(auth.getBytes(StandardCharsets.UTF_8));
                  String authHeader = "Basic " + new String(encodedAuth);
                  return Mono.just(clientRequest.mutate()
                          .header(HttpHeaders.AUTHORIZATION, authHeader)
                          .build());
              });
          }
      
          public static void main(String[] args) {
              WebClient webClient = WebClient.builder()
                      .filter(basicAuthentication("your_username", "your_password"))
                      .build();
      
              // Now use the webClient to make requests that require basic authentication.
          }
      }
      
    • Bearer Token (OAuth 2): Similar to Basic Authentication, but you add the Authorization header with the Bearer scheme.

      import org.springframework.http.HttpHeaders;
      import org.springframework.web.reactive.function.client.WebClient;
      
      public class WebClientBearerToken {
          public static void main(String[] args) {
              String accessToken = "your_access_token";
              WebClient webClient = WebClient.builder()
                      .defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
                      .build();
              //Use the webClient
          }
      }
      
  • Serialization/Deserialization (Codecs):

    Spring Boot provides default codecs (JSON, XML, etc.). If you need to customize serialization or deserialization (e.g., use a different JSON library, handle custom date formats), you can configure the ExchangeStrategies. This is less common but possible.

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.reactive.function.client.ExchangeStrategies;
    import org.springframework.http.codec.json.Jackson2JsonDecoder;
    import org.springframework.http.codec.json.Jackson2JsonEncoder;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.PropertyNamingStrategy;
    
    @Configuration
    public class WebClientConfig {
    
        @Bean
        public WebClient webClient(ObjectMapper objectMapper) {
            // Customize the ObjectMapper (e.g., property naming strategy)
            objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
    
            ExchangeStrategies strategies = ExchangeStrategies.builder()
                    .codecs(configurer -> {
                        configurer.defaultCodecs().jackson2JsonDecoder(new Jackson2JsonDecoder(objectMapper));
                        configurer.defaultCodecs().jackson2JsonEncoder(new Jackson2JsonEncoder(objectMapper));
                    }).build();
    
    
            return WebClient.builder()
                    .baseUrl("https://api.example.com")
                    .exchangeStrategies(strategies)
                    .build();
        }
    }
    

4. Best Practices and Considerations:

  • Asynchronous and Non-Blocking: WebClient is designed for reactive, non-blocking I/O. Embrace the reactive paradigm. Avoid .block() unless you have a very specific reason to use it (e.g., in a startup initialization).
  • Timeouts: Always configure appropriate timeouts to prevent your application from becoming unresponsive.
  • Error Handling: Implement robust error handling to gracefully handle failures and prevent errors from propagating.
  • Logging: Log requests and responses (especially in development and debugging) to understand what’s happening.
  • Retry Logic: Consider implementing retry mechanisms for transient errors.
  • Connection Pooling: Reactor Netty, used by default, provides connection pooling. Tune the pool size if necessary based on your application’s load.
  • Metrics: Integrate WebClient with metrics libraries (e.g., Micrometer) to monitor its performance.
  • Testing: Write unit tests and integration tests to verify your WebClient configuration and behavior. Use MockWebServer or WireMock for mocking external services in your tests.
  • Thread Safety: WebClient instances are generally thread-safe and can be reused. However, the WebClient.Builder is not thread-safe. Create a new WebClient instance from the builder for each thread if you’re using the builder directly in a multithreaded context. Usually, you’ll inject a configured WebClient bean, which eliminates this concern.

Example: Making a POST Request with JSON:

import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;

public class WebClientPostExample {

    public static void main(String[] args) {
        WebClient webClient = WebClient.create("https://example.com/api"); // Replace with your API endpoint

        // Sample request body (you'd typically use a Java object and Jackson for JSON serialization)
        String requestBody = "{\"name\": \"John Doe\", \"email\": \"john.doe@example.com\"}";

        Mono<String> response = webClient.post()
                .uri("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .bodyValue(requestBody)
                .retrieve()
                .bodyToMono(String.class);

        response.subscribe(
                result -> System.out.println("Response: " + result),
                error -> System.err.println("Error: " + error)
        );
    }
}

In summary:

  1. Start with the spring-boot-starter-webflux dependency.
  2. Create a @Configuration class and define a @Bean method to configure your WebClient.
  3. Use the WebClient.Builder to customize the base URL, default headers, timeouts, codecs, filters, and other options.
  4. Inject the configured WebClient bean into your services or components.
  5. Embrace the reactive programming model by using Mono and Flux and avoiding .block() in production code.
  6. Implement comprehensive error handling, logging, and retry logic.
  7. Test your WebClient configuration thoroughly.

By following these steps, you can effectively configure Spring Boot WebClient to meet the specific requirements of your application. Remember to tailor the configuration to your API’s needs and prioritize resilience and performance.

Leave a Comment

Your email address will not be published. Required fields are marked *


The reCAPTCHA verification period has expired. Please reload the page.

Scroll to Top