Как-то в общении с моим другом-разработчиком из одной крупной софтверной компании у нас зашёл разговор о взаимодействии распределённых команд. В его компании было множество достаточно изолированных команд, каждая из которых разрабатывала свой сервис. В ответ на мой вопрос, как команды расшаривают API, я получил ожидаемый ответ: Open API. Open API, безусловно, прекрасный инструмент, но у него есть ряд недостатков.
Меня зовут Андрей Зяблин, я главный разработчик в «Магните». Расскажу о том, как распространять API нативным для Java способом и пользоваться им в объектно-ориентированном стиле без использования генераторов кода.
Немного теории
Классическая ситуация: одна команда разрабатывает REST API (поставщик), другая его потребляет (потребитель). Чтобы распространять API, и был придуман стандарт Open API, также известный как Swagger. Он представляет собой стандарт описания и документации для RESTful — набора правил, который позволяет разработчикам описывать и документировать функциональность своего API.
OpenAPI-спецификация представлена в формате JSON или YAML и содержит подробное описание методов, параметров, структуры данных и другой информации, необходимой для взаимодействия с API.
OpenAPI — замечательный инструмент. Он позволяет распространять API между разработчиками, пишущими на различных языках и работающих в разных компаниях. Однако такое нужно далеко не всегда. Когда все команды разрабатывают на Java, возникает вопрос: так ли уж необходимо промежуточное звено в виде OpenAPI? Нельзя ли распространять API более нативным для Java способом и пользоваться им в объектно-ориентированном стиле без использования генераторов кода?
Да, существуют современные технологии, которые позволяют это сделать. Ниже я продемонстрирую подход для команд, разрабатывающих REST-сервисы на Java и Spring — это очень распространённый стек.
Как работает подход: рассмотрим на примере
Для демонстрации подхода воспроизведу ситуацию, описанную выше: когда есть поставщик API и его потребитель. Для этого потребуются два приложения:
поставщик API – producer,
потребитель API – consumer.
Producer будет распространять API через maven-репозитарий. Такой подход позволяет оперативно публиковать изменения, а также оперативно потреблять их без генерации кода.
Подход подразумевает классическую структуру API, состоящую из двух основных частей: POJO-модели и интерфейсов, предоставляющих методы работы с этими моделями.
Распространяем модели — producer
Создадим многомодульный проект со следующими настройками для простоты.
allprojects { group = 'com.example.producer' version = '0.0.1' sourceCompatibility = '11' repositories { mavenCentral() mavenLocal() } }
subprojects { apply plugin: 'java' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' } }
Добавим модуль model, который будет содержать модели. Настроим его для публикации в локальный maven-репозитарий с использованием плагина maven-publish.
apply plugin: 'maven-publish' jar { enabled=true archiveClassifier.set("") } bootJar { enabled = false } publishing { publications { maven(MavenPublication) { artifact jar } } repositories { mavenLocal() } }
Сама модель:
package com.example.producer.model; import lombok.Builder; import lombok.Getter; import lombok.Setter; @Getter @Setter @Builder public class Employee { private Long id; private String name; }
Теперь опубликуем модель:
gradle clean build publish
Потребляем модели — consumer
Создадим проект consumer со следующими зависимостями.
ext { set('producerVersion', "0.0.1") }
dependencies { implementation "com.example.producer:model:${producerVersion}" }
В принципе, на этом шаге модели из producer уже можно использовать, причём без всякой генерации кода.
Ну а где же API?
Конечно, распространение моделей — это хорошо, но это вещь достаточно тривиальная и древняя. А где же интерфейсы, где обещанный REST, спросите вы. Сейчас всё будет. Но для начала немного теории про технологии, которые позволяют это сделать.
Spring Cloud OpenFeign
Spring Cloud OpenFeign (далее Feign) — декларативный клиент REST для приложений Spring Boot. Он позволяет создавать веб-сервисы без написания какого-либо кода, кроме создания интерфейса. Этой его особенностью мы и воспользуется, чтобы делиться API с окружающими. Одна из замечательных возможностей Feign — поддержка аннотаций Spring MVC.
Создание REST-сервиса
Для этого воспользуемся ещё одной замечательной возможностью Spring – обнаруживать аннотации контроллеров в интерфейсах.
Модуль Common API
Модуль будет содержать интерфейс, в котором мы определим не только сигнатуры методов, но и типы веб-запросов. По сути этот интерфейс представляет собой REST-API. Почему это вынесено в отдельный модуль, станет понятно позднее.
dependencies { implementation project(":model") }
package com.example.producer.common.api; import com.example.producer.model.Employee; import java.util.List; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; public interface EmployeeApi { String path = "/employee"; @GetMapping("findById/{id}") Employee findById(@PathVariable Long id); @GetMapping("findByName/{name}") Employee findByName(@PathVariable String name); @GetMapping("findAll") List<Employee> findAll(); }
Модуль API
Модуль представляет собой Spring Boot приложение, содержащее реализацию интерфейса EmployeeAPI.
dependencies { implementation project(":model") implementation project(":common-api") }
package com.example.producer.controller; import com.example.producer.common.api.EmployeeApi; import com.example.producer.model.Employee; import java.util.List; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping(EmployeeApi.path) public class EmployeeController implements EmployeeApi { @Override public Employee findById(Long id) { return Employee.builder().id(id).name("test1").build(); } @Override public Employee findByName(String name) { return Employee.builder().id(1L).name(name).build(); } @Override public List<Employee> findAll() { return List.of(Employee.builder().id(1L).name("test3").build()); } }
Распространяем интерфейс
Теперь нужно опубликовать наш интерфейс в maven-репозитарии. Отсюда понятно, почему это вынесено в отдельный модуль. Добавим в Common API следующие настройки:
apply plugin: 'maven-publish' jar { enabled=true archiveClassifier.set("") } bootJar { enabled = false } publishing { publications { maven(MavenPublication) { artifact jar } } repositories { mavenLocal() } }
И заново публикуем проект, увеличив версию.
allprojects { group = 'com.example.producer' version = '0.0.2'
gradle clean build publish
Перенесём приложение на другой порт.
server: port: 8096
Полная структура приложения producer выглядит так:

Потребляем интерфейс — consumer
Сделаем из consumer Spring Boot приложение.
plugins { id 'org.springframework.boot' version '2.6.2' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = '11' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenLocal() mavenCentral() } ext { set('springCloudVersion', "2021.0.0") set('producerVersion', "0.0.2") } dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.cloud:spring-cloud-starter-openfeign' implementation 'io.github.openfeign:feign-jackson:9.3.1' implementation "com.example.producer:model:${producerVersion}" implementation "com.example.producer:common-api:${producerVersion}" compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } dependencyManagement { imports { mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}" } }
package com.example.consumer; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class ConsumerApplication { public static void main(String[] args) { SpringApplication.run(ConsumerApplication.class, args); } }
Далее настроим Feign.
@Configuration @RequiredArgsConstructor public class EmployeeConfig { @Value("${producer.api.url}") private String baseUrl; private final ObjectMapper mapper; @Bean public EmployeeApi employeeApi() { return Feign.builder() .contract(new SpringMvcContract()) .encoder(new JacksonEncoder(mapper)) .decoder(new JacksonDecoder(mapper)) .target(EmployeeApi.class, baseUrl.concat(EmployeeApi .path)); } }
Cоздадим контроллер для демонстрации использования producer API.
@RestController @RequestMapping("employee") @RequiredArgsConstructor public class EmployeeController { private final EmployeeApi employeeApi; @GetMapping("findById/{id}") public Employee findById(@PathVariable Long id) { return employeeApi.findById(id); } @GetMapping("findAll") public List<Employee> findAll() { return employeeApi.findAll(); } }
В коде видна вся магия такого подхода. Мы общаемся с REST-сервис, просто вызывая методы объекта, как в классическом ООП.
Настроим producer API URL.
producer: api: url: http://localhost:8096
Ну и перенесём приложение на другой порт.
server: port: 8097
Проверка результатов
Запускаем оба приложения и наслаждаемся результатом. Ниже приведён результат вызова метода findAll приложения consumer.

А что с документацией?
С документацией проблема решается при помощи старого доброго javadoc. Настроим проект producer для генерации документации. Для удобства настройки плагина публикаций перенесём в отдельный файл в корне проекта — publish.gradle.
apply plugin: 'maven-publish' jar { enabled=true archiveClassifier.set("") } bootJar { enabled = false } task packageJavadoc(type: Jar) { from javadoc classifier = 'javadoc' } publishing { publications { maven(MavenPublication) { artifact jar artifact tasks.packageJavadoc } } repositories { mavenLocal() } }
Здесь добавилась новая задача.
task packageJavadoc(type: Jar) { from javadoc classifier = 'javadoc' }
Теперь publish.gradle можно использовать в публикуемых модулях:
apply from: '../publish.gradle'
Задокументируем интерфейс.
** * It's a great interface */ public interface EmployeeApi {
Поднимем версию и опубликуем модули. Далее настроим проект consumer. Добавим плагин idea.
plugins { id 'org.springframework.boot' version '2.6.2' id 'io.spring.dependency-management' version '1.0.11.RELEASE' id 'java' id 'idea' }
Настроим его.
idea { module { downloadJavadoc = true } }
Сделаем реимпорт проекта. Теперь можем наслаждаться чтением документации.

Безусловно, OpenAPI — очень мощный и удобный инструмент. Однако и описываемый подход имеет свои преимущества:
Нет необходимости кодогенерации: уходит зависимость от кодогенераторов.
API распротраняется в виде библиотек через репозитарии maven, поэтому для их подключения в клиенте не требуется дополнительных плагинов — это просто обычная зависимость maven.
Несколько проще поддерживать версионность по сравнению с Open API.

