Powrót do strony głównej

HttpClient Spring 7: zastąpienie Feign

W Spring 7 przedstawiono deklaratywny HttpClient jako natywną alternatywę FeignClient. Wsparcie OpenAPI generator, virtual threads i Kotlin null-safety ułatwia integrację zewnętrznych API. Szczegółowo o konfiguracji, walidacji i obsłudze błędów.

Spring 7 HttpClient: pełny przewodnik po zastąpieniu Feign
Advertisement 728x90

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.

Google AdInline article slot

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:

Google AdInline article slot
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.

Google AdInline article slot

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-interface generuje 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

Advertisement 728x90

Czytaj dalej