Всем привет! Недавно мы решили задачу, как перейти на новую версию Spring + OpenFeign в мультимодульном проекте, в котором выделен API с навешенными аннотациями @RestController и @FeignClient. Я, Алексей Скакун @hyragano ведущий разработчик ДОМ.РФ, поделюсь с вами этим кейсом.

Рассматриваем вариант организации кода при очищении модуля API в микросервисной архитектуре с использованием Spring Boot + Feign.
Наша цель — организовать переход между различными версиями Spring при использовании Feign-клиентов. Поэтому сосредоточимся именно на моменте отвязки от конкретной реализации Feign или OpenFeign.
Для исключения и минимизации межсервисных запросов микросервисную архитектуру лучше разрабатывать при правильном разделении контекстов. Но, к сожалению, так получается далеко не всегда.
Исходные данные: ~100 микросервисов на Spring Boot 1.x и Spring Cloud 1.x.
Цель: инкрементально перейти на Spring Boot 2.x и Spring Cloud 3.x
Ссылка на репозиторий: github.com/hyragano/msa-with-API
Наша структура многомодульного проекта выглядела так:

order-api — контракт контроллеров или клиентов, DTO-шки и подготовленные Feign-клиенты.
order-service — реализует контракты order-api для обслуживания REST-запросов. По-простому — REST-контроллеры + весь остальной стандартный мир Spring-приложения.
Контракт order-api выглядит так:
package ru.domrf.order.client.tooold; import java.util.UUID; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import ru.domrf.order.API.dto.OrderDTO; import ru.domrf.order.API.dto.RequestOrderDTO; @FeignClient( name = "order-service", path = "/order", url = "${order-service.url}") public interface OrderApi { @RequestMapping(value = "/{id}", method = RequestMethod.GET) @Override OrderDTO findOrderById(@PathVariable("id") UUID id); @RequestMapping(method = RequestMethod.POST) @Override OrderDTO createOrder(@RequestBody RequestOrderDTO orderDTO); }
API-модуль можно использовать в других микросервисах и имплементировать в контроллере. Декларируя API, мы позволяем другим разработчикам использовать наш API практически без приседаний. Пока не приходит время обновить наш фреймворк.
С какой проблемой мы сталкиваемся: в новые сервисы транзитивно попадают зависимости, несовместимые с имеющейся версией Spring. В частности, новые версии Feign-клиента могут не взлететь или подкинуть любимый нами jar-hell.
Варианты решения этой проблемы:
OpenAPI и прикрутить автогенерацию. Каждый клиент генерировать под свой фреймворк, язык и т. д.
Protobuf или другие форматы описания контрактов взаимодействия. Плюсы те же, что у OpenAPI.
Убрать вообще все синхронные вызовы, разделить контексты и сделать все по фэн-шую.
Перенести все DTO и вызовы в места их использования.
Обновить все микросервисы разом.
Попробовать очистить контракт и сделать несколько специализированных версий под разные версии Spring и Feign.
1, 2 и 3 — очень классные подходы, но требуют много времени и высокую квалификацию, чтобы выполнить всё в обозримые сроки и не нарушить работу системы. Мы же хотели улучшить положение дел более скромными ресурсами.
4 — хороший вариант, но мы теряем возможность давать пользователям клиенты из коробки. Требуется вносить изменения на уровне кода в местах использования.
5 — реально, если вся система — это несколько микросервисов. Нам это не подошло.
6 — путь, по которому можно пройти с наименьшими потерями. Его мы и выбрали.
Реструктуризацией мы хотели добиться нескольких вещей:
Предоставлять пользователям настроенные Feign- или OpenFeign-клиенты из коробки.
Подсказать разработчику, что для поддержания нескольких реализаций нужно создать пользователям клиент.
Постараться отказаться от деталей реализации. Привет дядюшке Бобу.
Итак, приступим:
Добавляем ещё 2 модуля уже с конкретными деталями — зависимостями.
Один модуль для клиента с новыми зависимостями, другой — для клиента со старыми.

order-feign-client — модуль со старыми версиями Feign-клиентов.
order-open-feign-client — модуль с новыми версиями Feign-клиентов — OpenFeign.
order-api — только контракты для клиентов и контроллеров + DTO.
В API вычищаем всё от конкретных атрибутов Spring. Теперь у нас контракт без деталей:
package ru.domrf.order.API.API; import java.util.UUID; import ru.domrf.order.API.dto.OrderDTO; import ru.domrf.order.API.dto.RequestOrderDTO; public interface OrderApi { OrderDTO findOrderById(UUID id); OrderDTO createOrder(RequestOrderDTO orderDTO); }
Так выглядят клиенты для Feign:
package ru.domrf.order.client.tooold; import java.util.UUID; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import ru.domrf.order.API.API.OrderApi; import ru.domrf.order.API.dto.OrderDTO; import ru.domrf.order.API.dto.RequestOrderDTO; @FeignClient( name = "order-service", path = "/order", url = "${order-service.url}") public interface OrderClientTooOldFeignVersion extends OrderApi { @RequestMapping(value = "/{id}", method = RequestMethod.GET) @Override OrderDTO findOrderById(@PathVariable("id") UUID id); @RequestMapping(method = RequestMethod.POST) @Override OrderDTO createOrder(@RequestBody RequestOrderDTO orderDTO); }
И для новой версии Feign — OpenFeign:
package ru.domrf.order.client; import java.util.UUID; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import ru.domrf.order.API.API.OrderApi; import ru.domrf.order.API.dto.OrderDTO; import ru.domrf.order.API.dto.RequestOrderDTO; @FeignClient( value = "order-service", path = "/order", url = "${order-service.url}") public interface OrderClient extends OrderApi { @GetMapping("/{id}") @Override OrderDTO findOrderById(@PathVariable("id") UUID id); @PostMapping @Override OrderDTO createOrder(@RequestBody RequestOrderDTO orderDTO); }
Модули клиентов снабжаем автоконфигурируемыми бинами. Подробнее про автоконфигурации можно почитать тут. Теперь в микросервисах с этим API мы можем менять зависимость на соответствующую версии Spring + Feign.
Раньше во всех pom.xml зависимость прописывалась так:
<dependency> <groupId>ru.domrf.order</groupId> <artifactId>order-API</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
Так стало для сервисов, которые мы переводим на новую версию Spring:
<dependency> <groupId>ru.domrf.order</groupId> <artifactId>order-open-feign-client</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
Для всех ещё непереведённых сервисов мы просто меняем на:
<dependency> <groupId>ru.domrf.order</groupId> <artifactId>order-feign-client</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
Этот подход мы стараемся использовать и для других shared артефактов.
В целом shared — зачастую зло и антипаттерн. Но такой подход всё равно встречается. Особенно, если приходится жить с вынужденным наследием предков в коде.
Эти технологии используются у нас в кредитном конвейере для автоматического принятия предварительных решений по кредитным заявкам. В мае в нашем Банке ДОМ.РФ стартовала специальная льготная ипотечная программа для IT-специалистов с потенциально высоким спросом. Поэтому нам было особенно важно заранее улучшить и ускорить процесс. В наших условиях и временных рамках описанный метод оказался самым рабочим.
И пару слов об ИТ-ипотеке, вдруг ты именно сейчас в поисках самого выгодного предложения. Льготная программа рассчитана на сотрудников в возрасте от 22 до 45 лет аккредитованных российских IT-компаний. Минимальная ставка – 4,3%. Предложение распространяется только на первичный рынок жилья и действует до конца 2024 года. Такую ипотеку можно взять на покупку готовой или строящейся квартиры в новостройке, готового дома от застройщика или строительство частного дома. Первоначальный взнос – от 15% от стоимости недвижимости, а максимальный срок кредитования — 30 лет. Подать заявку на любую программу можно на сайте или в офисе Банка ДОМ.РФ.
