Как мы перевели API-модули микросервисного проекта с Feign на OpenFeign
Всем привет! Недавно мы решили задачу, как перейти на новую версию 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 лет. Подать заявку на любую программу можно на сайте или в офисе Банка ДОМ.РФ.