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) orWebClient
(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 ourPostClient
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.