Всем привет! Недавно мы решили задачу, как перейти на новую версию 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.

Варианты решения этой проблемы:

  1. OpenAPI и прикрутить автогенерацию. Каждый клиент генерировать под свой фреймворк, язык и т. д.

  2. Protobuf или другие форматы описания контрактов взаимодействия. Плюсы те же, что у OpenAPI.

  3. Убрать вообще все синхронные вызовы, разделить контексты и сделать все по фэн-шую.

  4. Перенести все DTO и вызовы в места их использования.

  5. Обновить все микросервисы разом.

  6. Попробовать очистить контракт и сделать несколько специализированных версий под разные версии Spring и Feign.

1, 2 и 3 — очень классные подходы, но требуют много времени и высокую квалификацию, чтобы выполнить всё в обозримые сроки и не нарушить работу системы. Мы же хотели улучшить положение дел более скромными ресурсами.

4 — хороший вариант, но мы теряем возможность давать пользователям клиенты из коробки. Требуется вносить изменения на уровне кода в местах использования.

5 — реально, если вся система — это несколько микросервисов. Нам это не подошло.

6 — путь, по которому можно пройти с наименьшими потерями. Его мы и выбрали.

Реструктуризацией мы хотели добиться нескольких вещей:

  1. Предоставлять пользователям настроенные Feign- или OpenFeign-клиенты из коробки.

  2. Подсказать разработчику, что для поддержания нескольких реализаций нужно создать пользователям клиент. 

  3. Постараться отказаться от деталей реализации. Привет дядюшке Бобу.

Итак, приступим:

Добавляем ещё 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 лет. Подать заявку на любую программу можно на сайте или в офисе Банка ДОМ.РФ.