Mastering @HttpExchange in Spring Boot 3: Real‑World Cases & Advanced Tips
This article compares Feign and Spring 6's @HttpExchange for remote service calls, walks through practical examples—including interface definition, proxy creation, various client options, testing, reactive returns, custom argument resolvers, and error handling—while providing complete code snippets for Spring Boot 3.4.2.
1. Introduction
In micro‑service architectures, remote service invocation is essential. Feign and @HttpExchange both enable declarative HTTP calls but differ significantly.
Feign is an open‑source, declarative HTTP client from Netflix, integrated into the Spring Cloud ecosystem. It offers load balancing, circuit breaking, and service discovery, but requires additional third‑party dependencies and configuration.
@HttpExchange is introduced in Spring Framework 6 (used by Spring Boot 3). It defines HTTP services via declarative interfaces without external dependencies, integrates seamlessly with native Spring components such as WebClient , and yields cleaner, higher‑performance code.
This article details how to use @HttpExchange in practice.
2. Practical Cases
2.1 Interface Definition
<code>@HttpExchange("/api")
public interface RemoteService {
@GetExchange("/{id}")
public ResponseEntity<User> query(@PathVariable Long id);
@PostExchange("")
public ResponseEntity<String> create(@RequestBody User user,
@RequestHeader(required = false, name = "Authorization") String token);
}
</code>The annotations work similarly to @RequestMapping or @GetMapping , but the interface‑level @HttpExchange applies a common base path to all methods.
2.2 Creating the Proxy
<code>@Configuration
public class RemoteConfig {
@Bean
RemoteService remoteService() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8888")
.build();
WebClientAdapter adapter = WebClientAdapter.create(webClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
return factory.createClient(RemoteService.class);
}
}
</code>The WebClient is used as the underlying HTTP client. Add the following Maven dependency:
<code><dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
</code>2.3 Other Clients
RestTemplate client
<code>RestTemplate restTemplate = new RestTemplate();
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory("http://localhost:8888"));
RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
RemoteService service = factory.createClient(RemoteService.class);
</code>RestClient client
<code>RestClient restClient = RestClient.builder()
.baseUrl("http://localhost:8888")
.build();
RestClientAdapter adapter = RestClientAdapter.create(restClient);
HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build();
RemoteService service = factory.createClient(RemoteService.class);
</code>2.4 Test Controller
<code>@RestController
@RequestMapping("/remote")
public class RemoteController {
private final RemoteService remoteService;
public RemoteController(RemoteService remoteService) {
this.remoteService = remoteService;
}
@GetMapping("/{id}")
public ResponseEntity<User> query(@PathVariable Long id) {
return this.remoteService.query(id);
}
}
</code>When invoked, the controller returns the response from the remote service.
2.5 Advanced Features
Reactive Return Types
You can declare methods returning Mono or Flux :
<code>@GetExchange("/{id}")
public Mono<User> query(@PathVariable Long id);
</code>Custom Argument Resolver
Define a record to hold query parameters:
<code>public record Search(String name, Integer age, String email, String address) {}
</code>Interface method using the record:
<code>@GetExchange("/search")
public ResponseEntity<String> search(Search search);
</code>Resolver implementation:
<code>public class SearchQueryArgumentResolver implements HttpServiceArgumentResolver {
@Override
public boolean resolve(Object argument, MethodParameter parameter, HttpRequestValues.Builder requestValues) {
if (parameter.getParameterType() == Search.class) {
Search search = (Search) argument;
requestValues.addRequestParameter("name", search.name());
requestValues.addRequestParameter("age", String.valueOf(search.age()));
requestValues.addRequestParameter("email", search.email());
requestValues.addRequestParameter("address", search.address());
return true;
}
return false;
}
}
</code>The generated request URL will include all fields as query parameters, e.g., http://localhost:8888/api/search?name=pack&age=22&[email protected]&address=xxxooo .
Exception Handling (WebClient example)
<code>@Bean
RemoteService remoteService() {
WebClient webClient = WebClient.builder()
.baseUrl("http://localhost:8888")
.defaultStatusHandler(HttpStatusCode::isError, resp -> {
return Mono.just(new RuntimeException("Request error"));
})
.build();
// ... create proxy as before
}
</code>This configures a custom handler that converts HTTP 4xx/5xx responses into a RuntimeException .
All the above demonstrates how to replace Feign with the native @HttpExchange approach, leverage various client adapters, work with reactive types, customize parameter binding, and handle errors in Spring Boot 3.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.