- PVSM.RU - https://www.pvsm.ru -
За последние несколько лет для вызова внешних API в каждом втором (если не первом) проекте я видел одну и ту же картину:
RestTemplate
или FeignClient
Причём Feign почти всегда шёл в связке с OpenAPI: сгенерировали клиент, получили интерфейсы и не думаем о реализации. Удобно, красиво, привычно.
Но потом в Spring появился нативный декларативный HttpClient, который работает поверх RestClient / WebClient
И у меня возник вопрос: а можно ли им заменить Feign, не потеряв удобство?
Спойлер: да, можно и будет даже удобнее.
Идея, на самом деле, очень простая.
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 это уже полноценный инструмент, на который явно делают ставку.
Начиная с 2022 года, FeignClient официально перешёл в стадию поддержки [1]. То есть он никуда не исчез, но активного развития уже нет.
И логично, что я начал смотреть в сторону нового HttpClient как замены.
Feign хорош не только декларативностью.
Один из его главных плюсов – работа в паре с openapi-generator:
берём openapi.yaml
прогоняем через openapi-generator
получаем готовый клиент
И просто используем готовый бин:
@Component
@RequiredArgsConstructor
public WeatherClient {
// Декларативный сгенерированный интерфейс от feign
private final WeatherApi api;
public Forecast getWeather(...) {
var weather = api.getWeather(...);
// ...
}
}
И для HttpExchange эта опция тоже доступна.
Ключевая вещь – это опция в генераторе:
<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, интерцепторами.
Да, можно использовать и его.
Можно даже генерировать реактивные сигнатуры, нужно всего лишь добавить в генератор соответствующий флажок и настроить в конфиге не 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 [2] на вложенных сущностях. То есть валидируется только верхний уровень.
Я нашёл три варианта:
x-field-extra-annotation: "@jakarta.validation.Valid"
Работает, но требует менять спецификацию. А хотелось бы бездумно копировать спеку.
Можно заставить валидатор всегда обходить всё дерево.
public class AlwaysTraversableResolver implements TraversableResolver
Но:
риск циклов
возможный удар по производительности
влияет глобально
Звучит как «можно, но лучше не надо».
Самый адекватный вариант:
переопределяем 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-stdlib
com.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 - WebClient
OpenAPI → генерация интерфейсов и классов
Bean Validation → глубоко настраеваемая валидация
и возможно рассмотреть вариант генерации Kotlin классов как защита от null (fail fast).
P.S. Я не выдаю свои решения за чистую монету – это лишь сказ о том, как я перешёл на новый HttpClient и к каким пришёл умозаключениям. Может быть, идея с Kotlin не стоит свеч, но мне кажется, что можно хотя бы попробовать это реализовать.
Если у кого-то есть свои идеи как можно сделать лучше/красивее/стильнее – приглашаю обсудить их в комментариях.
Автор: ulllie21
Источник [3]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/java/449452
Ссылки в тексте:
[1] официально перешёл в стадию поддержки: https://spring.io/blog/2022/12/16/spring-cloud-2022-0-0-codename-kilburn-has-been-released#spring-cloud-openfeign-feature-complete-announcement
[2] @Valid: https://www.pvsm.ru/users/Valid
[3] Источник: https://habr.com/ru/articles/1022466/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1022466
Нажмите здесь для печати.