Migrating from Feign to @HttpExchange in Spring Boot 4: Why It's Not That Simple
The author shares their experience replacing OpenFeign with Spring's @HttpExchange during a Spring Boot 4 upgrade, demonstrating how interface definitions simplify but cross-cutting concerns like authentication, timeouts, and error handling remain, leading to a pragmatic coexistence of both tools based on project needs.
Recently upgrading a legacy project to Spring Boot 4, the author decided to refactor remote service calls that had long relied on OpenFeign. Initially Feign provided clean interface-based HTTP clients, but over years each client accumulated configuration: request interceptors for authentication and headers, per-service timeout settings, and custom error decoders to map HTTP status codes to business exceptions. With multiple clients (Inventory, Member, Coupon, Logistics, Payment), this boilerplate grew substantially.
Trying Spring's @HttpExchange
Spring Framework's HTTP Interface (@HttpExchange) had existed but felt like syntactic sugar over RestClient. Spring Boot 4 improved its support, so the author tested it on a simple logistics query client.
Before (Feign)
@FeignClient(name = "logistics-service", url = "${service.logistics.url}")
public interface LogisticsFeignClient {
@GetMapping("/api/logistics/orders/{orderNo}")
LogisticsDTO query(@PathVariable("orderNo") String orderNo);
}After (@HttpExchange)
@HttpExchange
public interface LogisticsClient {
@GetExchange("/api/logistics/orders/{orderNo}")
LogisticsDTO query(@PathVariable String orderNo);
}The interface definition is simpler, but the real difference lies in client registration and configuration.
Group Registration with @ImportHttpServices
Instead of manually building RestClient, Adapter, and ProxyFactory for each client, Spring Boot 4 allows grouping clients by business domain:
@Configuration
@ImportHttpServices(group = "logistics", types = LogisticsClient.class)
@ImportHttpServices(group = "inventory", types = InventoryClient.class)
public class HttpClientConfig {}Base URLs and timeouts move to configuration files:
spring:
http:
serviceclient:
logistics:
base-url: http://logistics-service
connect-timeout: 2s
read-timeout: 3s
inventory:
base-url: http://inventory-service
connect-timeout: 1s
read-timeout: 2sThe client interface now contains only the HTTP contract; environment-specific details are externalized.
Handling Authentication per Client Group
Internal services require propagating the user's JWT token and an application name. Adding @RequestHeader to every method would pollute business code. Instead, a RestClientHttpServiceGroupConfigurer bean configures a request interceptor for selected groups:
@Bean
RestClientHttpServiceGroupConfigurer httpClientConfigurer() {
return groups -> {
groups.filterByName("inventory")
.forEachClient((group, builder) -> configureInternalClient(builder));
groups.filterByName("member")
.forEachClient((group, builder) -> configureInternalClient(builder));
};
}
private void configureInternalClient(RestClient.Builder builder) {
builder.requestInterceptor((request, body, execution) -> {
String token = UserContextHolder.getToken();
if (token != null) {
request.getHeaders().set("Authorization", token);
}
request.getHeaders().set("X-App-Name", "order-service");
return execution.execute(request, body);
});
}This keeps authentication logic out of business services and applies it only to internal service groups, avoiding leaking tokens to third-party APIs.
Exception Handling with RestClient Status Handlers
Feign's ErrorDecoder is replaced by defaultStatusHandler on the RestClient builder, again scoped to the inventory group:
private void configureInventoryClient(RestClient.Builder builder) {
builder.defaultStatusHandler(status -> status.value() == 404,
(request, response) -> { throw new SkuNotFoundException(); });
builder.defaultStatusHandler(status -> status.value() == 409,
(request, response) -> { throw new StockNotEnoughException(); });
builder.defaultStatusHandler(status -> status.is5xxServerError(),
(request, response) -> { throw new InventoryServiceException("库存服务异常: " + response.getStatusCode()); });
}Business code continues to catch typed exceptions:
try {
return inventoryClient.lock(request);
} catch (StockNotEnoughException e) {
throw new OrderCreateException("商品库存不足");
} catch (InventoryServiceException e) {
log.error("库存服务调用失败, skuId={}", request.skuId(), e);
throw e;
}Realization: Cross-Cutting Concerns Don't Disappear
The migration revealed that @HttpExchange only simplifies the interface definition. Authentication, timeouts, exception translation, logging, tracing, retries, connection pooling, circuit breaking, service discovery, and load balancing remain necessary engineering tasks. They don't vanish by changing an annotation.
Pragmatic Coexistence
The author did not replace all Feign clients. Stable, complex integrations relying on Nacos service discovery, Spring Cloud LoadBalancer, CircuitBreaker, and custom Feign encoders/decoders were left untouched. Rewriting proven infrastructure for marginal syntactic gain introduces unjustified risk. New, simple HTTP calls (third-party APIs, straightforward internal services) start with @HttpExchange; existing Feign clients continue running.
Extracting Business Logic into a Gateway Layer
During the refactor, the author discovered business logic leaking into Feign interfaces via default methods (e.g., hasStock calling query and comparing quantities). This logic was moved to a dedicated InventoryGateway service:
@Service
@RequiredArgsConstructor
public class InventoryGateway {
private final InventoryClient client;
public boolean hasStock(Long skuId, int quantity) {
InventoryDTO inventory = client.query(skuId);
return inventory.available() >= quantity;
}
}The call chain becomes
OrderService → InventoryGateway → InventoryClient → HTTP, decoupling business rules from the underlying HTTP client implementation. Future switches (Feign, @HttpExchange, RPC) affect only the client layer.
Performance and Dependency Benefits
No significant performance difference was observed; network latency dominates. The main gains are structural: fewer dependencies (no spring-cloud-starter-openfeign), cleaner separation of concerns, and a lighter starting point for new HTTP integrations.
Conclusion
Feign isn't obsolete; it remains solid for complex, established service meshes. For new Spring Boot 4 projects with ordinary HTTP needs, @HttpExchange is a viable first choice. Tool selection should follow project requirements, not dogma. The author now starts with @HttpExchange and only reaches for Feign when advanced Spring Cloud integration is genuinely needed.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
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.
