Deklaratywny HttpClient w Spring 7: alternatywa dla FeignClient
Spring 7 wprowadza natywny deklaratywny HttpClient oparty na RestClient i WebClient. Wykorzystuje znane adnotacje @GetExchange, @PostExchange, podobne do @GetMapping w kontrolerach. Dzięki temu można opisywać wychodzące żądania HTTP za pomocą interfejsów bez ręcznej implementacji.
Kluczowy interfejs wygląda następująco:
public interface UserClient {
@GetExchange("/api/users/{id}")
UserResponse getUser(
@PathVariable("id") Long id,
@RequestParam(name = "includeDetails", defaultValue = "false") boolean includeDetails,
@RequestHeader("Authorization") String authToken,
@RequestHeader("X-Request-Id") String requestId
);
@PostExchange("/api/users")
UserResponse createUser(
@RequestBody @Valid CreateUserRequest request,
@RequestHeader("Authorization") String authToken
);
@PatchExchange("/api/users/{id}")
void updateUserEmail(
@PathVariable("id") Long id,
@RequestParam("email") String email,
@RequestHeader("Authorization") String authToken
);
}
HttpClient integruje się z Observability od razu po wyjęciu z pudełka i nie wymaga Spring Cloud, w przeciwieństwie do FeignClient, który od 2022 roku jest w trybie maintenance.
Integracja z OpenAPI Generator
Do automatyzacji generowania klientów używa się openapi-generator z biblioteką spring-http-interface.
Konfiguracja wtyczki Maven:
<plugin>
<groupId>org.openapitools</groupId>
<artifactId>openapi-generator-maven-plugin</artifactId>
<version>${openapi-generator.version}</version>
<executions>
<execution>
<id>generate-weather-client</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<inputSpec>${project.basedir}/src/main/resources/openapi/external/weather-api.yaml</inputSpec>
<generatorName>spring</generatorName>
<library>spring-http-interface</library>
<output>${project.build.directory}/generated-sources/openapi-client</output>
<apiPackage>ulllie.exchange.openapi.gen.client.api</apiPackage>
<modelPackage>ulllie.exchange.openapi.gen.client.model</modelPackage>
<generateApis>true</generateApis>
<generateModels>true</generateModels>
<generateSupportingFiles>false</generateSupportingFiles>
<configOptions>
<useSpringBoot4>true</useSpringBoot4>
<useJackson3>true</useJackson3>
<interfaceOnly>true</interfaceOnly>
<skipDefaultInterface>true</skipDefaultInterface>
<useBeanValidation>true</useBeanValidation>
<annotationLibrary>none</annotationLibrary>
<serializationLibrary>jackson</serializationLibrary>
<useTags>true</useTags>
</configOptions>
</configuration>
</execution>
</executions>
</plugin>
Wygenerowany interfejs:
public interface OpenMeteoApi {
@HttpExchange(method = "GET", value = "/v1/forecast")
ResponseEntity<ForecastResponse> getForecast(
@RequestParam("latitude") Double latitude,
@RequestParam("longitude") Double longitude
);
}
Podłączanie i konfiguracja serwisów
Konfiguracja poprzez import:
@Configuration @ImportHttpServices(group = "weather", types = OpenMeteoApi.class)
public class WeatherRestClientConfig implements RestClientHttpServiceGroupConfigurer {
@Override
public void configureGroups(Groups<RestClient.Builder> groups) {
groups.filterByName("weather").forEachClient(
($, builder) -> builder.baseUrl("https://api.weather.com")
);
}
}
Obsługa błędów konfigurowana przez defaultStatusHandler:
builder.baseUrl(properties.baseUrl())
.defaultStatusHandler(
status -> status.is4xxClientError() || status.is5xxServerError(),
errorHandler
);
Implementacja HttpErrorHandler pozwala na dostosowanie logiki dla statusów 4xx/5xx, integrację z Resilience4j lub @ControllerAdvice.
Możliwości:
- Konfiguracja interceptorów
- Integracja z Resilience4j
- Obsługa błędów na poziomie ResponseSpec
RestClient vs WebClient z Project Loom
Z virtual threads (spring.threads.virtual.enabled=true) blokujący RestClient zapewnia skalowalność operacji I/O bez reaktywnego stylu.
Zalety:
- Imperatywny kod, prosty w debugowaniu
- Brak reaktywnych pipeline'ów
- Porównywalna wydajność z WebClient dla zadań związanych z I/O
Dla scenariuszy reaktywnych generowane są sygnatury Mono/Flux przy wyborze WebClient.Builder.
Problemy walidacji i rozwiązania
Bean Validation nie przechodzi w głąb zagnieżdżonych obiektów, ponieważ generator nie dodaje @Valid.
Opcje rozwiązania:
- x-field-extra-annotation w OpenAPI:
@jakarta.validation.Valid— wymaga zmiany specyfikacji. - AlwaysTraversableResolver — globalnie przechodzi przez drzewo, ryzyko pętli i spadku wydajności.
- Niestandardowe szablony mustache — automatyczne dodawanie @Valid podczas generowania.
Dodatkowo: generowanie data class w Kotlinie dla null-safety.
data class ForecastResponse(
val temperature: Double,
val windSpeed: Double?
)
Zależności: kotlin-stdlib + jackson-module-kotlin. Fail-fast podczas deserializacji przy naruszeniu nullability.
Co najważniejsze
- HttpClient to natywny Spring Framework, bez zależności od Spring Cloud.
- OpenAPI generator z
spring-http-interfacegeneruje gotowe interfejsy. - Virtual threads ułatwiają operacje I/O bez reaktywności.
- Data class w Kotlinie zapewnia wczesną kontrolę null.
- Konfiguracja błędów i walidacji jest w pełni dostosowywalna.
— Editorial Team
Brak komentarzy.