Learnitweb

Spring @HttpExchange: The New and Improved Way to Build HTTP Clients

A new and powerful feature introduced in Spring Framework 6 and Spring Boot 3 is the declarative HTTP client, enabled by the @HttpExchange annotation. This modern approach simplifies the creation of HTTP clients by allowing you to define them as Java interfaces, completely removing the need for boilerplate code that was common with older clients like RestTemplate.

This tutorial will guide you through creating and using a declarative HTTP client with @HttpExchange.

What is @HttpExchange?

@HttpExchange is an annotation that marks a Java interface as a client for a remote HTTP service. Instead of manually constructing HTTP requests and handling responses, you simply define methods on the interface that correspond to the API endpoints you want to call. Spring then generates a proxy implementation for this interface at runtime, handling all the low-level details of making the HTTP call.

This approach offers several key benefits:

  • Reduced boilerplate code: You no longer need to manually set up requests, handle message converters, or manage the response lifecycle.
  • Improved readability: The interface acts as a clear contract for the remote API, making your code easier to understand and maintain.
  • Seamless integration: It leverages familiar Spring Web annotations like @PathVariable, @RequestParam, and @RequestBody.
  • Flexible underlying client: It can be used with modern, fluent clients like RestClient (synchronous) or WebClient (reactive).

Step 1: Add the Necessary Dependencies

To use @HttpExchange, you need the spring-web dependency. If you’re building a Spring Boot application, this is typically included by default with either spring-boot-starter-web (for synchronous applications) or spring-boot-starter-webflux (for reactive applications).

For a typical Spring Boot web application, your pom.xml should look something like this:

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

If you are building a reactive application, use spring-boot-starter-webflux instead.

Step 2: Define the HTTP Client Interface

Create a Java interface that represents the remote service you want to call. Use @HttpExchange at the class level to define a base URL for all methods in the interface. You can also use it on individual methods, but using the more specific @GetExchange, @PostExchange, @PutExchange, @DeleteExchange, and @PatchExchange is recommended for clarity.

Let’s assume we want to interact with a simple “post” API.

Post.java

public record Post(Integer id, String title, String body) {}

PostClient.java

import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.service.annotation.GetExchange;
import org.springframework.web.service.annotation.HttpExchange;
import org.springframework.web.service.annotation.PostExchange;

import java.util.List;

@HttpExchange("/posts")
public interface PostClient {

    @GetExchange
    List<Post> getPosts();

    @GetExchange("/{id}")
    Post getPostById(@PathVariable("id") Integer id);

    @PostExchange
    Post createPost(@RequestBody Post post);
}

In this example:

  • @HttpExchange("/posts") sets a base URL path for all methods in this interface.
  • @GetExchange and @PostExchange annotations map to the corresponding HTTP methods.
  • Familiar annotations like @PathVariable and @RequestBody are used just as they are in Spring MVC controllers.

Step 3: Configure and Create the Client Proxy

Next, you need to create an instance of your client interface. This is done using an HttpServiceProxyFactory, which takes an underlying HTTP client (RestClient or WebClient) and creates a proxy that implements your interface.

You can do this in a @Configuration class.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestClient;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;

@Configuration
public class HttpClientConfig {

    @Bean
    PostClient postClient() {
        RestClient restClient = RestClient.builder()
            .baseUrl("https://jsonplaceholder.typicode.com")
            .build();
        
        HttpServiceProxyFactory factory = HttpServiceProxyFactory
            .builderFor(RestClientAdapter.create(restClient))
            .build();
        
        return factory.createClient(PostClient.class);
    }
}

Explanation of the code:

  • We define a RestClient bean. This is the underlying client that will actually perform the HTTP requests.
  • We use HttpServiceProxyFactory to create the proxy. builderFor is used to specify the underlying client.
  • factory.createClient(PostClient.class) generates a proxy implementation of our PostClient interface.
  • This proxy is then registered as a Spring bean, which means you can @Autowired it anywhere in your application.

For Reactive Applications (WebClient):

If you’re building a reactive application, the configuration is very similar, but you would use WebClient instead of RestClient.

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
import reactor.core.publisher.Flux;

@Configuration
public class ReactiveHttpClientConfig {

    @Bean
    PostClient postClient() {
        WebClient webClient = WebClient.builder()
            .baseUrl("https://jsonplaceholder.typicode.com")
            .build();
        
        HttpServiceProxyFactory factory = HttpServiceProxyFactory
            .builder(WebClientAdapter.create(webClient))
            .build();
        
        return factory.createClient(PostClient.class);
    }
}

Note: For reactive clients, the methods on your interface would return Mono or Flux to handle the asynchronous stream of data.

Step 4: Use the Client

Now that your PostClient bean is configured, you can inject it into any other Spring component (like a service or controller) and use it just like any other Java object.

PostService.java

import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class PostService {

    private final PostClient postClient;

    public PostService(PostClient postClient) {
        this.postClient = postClient;
    }

    public List<Post> getAllPosts() {
        return postClient.getPosts();
    }

    public Post getPost(Integer id) {
        return postClient.getPostById(id);
    }

    public Post createNewPost(Post newPost) {
        return postClient.createPost(newPost);
    }
}

This service can now be used in a REST controller or any other component to interact with the remote API in a clean, type-safe manner, without any of the low-level HTTP client code.