API First - это принцип разработки, по которому API вашего приложения является наиболее важной частью вашего приложения. Этот принцип применяется как для клиентской разработки, так и для серверной. Ранее мы уже говорили о том, как можно реализовать принципы API First для приложений, использующих http интерфейсы (API-First и микросервисы). В этот раз мы поговорим о том, как можно реализовать принципы API First, если ваши приложения используют брокеры сообщений для общения друг с другом (такие как Kafka, RabbitMq и другие). Ниже приведена типичная ошибка при рассогласовании протоколов взаимодействия.

Проблемы документирования интерфейсов при работе с брокерами сообщений
Если для документирования http интерфейсов существует спецификация OpenAPI, то для асинхронного сообщения с использованием брокеров сообщений мы не нашли подобного инструмента. На проектах выбирают, как правило, следующие способы документирования таких интерфейсов:
файлы (doc, pdf, excel);
Wiki.
Формат такого рода документов от проекта к проекту (или внутри одного проекта) разный. Кроме отсутствия единого формата документирования таких интерфейсов есть и другая проблема - рассинхронизация версии документации и версии приложения. На разных стендах может быть разная версия приложения, и сопоставление документации и версии приложения становится нетривиальной задачей.
OpenAPI и брокеры сообщений
Увидев текущие проблемы документирования интерфейсов, использующих брокеры сообщений, мы подумали, как их можно решить. Для решения проблемы отсутствия единого формата документирования решили использовать OpenAPI нотацию. Преимущества OpenAPI:
известная для аналитиков, QA инженеров и разработчиков нотация;
существуют средства чтения и проверки нотации;
существуют средства для создания "моков";
существуют решения для генерации кода по спецификации.
Как мы знаем, OpenAPI - это нотация для описания http интерфейсов. Соответственно, вся инфраструктура вокруг OpenAPI спецификации построена для http. Для использования OpenAPI инфраструктуры необходимы доработки.
API First и брокеры сообщений - обзор существующих решений
Рассмотрим текущие решения, которые уже существуют для реализации подхода API first с интерфейсами, использующие брокеры сообщений. Их достоинства и недостатки рассмотрены с точки зрения наших потребностей.

Springwolf
URL: https://github.com/springwolf/springwolf-core
Позволяет создать спецификацию в формате AsyncAPI - формат для описания асинхронного сообщения.
При подключении библиотеки к своему проекту возникли некоторые проблемы (так и не получилось это сделать), хотя по документации подключение библиотеки Springwolf достаточно простое. За несколько попыток подключения нашлось много неточностей в документации. Пример из репозитория запустился. В примере есть API, которые принимают Headers, но я не нашла отражение этого в сгенерированной документации. Также, headers в сгенерированном UI никак не отражаются, и они не требуются при формировании message (можно опубликовать message без headers).
Достоинства:
удобный UI;
поддержка SASL авторизации к Kafka;
простота подключения (по документации).
Недостатки:
сгенерированный библиотекой UI невозможно объединить с Swagger UI;
небольшое сообщество пользователей;
совместима только с версией java не ниже 17;
не подходящая нам работа с headers;
ошибки в документации.
AsyncAPI Generator
URL: https://www.asyncapi.com/tools/generator
AsyncApi - протокол для описания асинхронного общения между приложениями. Протокол позволяет проектировать приложения в Event-Driven Architecture. У OpenAPI и у AsyncAPI есть много общего (например, описание моделей). На данный момент для работы c AsyncAPI (как и c OpenAPI) существует:
online редактор https://studio.asyncapi.com/;
offline клиент https://www.asyncapi.com/tools/cli;
генератор кода https://www.asyncapi.com/tools/generator;
генератор моделей https://www.asyncapi.com/tools/modelina (онлайн версия https://modelina.org/playground?language=java).
При использовании генератора кода не удалось сгенерировать только отдельную часть приложения - приложение генерируется целиком. Протокол AsyncAPI действительно очень похож на своего родителя OpenAPI (как это и заявлено в документации). Генератор моделей Modelina может принимать спецификации в формате как AsyncAPI, так и OpenAPI, но он нас не устроил. Modelina не добавляет правила валидации в сгенерированный код, даже если правила валидации указаны в спецификации. Инфраструктура вокруг протокола AsyncAPI сейчас активно развивается, и в ней происходит много изменений, но все продукты вокруг AsyncAPI написаны не на Java (кроме Springwolf), что усложняет их “подгонку” под наши нужды Java командами.
Достоинства:
наличие богатой инфраструктуры;
поддержка AsyncAPI - специального протокола для описания взаимодействия приложений в Event-Driven Architecture.
Недостатки:
AsyncAPI - слишком молодой протокол, чтобы использовать его в production, он появился только в 2019-2020 году;
небольшое сообщество пользователей;
генерацию кода приходится делать с помощью другого приложения (нет maven или gradle плагина);
решение написано не на Java - сложность поддержки генератора для Java команд;
игнорирование правил валидации моделей при генерации кода;
нет возможности сгенерировать отдельно код клиента для сервиса, использующий Kafka в качестве средства взаимодействия с ним - можно сгенерировать либо только модели, либо приложение целиком (с конфигурациями и подключением).
Оба этих решения нам не подошли, потому что:
не устроило качество генерации моделей - игнорируются правила валидации;
не устроила работа с хедерами;
текущие решения по генерации кода сложно встроить в процесс сборки приложения;
небольшое сообщество пользователей.
Когда мы следовали принципам API First при разработке приложения с http интерфейсом мы сделали следующие шаги (подробнее можно прочитать в статье API-First и микросервисы):
создали и опубликовали в mvn- репозиторий (nexus) спецификацию API как отдельный артефакт;
при сборке приложения, реализующего API, забирали спецификацию и генерировали по ней модели и интерфейсы Spring контроллеров;
при сборке клиента этого приложения забирали спецификацию и генерировали по ней модели и интерфейс Feign client.
После исследования уже имеющихся решений мы начали разрабатывать собственное решение для реализации подхода API First при разработке приложений, использующих брокеры сообщений в качестве средства интеграции. Мы решили дать возможность полностью повторить шаги генерации кода и создания спецификации как и в случае использования http интерфейсов.
AxenAPI - разработка Axenix
URL: https://github.com/AxenAPI
Мы в MVNRepository: https://mvnrepository.com/artifact/pro.axenix-innovation
Итак, у нас появилась цель - воссоздать API First для приложений, использующих брокеры сообщений. Одним из наиболее распространенных решений для асинхронного общения между сервисами является Kafka. Решили начать с реализации этих идей именно для неё.
Для того чтобы начать использовать подход API First необходимо:
Инициировать спецификацию. Этот шаг необходим, если вы хотите начать использовать подход API First в уже разрабатываемом долго продукте.
Внедрить генерацию кода по спецификации в процесс разработки.
Использовать сгенерированный код.
AxenAPI состоит из двух частей:
AxenAPI Gradle plugin - gradle плагин, позволяющий генерировать код по спецификации интерфейса, реализованного с помощью брокера сообщений.
AxenAPI library - библиотека, позволяющая генерировать контроллеры для ваших kafka-listeners. Далее, с помощью уже имеющихся средств (например, https://springdoc.org/) можно сгенерировать спецификацию OpenAPI.
Основным шагом при использования подхода API First является генерация кода по спецификации, поэтому сначала поговорим о плагине, позволяющим генерировать код.
AxenAPI Gradle plugin. Генерация кода
Для того чтобы обеспечить полный цикл API First необходимо генерировать по спецификации код сервера и клиента. Это дает гарантию, что ваше приложение реализует данное в спецификации API. Рассмотрим генерацию кода сервера. За несколько шагов вы можете избавить себя от рутины и сгенерировать код моделей и обработчиков событий (handlers), а не писать их самостоятельно.
AxenAPI Gradle plugin. Подключение
Подключите плагин:
plugins { //.... id 'pro.axenix-innovation.axenapi.generator' version '1.0.1' }
Добавьте в проект json со спецификацией. Мы добавили в ресурсы проекта (...\src\main\resources\test.json) следующий json:
Спецификация API (...\src\main\resources\test.json)
{ "openapi": "3.0.1", "info": { "title": "App API", "version": "snapshot" }, "servers": [ { "url": "http://axenapi.demo", "description": "Generated server url" } ], "paths": { "/kafka/example_group/example_topic/ExampleIn": { "post": { "description": "example handler", "operationId": "ExampleHandler", "tags": ["example"], "security": [{ "Internal-Token": [] }], "requestBody": { "description": "Example in", "content": { "*/*": { "schema": { "$ref": "#/components/schemas/ExampleIn" } } } }, "responses": { "200": { "description": "Example out", "content": { "*/*": { "schema": { "$ref": "#/components/schemas/ExampleOut" } } } } } } } }, "components": { "schemas": { "ExampleIn": { "type": "object", "properties": { "message": { "type": "string", "description": "example field" } }, "description": "example message" }, "ExampleOut": { "type": "object", "properties": { "message": { "type": "string", "description": "example field" } }, "description": "example message" } }, "securitySchemes": { "Internal-Token": { "type": "apiKey", "name": "SERVICE_ACCESS_TOKEN", "in": "header" } } } }
Добавьте настройки для плагина в build.gradle:
codegenData { openApiPath = getProjectDir().getAbsolutePath() + '/src/main/resources/test.json' outDir = getProjectDir().getAbsolutePath() + '/build' srcDir = 'src/main/java' listenerPackage = 'axenapi.listener' modelPackage = 'axenapi.model' kafkaClient = false }
Добавьте зависимость шага компиляции от шага генерации кода. Для этого вставьте в build.gradle следующие строки:
compileJava { dependsOn "generateKafka"}
Соберите ваш проект - выполните gradle build.
Вам сгенерировался код моделей в путь build/src/main/java/axenapi, и теперь вы можете подключить папку build/src в gradle.build как source:
Давайте посмотрим на результат:

ExampleTopicExampleGroupListener - сгенерированный интерфейс
package axenapi.generated; import org.axenix.axenapi.annotation.KafkaHandlerDescription; import org.axenix.axenapi.annotation.KafkaHandlerTags; import org.axenix.axenapi.annotation.KafkaSecured; import org.springframework.kafka.annotation.KafkaHandler; import org.springframework.kafka.annotation.KafkaListener; import org.axenix.axenapi.annotation.KafkaHandlerHeader; import org.axenix.axenapi.annotation.KafkaHandlerHeaders; import axenapi.generated.model.ExampleIn; import axenapi.generated.model.ExampleOut; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.handler.annotation.Headers; import java.util.Map; //@KafkaListener(topics = "example_topic", groupId = "example_group") public interface ExampleTopicExampleGroupListener { @KafkaHandler @KafkaHandlerDescription("example handler") @KafkaHandlerTags(tags = { "example" }) @KafkaHandlerHeaders(headers = { }) ExampleOut handleExampleIn(@Payload ExampleIn examplein, @Headers Map<String, String> headers); }
AxenAPI Gradle plugin. Параметры плагина
Сначала опишем подробнее параметры из приведенного выше примера.
codegenData { openApiPath = getProjectDir().getAbsolutePath() + '/src/main/resources/test.json' outDir = getProjectDir().getAbsolutePath() + '/build' srcDir = 'src/main/java' listenerPackage = 'axenapi.listener' modelPackage = 'axenapi.model' kafkaClient = false }
openApiPath - путь, по которому лежит ваш json со спецификацией;
outDir - каталог, в который будет помещен сгенерированный код;
srcDir - структура каталогов для сгенерированного кода;
listenerPackage - package, в который будут сгенерированы listeners интерфейсы;
modelPackage - package, в который будут сгенерированы модели (Data Transfer Object);
kafkaClient - если false, то будет сгенерирован код сервера, а если true - то код клиента.
Все настройки плагина (* - обязательные)
Наименование | Тип | Значение по умолчанию | Описание |
|---|---|---|---|
openApiPath * | String | Нет значения по умолчанию | Путь к спецификации в формате OpenAPI 3.* |
outDir * | String | Нет значения по умолчанию | Каталог, куда будет сложен сгенерированный код |
srcDir * | String | Нет значения по умолчанию | Путь к src каталогу. Рекомендуемое значение |
listenerPackage * | String | Нет значения по умолчанию | package, в который попадут сгеренированные client/listeners |
modelPackage * | String | Нет значения по умолчанию | package, к который попадут сгеренированные модели (Data Transfer Object) |
useSpring3 | Boolean |
| Если true, то генерация будет происзодить для springboot 3.1. Если false, то для spring boot 2.7 |
kafkaClient | Boolean |
| Если true, будет генерироваться клиент (producer сообщений), false - интерфейсы сервера (consumer) |
interfaceOnly | Boolean |
| Влияет только на генерацию клинета. Если true - то будут сгенерированы классы реализации отправки сообщений в kafka. Если false - только интерфейсы. |
resultWrapper | String |
| Класс, в который будет обернуто возвращаемое значение. Необходимо описать полный путь к классу. |
securityAnnotation | String |
| Класс аннотации, который выставляется при генерации сервера при использовании в consumer авторизации. Если ничего не указано, то security-аннотации не будут ставится. |
sendBytes | Boolean |
| Если стоит |
useAutoconfig | Boolean |
| Если |
generateMessageId | Boolean |
| Если |
generateCorrelationId | Boolean |
| Если |
messageIdName | String | "kafka_messageId" | Наименование header, в который положится значение messageId (если |
correlationIdName | String | "kafka_correlationId" | Наименование header, в который положится значение correlationId (если |
AxenAPI Gradle plugin. Генерация клиента
Рассмотрим второй пример генерации - генерация клиента. Для этого измените значение параметра kafkaClient на true из примера выше. Соберем проект и посмотрим результат генерации:

Мы видим сгенерированный producer и его имплементацию, сервис для отправки сообщений в Kafka и его имплементацию, файлы для автоконфигурации spring-boot приложений. Ниже приведены примеры сгенерированного кода.
Сгенерированный код producer (ExampleTopicExampleGroupProducerImpl)
package axenapi.generated.impl; import service.KafkaSenderService; import org.springframework.stereotype.Component; import axenapi.generated.ExampleTopicExampleGroupProducer; import java.util.Map; import axenapi.generated.model.ExampleIn; import axenapi.generated.model.ExampleOut; @Component public class ExampleTopicExampleGroupProducerImpl implements ExampleTopicExampleGroupProducer { private final KafkaSenderService kafkaSenderService; public ExampleTopicExampleGroupProducerImpl(KafkaSenderService kafkaSenderService) { this.kafkaSenderService = kafkaSenderService; } @Override public void sendExampleIn(ExampleIn examplein, Map<String, String> params) { kafkaSenderService.send("example_topic", examplein, params); } }
Сгенерированный код сервиса для отправки сообщений в Kafka (KafkaSenderServiceImpl)
package service.impl; import org.springframework.kafka.support.KafkaHeaders; import org.springframework.messaging.support.MessageHeaderAccessor; import org.springframework.stereotype.Service; import service.KafkaSenderService; import org.apache.kafka.clients.producer.ProducerRecord; import org.springframework.kafka.core.KafkaTemplate; import java.util.Map; import org.springframework.kafka.support.converter.MessagingMessageConverter; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; import java.nio.charset.StandardCharsets; import java.util.UUID; @Service public class KafkaSenderServiceImpl implements KafkaSenderService { private String messageIdName = "kafka_messageId"; private String correlationIdName = "kafka_correlationId"; private Boolean sendBytes = true; private Boolean generateMessageId = true; private Boolean generateCorrelationId = true; private final KafkaTemplate<String, Object> kafkaTemplate; private final MessagingMessageConverter converter; public KafkaSenderServiceImpl(KafkaTemplate<String, Object> kafkaTemplate, MessagingMessageConverter converter) { this.kafkaTemplate = kafkaTemplate; this.converter = converter; } public KafkaTemplate<String, Object> getKafkaTemplate() { return kafkaTemplate; } public void send(String topicName, Object message, Map<String, String> params) { MessageHeaderAccessor headerAccessor = new MessageHeaderAccessor(); if (generateMessageId) { if (sendBytes) { headerAccessor.setHeader(messageIdName, UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); } else { headerAccessor.setHeader(messageIdName, UUID.randomUUID()); } } if (generateCorrelationId) { if (sendBytes) { headerAccessor.setHeader(correlationIdName, UUID.randomUUID().toString().getBytes(StandardCharsets.UTF_8)); } else { headerAccessor.setHeader(correlationIdName, UUID.randomUUID()); } } if (params != null && params.size() > 0) { for (var entry : params.entrySet()) { if (sendBytes) { headerAccessor.setHeader(entry.getKey(), entry.getValue().getBytes(StandardCharsets.UTF_8)); } else { headerAccessor.setHeader(entry.getKey(), entry.getValue()); } } } Message<Object> msg = MessageBuilder.createMessage(message, headerAccessor.getMessageHeaders()); ProducerRecord producerRecord = converter.fromMessage(msg, topicName); kafkaTemplate.send(producerRecord); } }
Сгенерированный код конфигурации (KafkaProducerConfig, KafkaSenderServiceConfig)
package config; import org.apache.kafka.clients.producer.ProducerConfig; import org.apache.kafka.common.serialization.StringSerializer; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.kafka.core.DefaultKafkaProducerFactory; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.core.ProducerFactory; import org.springframework.kafka.support.serializer.JsonSerializer; import org.springframework.kafka.transaction.KafkaTransactionManager; import java.util.HashMap; import java.util.Map; public class KafkaProducerConfig { @Value("${spring.kafka.bootstrap-servers}") private String kafkaBootstrap; @Bean("producerFactory") @ConditionalOnMissingBean public ProducerFactory<String, Object> producerFactory() { Map<String, Object> configProps = new HashMap<>(); configProps.put( ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, kafkaBootstrap); configProps.put( ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); configProps.put( ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); configProps.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, "true"); configProps.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "prod-1"); DefaultKafkaProducerFactory<String, Object> factory = new DefaultKafkaProducerFactory<>(configProps); //factory.setTransactionIdPrefix("prod"); return factory; } @Bean public KafkaTransactionManager kafkaTransactionManager(ProducerFactory<String, Object> producerFactory) { return new KafkaTransactionManager(producerFactory); } @Bean @ConditionalOnMissingBean public KafkaTemplate<String, Object> kafkaTemplate(ProducerFactory<String, Object> producerFactory) { return new KafkaTemplate<>(producerFactory); } }
package config; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.context.annotation.Bean; import org.springframework.kafka.core.KafkaTemplate; import org.springframework.kafka.support.converter.MessagingMessageConverter; import service.KafkaSenderService; import service.impl.KafkaSenderServiceImpl; import org.springframework.context.annotation.ComponentScan; @ComponentScan("axenapi") public class KafkaSenderServiceConfig { private final KafkaTemplate<String, Object> kafkaTemplate; public KafkaSenderServiceConfig(KafkaTemplate<String, Object> kafkaTemplate) { this.kafkaTemplate = kafkaTemplate; } @Bean @ConditionalOnMissingBean public MessagingMessageConverter converter() { MessagingMessageConverter converter = new MessagingMessageConverter(); converter.setGenerateMessageId(true); converter.setGenerateTimestamp(true); return converter; } @Bean @ConditionalOnMissingBean public KafkaSenderService kafkaSenderService(MessagingMessageConverter converter) { return new KafkaSenderServiceImpl(kafkaTemplate, converter); } }
Мы рассмотрели примеры генерации кода по спецификации - основной шаг при использовании подхода API First. Теперь давайте рассмотрим AxenAPI library. AxenAPI library позволяет решить проблему инициирования спецификации.
AxenAPI library
AxenAPI library - библиотека, позволяющая генерировать контроллеры для ваших kafka-listeners. Далее, с помощью уже имеющихся средств (например, https://springdoc.org/) можно сгенерировать спецификацию OpenAPI. Http запрос будет перенаправлен в соответствующие топики. Библиотека поддерживает стандартные аннотации библиотеки io.swagger.core.v3 (https://github.com/swagger-api/swagger-core). Если в приложение подключен swagger-ui, то ваши listeners будут отражены в swagger-ui (т.к. для них сгенерированы контроллеры). Сгенерированную библиотекой OpenAPI документацию можно импортировать в Postman или другие средства тестирования http.
AxenAPI library. Подключение
Приступим к подключению AxenAPI. Это делается буквально за несколько шагов:
Подключите нужные зависимости. Для этого необходимо прописать следующие зависимости в gradle.build:
// swagger for kafka annotationProcessor "pro.axenix-innovation:axenapi:1.0.1" implementation ("pro.axenix-innovation:axenapi:1.0.1")
Добавьте в зависимости аннотации Swagger (этот шаг можно пропустить, если вы подключите Swagger-UI):
implementation 'io.swagger.core.v3:swagger-annotations:2.2.10'
В application.yml вашего приложения добавьте:
axenapi.kafka.swagger.enabled: true
Соберите проект. Во время сборки проекта сгенерируются контроллеры для ваших listeners.
Добавьте Swager-UI в ваше приложение, чтобы увидеть результат - это самый простой способ воспользоваться и наглядно увидеть ваше API и все используемые в нем модели:
implementation 'org.springdoc:springdoc-openapi-ui:1.6.13'
Запустите приложение.
Затем откройте http://<host>:<port>/swagger-ui/index.html в вашем браузере, и вы увидите Swagger-UI, в котором присутствуют Post методы для отправки сообщений в соответствующий топик и группу (информация о топике и группе есть в url метода).

Разделите спецификацию и Swagger-UI на две части, если у вас в приложении есть и Kafka handlers и http API. Для этого добавьте следующую конфигурацию для Springdoc:
@Configuration public class OpenApiConfiguration { @Bean GroupedOpenApi restApis() { return GroupedOpenApi.builder().group("kafka").pathsToMatch("/**/kafka/**").build(); } @Bean GroupedOpenApi kafkaApis() { return GroupedOpenApi.builder().group("rest").pathsToMatch("/**/users/**").build(); } }
Как уже говорилось, во время сборки проекта будут генерироваться контроллеры для ваших listeners. Самый простой способ наглядно увидеть результат - подключить Swagger-UI. Можно сгенерировать спецификацию с помощью gradle плагинов (например, springdoc-openapi-gradle-plugin).
В application.yml (или application.properties) можно прописать следующие настройки:
axenapi.kafka.swagger.enabled: true/false - сгенерированные контроллеры будут подняты/не подняты при старте приложения (значение по умолчанию - false);
axenapi.headers.sendBytes: true/false - header’ы либо будут присылаться вместе с типами, либо все header’ы будут считаться массивом байт (значение по умолчанию false).
Можно добавить файл axenapi.properties в корень проекта для настройки annotation processor:
package = com.example.demo - работа будет производиться с listeners только из указанного пакета; если не указано, то работа будет идти со всеми listeners из вашего проекта;
kafka.handler.annotaion = ru.axteam.MyHandler - своя аннотация для поиска listeners, которую необходимо указывать, если на вашем проекте используется своя аннотация, а не @KafkaHandler из библиотеки Spring;
use.standart.kafkahandler.annotation = true/false - вместе с аннотацией из property kafka.handler.annotaion будет/не будет использоваться и @KafkaHandler из библиотеки Spring (значение по умолчанию false);
kafka.access.token.header = SERVICE_ACCESS_TOKEN - указывается наименование header, в котором присылаете токен авторизации (значение по умолчанию - SERVICE_ACCESS_TOKEN).
language = rus - язык генерации дополнительной информации. Возможные значения: eng, rus (значение по умолчанию - eng).
Выводы
Использование принципов API First при разработке асинхронных интерфейсов решает сразу несколько проблем:
сервисы вовремя узнают об изменении формата вашего API;
спецификация вашего API находится в актуальном состоянии - без актуальной спецификации вы не сможете начать разработку;
уменьшает время простоя команды - позволяет распараллелить разработку.
Следовать этим принципам можно и без каких-либо технических средств. Но зачем, когда с их помощью можно сократить большое количество рутинной работы - создание обработчиков событий, создание моделей (Data Transfer Object).
Что вам дает подключение AxenAPI библиотеки и использование axenapi-generator-plugin:
вы всегда сможете отдать ссылку на актуальную спецификацию вашего API или артефакт, содержащий спецификацию вашего API;
спецификация вашего API становится частью артефакта - вы гарантируете то, что ваше приложение реализует API, описанный в спецификации;
появляется версионирование спецификации вашего API.
Как поучаствовать в тестировании и разработке AxenAPI
AxenAPI полностью открыт. Вы можете его найти по ссылке: https://github.com/AxenAPI
По ссылке вы найдете 4 репозитория: библиотека (axenapi-library), плагин (axenapi-gradle-plugin), генератор(axenapi-generator), примеры(axenapi-demo).
Будем благодарны за обратную связь и помощь в развитии продукта.
