За последние несколько лет для вызова внешних API в каждом втором (если не первом) проекте я видел одну и ту же картину:
RestTemplateили
FeignClient
Причём Feign почти всегда шёл в связке с OpenAPI: сгенерировали клиент, получили интерфейсы и не думаем о реализации. Удобно, красиво, привычно.
Но потом в Spring появился нативный декларативный HttpClient, который работает поверх RestClient / WebClient
И у меня возник вопрос: а можно ли им заменить Feign, не потеряв удобство?
Спойлер: да, можно и будет даже удобнее.
Откуда вообще взялся HttpClient
Идея, на самом деле, очень простая.
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 ); }
В Spring уже есть:
@RestController– принимаем HTTP запросыаннотации вроде
@GetMapping,@RequestParam,@PathVariable
Так почему бы не использовать тот же подход, но для исходящих запросов? Так появился HttpClient. Те же аннотации, тот же стиль – только теперь это клиент.
Если вы раньше работали с Feign – здесь может возникнуть логичный вопрос: разве это не то же самое? По ощущениям – очень похоже:
интерфейс
аннотации
декларативный вызов
Но есть важное отличие: Feign – это часть Spring Cloud и отдельная экосистема со своей инфраструктурой.
А HttpExchange – это нативный механизм Spring Framework, который работает поверх стандартного HTTP-клиента (RestClient или WebClient) и не требует дополнительного стека. Плюс более лёгкая настройка и интеграция с Observability из коробки.
То есть внешне они выглядят почти одинаково, но HttpExchange – это "тот же подход", только встроенный прямо в Spring.
И вот в Spring 7 это уже полноценный инструмент, на который явно делают ставку.
А что с Feign?
Начиная с 2022 года, FeignClient официально перешёл в стадию поддержки. То есть он никуда не исчез, но активного развития уже нет.
И логично, что я начал смотреть в сторону нового HttpClient как замены.
Что насчёт интеграции с openapi-generator
Feign хорош не только декларативностью.
Один из его главных плюсов – работа в паре с openapi-generator:
берём
openapi.yamlпрогоняем через
openapi-generatorполучаем готовый клиент
И просто используем готовый бин:
@Component @RequiredArgsConstructor public WeatherClient { // Декларативный сгенерированный интерфейс от feign private final WeatherApi api; public Forecast getWeather(...) { var weather = api.getWeather(...); // ... } }
И для HttpExchange эта опция тоже доступна.
Как настроить openapi-generator и новый клиент
Ключевая вещь – это опция в генераторе:
<library>spring-http-interface</library>
Полная конфигурация:
<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>
На выходе получаем:
public interface OpenMeteoApi { @HttpExchange(method = "GET", value = "/v1/forecast") ResponseEntity<ForecastResponse> getForecast( @RequestParam("latitude") Double latitude, @RequestParam("longitude") Double longitude ); }
То есть это тот же подход, но на нативном Spring API.
Как это подключается
// Такой подход через имплементацию сразу добавляет Observability внутрь клиента @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") ); } }
Настройка обработки ошибок
builder.baseUrl(properties.baseUrl()) .defaultStatusHandler( status -> status.is4xxClientError() || status.is5xxServerError(), //errorHandler реализует RestClient.ResponseSpec.ErrorHandler errorHandler );
public class HttpErrorHandler implements RestClient.ResponseSpec.ErrorHandler //... @Override public void handle(HttpRequest request, ClientHttpResponse response)
То есть мы можем обрабатывать 4хх и 5хх статусы сразу на месте, по любой логике, которая нам нужна. Либо можем обернуть это в нашу ошибку, например, ApiRequestException, и затем настроить @ControllerAdvice – всё зависит от вашего воображения.
Также в новом клиенте легче настраивается работа с Resilience4j, интерцепторами.
А как же WebClient?
Да, можно использовать и его.
Можно даже генерировать реактивные сигнатуры, нужно всего лишь добавить в генератор соответствующий флажок и настроить в конфиге не RestClient builder, а WebClient.Builder
Mono<ForecastResponse> // или Flux
Но тут появляется интересный момент - Project Loom и его работа из коробки в Spring.
spring.threads.virtual.enabled=true
И внезапно:
блокирующий код перестаёт быть проблемой
RestClientстановится «достаточно хорошим»код остаётся простым, те пишем код как раньше в блокирующем стиле, никакой реактивщины
Остаётся дождаться Structured Concurrency и тогда конкурентный код с помощью VT станет намного user-friendly.
Лично моё мнение:WebClient приносит с собой реактивный стиль, который не всегда просто читать и дебажить.
В сценариях без реактивных пайплайнов RestClient + virtual threads даёт схожую масштабируемость для I/O, но с более понятным императивным кодом.
Самая неожиданная проблема - валидация
Сгенерированные Response выглядят примерно так:
public class ForecastResponse { // ... private CurrentWeather currentWeather; // без @Valid }
И тут важный момент: Bean Validation не идёт вглубь, так как генератор не ставит аннотации@Valid на вложенных сущностях. То есть валидируется только верхний уровень.
Что с этим делать?
Я нашёл три варианта:
1. Аннотации через OpenAPI
x-field-extra-annotation: "@jakarta.validation.Valid"
Работает, но требует менять спецификацию. А хотелось бы бездумно копировать спеку.
2. TraversableResolver
Можно заставить валидатор всегда обходить всё дерево.
public class AlwaysTraversableResolver implements TraversableResolver
Но:
риск циклов
возможный удар по производительности
влияет глобально
Звучит как «можно, но лучше не надо».
3. Кастомные шаблоны генерации
Самый адекватный вариант:
переопределяем mustache-шаблоны
добавляем
@Validавтоматически
В этот момент я задался ещё одним вопросом
Как же правильно валидировать контракт быстро? И как сделать так, чтобы мы сразу падали, если внешний API промазал мимо контракта? Те есть в поле notNull пришло null.
В такие моменты кажется, что лучший способ валидировать REST контракт – это не использовать REST и перейти на gRPC

Но gRPC не панацея.
Я подумал:
а что, если базовую валидацию делать во время SerDe?
И тут появляется Kotlin.
data class ForecastResponse( val temperature: Double, val windSpeed: Double? )
Идея такая: чтобы вынести логику генерации HttpClient в отдельный Spring starter. То есть этот репозиторий будет хранить openapi спеки, и openapi-generator будет генерировать Kotlin классы.
Kotlin тут нужен как первая станция защиты – проверка на null safety. По идее, конечный докер образ не должен сильно распухнуть, так как нужно в стартер добавить две зависимости (помимо HttpClient и openapi-generator):
org.jetbrains.kotlin:kotlin-stdlibcom.fasterxml.jackson.module:jackson-module-kotlinМного они не весят – примерно 5MB.
Если API внезапно вернёт null в non-null поле – мы упадём сразу на десериализации.
И это удобно:
ошибка максимально ранняя
никаких неожиданных NPE дальше
При этом важно понимать, что такое поведение зависит от конфигурации Jackson и kotlin-module, а также от того, как именно сгенерированы модели.
То есть это не абсолютная гарантия, а скорее способ добиться fail-fast поведения для части ошибок контракта, в первую очередь связанных с nullability.
А @Min, @Max и прочее можно оставить как второй слой и проверять конкретно в каждом сервисе, который будет использовать этот стартер.
Финал
Если вы уже переходите на 6 или 7 Spring, можно спокойно использовать новый декларативный HttpClient.
Если у вас пару вызовов внешнего API – проще будет вручную написать новый декларативный интерфейс. Если же внешних API вызовов много, или у вашего сервиса много RestController’ов, и вы хотите поставить client коллегам внутри Java Spring микросервисов - это хорошая возможность воспользоваться openapi-generator в связке с новым HttpClient.
Подводя итог, стек получается такой:
HttpСlient → декларативный клиент + возможность работы в синхронном стиле через
RestClientлибо же реактивно через ProjectReactor -WebClientOpenAPI → генерация интерфейсов и классов
Bean Validation → глубоко настраеваемая валидация
и возможно рассмотреть вариант генерации Kotlin классов как защита от null (fail fast).
P.S. Я не выдаю свои решения за чистую монету – это лишь сказ о том, как я перешёл на новый HttpClient и к каким пришёл умозаключениям. Может быть, идея с Kotlin не стоит свеч, но мне кажется, что можно хотя бы попробовать это реализовать.
Если у кого-то есть свои идеи как можно сделать лучше/красивее/стильнее – приглашаю обсудить их в комментариях.

