Migrating from Feign to @HttpExchange in Spring Boot 4: Why a Full Rewrite Isn't Worth It
The author shares practical experience replacing OpenFeign with Spring's @HttpExchange during a Spring Boot 4 upgrade, demonstrating configuration simplification, authentication handling via RestClient interceptors, error mapping with defaultStatusHandler, and the decision to coexist both clients rather than force a full migration.
While upgrading a legacy project to Spring Boot 4, the author evaluated replacing OpenFeign with Spring Framework's built-in HTTP Interface ( @HttpExchange). The project had multiple Feign clients (Inventory, Member, Coupon, Logistics, Payment) each with custom configurations: RequestInterceptor for authentication headers, per-service timeouts in YAML, and ErrorDecoder implementations for mapping HTTP status codes to domain exceptions.
Initial Migration: Logistics Client
The author started with a simple Logistics client. The Feign version used
@FeignClient(name="logistics-service", url="${service.logistics.url}")with @GetMapping. The @HttpExchange version reduced boilerplate:
@HttpExchange
public interface LogisticsClient {
@GetExchange("/api/logistics/orders/{orderNo}")
LogisticsDTO query(@PathVariable String orderNo);
}However, manually creating RestClient, RestClientAdapter, and HttpServiceProxyFactory beans for each client would just swap Feign configuration for another set of bean definitions.
Spring Boot 4 Group Registration
Spring Boot 4 introduces @ImportHttpServices for grouped registration:
@Configuration
@ImportHttpServices(group = "logistics", types = LogisticsClient.class)
@ImportHttpServices(group = "inventory", types = InventoryClient.class)
public class HttpClientConfig {}Base URLs and timeouts move to application.yml under spring.http.serviceclient:
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: 2sThis keeps interfaces focused on HTTP contracts while infrastructure handles environment-specific details.
Authentication: Interceptor per Group
Internal services require propagating the user's JWT token and an application identifier. Adding @RequestHeader to every method would pollute business code. Instead, a RestClientHttpServiceGroupConfigurer bean applies a request interceptor selectively:
@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);
});
}Third-party clients (e.g., external logistics providers) are excluded from this interceptor to avoid leaking internal tokens.
Error Handling: Status Handlers per Client
Feign's ErrorDecoder mapped 404 → InventoryNotFoundException, 409 → StockConflictException, else InventoryServiceException. With RestClient, the author uses defaultStatusHandler on a per-group basis:
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 services continue to catch typed exceptions without change.
Coexistence Over Full Replacement
The author concludes that @HttpExchange simplifies interface definitions but does not eliminate the need for authentication, timeouts, error translation, retries, connection pooling, tracing, circuit breaking, or service discovery. For stable, complex Feign integrations (Nacos discovery, Spring Cloud LoadBalancer, custom encoders/decoders, circuit breakers), the risk of rewriting outweighs the benefit of swapping @GetMapping for @GetExchange.
The system now runs both: new or simple HTTP calls use @HttpExchange; battle-tested Feign clients remain untouched. Migration is deferred until a module is touched for other reasons.
Architectural Cleanup: Gateway Pattern
During migration, the author discovered business logic leaking into Feign interfaces via default methods (e.g., hasStock calling query and comparing availability). These were extracted into a separate 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;
}
}Call chain becomes
OrderService → InventoryGateway → InventoryClient → HTTP, decoupling business logic from the underlying HTTP client implementation.
Performance and Dependency Impact
No measurable latency difference was observed — network-bound calls dominate. 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. The author now defaults to @HttpExchange for new clients and only reaches for Feign when service discovery, advanced load balancing, or existing Spring Cloud ecosystem integration are required.
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.
Java Tech Enthusiast
Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!
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.
