Привет, Хабр! Меня зовут Александр Митин. Я Java разработчик в компании ИТ-холдинг Т1 с 15 летним опытом, из которых последние 5 лет работаю в финтехе. Мой любимый стек — Java Spring. Я хочу рассказать такое AsyncAPI, как работать со спецификациями, какие есть инструменты и поделюсь нашим опытом перехода на подход API First в наших системах.

Синхронное взаимодействие
Прежде чем говорить про AsyncAPI, рассмотрим стандартную схему синхронного взаимодействия.

В традиционной модели синхронного взаимодействия есть http-клиент и http-сервер, клиент отправляет запросы, а сервер обрабатывает и возвращает ответ. Всё это можно описать с помощью стандарта OpenAPI (ранее известного как Swagger). Этот стандарт позволяет формализовать API, обеспечивая удобную документацию и упрощая интеграцию.
В отличие от синхронного, асинхронное взаимодействие — сложное. У него отсутствует прямой запрос-ответ. Вместо этого обмен сообщениями происходит через очереди, топики и другие механизмы публикации-подписки.

У нас есть publisher, который отправляет сообщение через месседж-брокер, и кто-то это сообщение слушает. В таких случаях OpenAPI уже не применим, и приходится пользоваться другими способами:
Изучение исходного кода — надёжный, классический подход, проверенный временем. При такой интеграции можно запросить доступ к другой системе, написать пару писем безопасникам с обоснованием, зачем нам это надо, и подождать недельку, пока дадут доступ. И только тогда можно будет посмотреть в исходники и реализовать корректную интеграцию с другой системой.
Поискать в документации, которая в реальной практике часто бывает устаревшей или отсутствует.
Написать в телегу тимлиду другой команды в пятницу вечером, а лучше поставить встречу человек на 20 и получить нужные нам DTO по почте.
К сожалению, все эти способы зачастую приводят к тому, что первая интеграция происходит примерно так.

Чтобы этого избежать, лучше описать API c помощью стандарта — AsyncAPI.
AsyncAPI — версии стандарта
AsyncAPI позволяет формализовать архитектуру событийно-ориентированных систем, где компоненты обмениваются сообщениями через брокеры сообщений, очереди или топики. Этот стандарт помогает не только документировать структуру сообщений и каналы коммуникации, но и автоматизировать многие процессы: генерацию кода, тестирование, мониторинг и визуализацию API.
Одно из ключевых преимуществ AsyncAPI — его независимость от технологии передачи сообщений. Он одинаково хорошо подходит для Kafka, MQTT, RabbitMQ и других систем обмена сообщениями. Благодаря этому архитекторы и разработчики получают универсальный инструмент для создания и поддержки асинхронных сервисов.
Первая версия AsyncAPI была набором инструментов, аналогичных Swagger или OpenAPI. Она включала в себя пользовательский интерфейс (UI), средства генерации кода и сам стандарт, оформленный в виде JSON-схемы. Далее спецификации AsyncAPI активно развивались. В 2023 году была выпущена последняя стабильная версия — AsyncAPI 2.6. В том же году появилась новая версия спецификации 3.0.0, а чуть позже 3.0.1, которая была удалена из-за ошибки при выпуске.
В отличии от стандарта OpenaAPI (который поддерживается и финансируется крупными компаниями, такими как Amazon и Microsoft), AsyncAPI создан и поддерживается небольшим комьюнити энтузиастов.
Пользовательский интерфейс AsyncAPI похож на Swagger UI. Главное отличие — в отсутствии кнопки «Try It Out», которая в Swagger отправляет запрос и позволяет получать мгновенный ответ. В AsyncAPI такой кнопки быть не может, поскольку для отправки сообщений в брокер потребуется бэкенд.

Есть официальный сайт с документацией и описанием всех утилит.
Спецификация AsyncAPI имеет такой же формат описания, как и OpenAPI. Первой строкой в спецификации AsyncAPI указывается версия стандарта, на которой базируется описание. Далее — сама спецификация в формате YAML либо JSON c подробным описанием структуры и взаимодействия асинхронного API.

Если вы хотите разобраться, насколько близки по своей концепции и структуре оба стандарта, в официальной документации AsyncAPI есть целая глава, посвящённая сравнению AsyncAPI и OpenAPI.

Если коротко, то очень похожи по структуре, но различаются по терминологии и моделям. Например, в OpenAPI ключевыми являются сущности Request и Response, тогда как в AsyncAPI основная единица — это Message.
Итак, у нас есть спецификация AsyncAPI, далее возникает вопрос: как её визуализировать и представить пользователям?
UI-инструменты
Для работы с AsyncAPI доступно несколько UI-инструментов, которые упрощают создание и визуализацию спецификаций:
AsyncAPI Studio — это аналог Swagger Editor. В нём можно написать спецификацию и сразу увидеть результат визуализации. Инструмент написан на NodeJS, доступен как в онлайн-версии, так и для локального запуска, что обеспечивает гибкость в использовании.
AsyncAPI Generator + HTML Template — позволяет на основе спецификации сгенерировать статический набор файлов (HTML, CSS, JavaScript). Полученный сайт можно разместить на любом веб-сервере, просто закинув в него статичный набор сгенерированных файлов.
React UI Component — компонент для отображения спецификации на базе React, который можно интегрировать в собственные проекты, обеспечивая единый стиль и интерфейс.
Кроме того, существуют и другие решения (SwaggerHub, Bump.sh, Redocly), расширяющие возможности работы с AsyncAPI. Все это платные решения уровня enterprise, поэтому мы их рассматривать не будем.
Когда мы разобрались со спецификацией AsyncAPI и способами её визуализации, следующий шаг — понять, что можно делать со спецификацией на практике.
AsyncAPI code-first: пишем код и получаем готовую спецификацию
В подходе code-first разработчик пишет исходный код приложения, а спецификация генерируется автоматически на его основе. Для AsyncAPI таких инструментов немного. Например, для Java есть библиотека-стертер Springwolf. Она названа по аналогии с библиотекой Springfox для Swagger 2.0 и построена по тем же принципам.
@Component
public class OrderListener {
@AsyncListener(
operation @AsyncOperation(
channelName = "orders",
description = "Обработка новых заказов"
),
message @AsyncMessage(
payloadType = OrderEvent.class,
description = "Событие создания заказа"
)
)
@KafkaListener(topics = "orders")
public void handle (@Payload OrderEvent event) {
// ...
}
}
В коде можно использовать специальные аннотации, которые при запуске микросервиса автоматически формируют документацию AsyncAPI, доступную по определённой ссылке. Аналогичные инструменты есть и для других языков и стеков.
AsyncAPI API First: сначала документация — затем код
Особенно интересно использовать AsyncAPI в подходе API First, когда сначала создаётся документация, а затем пишется код.
Мы начали применять API-First, поставив перед собой две основные задачи:
Стандартизировать описание API. Убрать описание асинхронных взаимодействий в разрозненных местах — Confluence, таблицах, Excel-файлах, которые рассылались по почте. Оставить единое стандартизированное описание API, которое будет всегда одинаково отображаться в UI.
Использовать полную или частичную спецификацию для генерации кода, насколько это возможно, чтобы сократить ручной труд и снизить вероятность ошибок.
С помощью AsyncAPI возможно описать практически все асинхронные виды взаимодействий: Kafka, WebSockets, JMS, gRPC и прочие. Однако при описании спецификаций у вас могут возникнуть следующие проблемы:
AsyncAPI не поддерживает работу с XSD-схемами напрямую. Если ваше взаимодействие основано на XML и задействованы XSD-схемы, описать их в спецификации AsyncAPI можно лишь через блок externalDocs. В этот блок вы можете добавить ссылку на XSD-схему или стороннюю документацию, например, расположенную в Confluence или другом месте.
Динамические топики. Из коробки, динамическое описание топиков выглядит аналогично описанию path-параметров в Swagger. Это выглядит очень красиво, но при этом генерация кода AsyncAPI работать с таким описанием не сможет. Здесь сможет помочь только ручная доработка сгенерированого кода или свои шаблоны для генерации.
AsyncAPI описывает контракты сообщений и не подходит для сложных workflow, таких как SAGA или CQRS, которые требуют оркестрации и управления состояниями. Это логично, ведь стандарт изначально создавался только для описания контрактов. Однако, ничто не мешает в поле description добавить детальное описание или ссылку на внешнюю документацию, чтобы использовать AsyncAPI как единую точку входа для контрактов и бизнес-логики микросервиса.
Помимо всего перечисленного, когда мы начали описывать AsyncAPI и внедрять его в свои проекты, то столкнулись и с другими проблемами.
Наши «грабли» при описании документации
YAML-файл со спецификацией оказался слишком большим и неудобным для совместной работы, особенно при параллельной разработке нескольких функций. Например, слияние в git приводило к частым конфликтам, что сильно замедляло процесс.
Поэтому мы вынесли спецификацию в отдельный проект с документацией и разбили на множество небольших файлов. Разбивка достаточно условная, то есть она принята только у нас команде. Тем не менее, мы выделили доменную модель (объекты которыми оперирует наша система) и обернули её в сообщения (messages), образовав следующую иерархию:

Другая проблема заключалась в том, что мы хотели видеть все наши спецификации в одном месте. Очень неудобно в системе, с состоящей из множества микросервисов, иметь разные ссылки на спецификации и делиться ими. Чтобы решить эту задачу, мы добавили ещё один уровень иерархии в проекте документации: создали каталоги с названиями микросервисов.

Когда мы описывали API нашей системы, мы поняли, что очень часто оперируем одними и теми же доменными объектами в системе. Эти объекты мы решили вынести в отдельный каталог "common" на самом верхнем уровне.

Эти общие объекты доменной модели мы импортируем через файл common.yaml для того, чтобы скрыть длинный относительный путь до "common-схем" и всегда видеть, какие общие объекты в каких сервисах используются. Это очень удобно при рефакторинга и поддержки системы.
Последнее, что мы захотели сделать — полностью описать API всех наших микросервисов: синхронное API, асинхронное API в одном проекте с документацией. Это оказалось достаточно просто из-за сходства AsyncAPI и OpenAPI. Мы добавили файл openapi.yaml и недостающие компоненты (описание request и response). Всё остальное осталось общим.

Дальше у нас встает вопрос - а как нам отобразить документацию? У нас есть проект с документацией и множество yml-файлов, отдельно Swagger UI и AsyncAPI Studio. Несмотря на то, что платные решения могли бы упростить задачу, мы посчитали их избыточными для наших нужд. Потратив пару вечеров и много чашек кофе, мы взяли Swagger UI React Component, AsyncAPI React Component, дизайн систему Admiral и написали собственное решение.
В итоге получился дистрибутив, построенный по принципу Swagger UI. Это точно такой же статический набор файлов (HTML, CSS, JS), который можно закинуть на любой сервер, например, в Nginx, рядом закинуть документацию, развернуть в Docker-образе и пользоваться. Наш продукт называется RomeAPI: по аналогии с тем как все дороги ведут в Рим - все ссылки на спецификации ведут к нам в UI. Исходники вы можете найти в моем GitHub, забрать их и использовать в своих проектах.

Здесь есть список микросервисов и «переключалка» между REST API и AsyncAPI, а также версионирование спецификаций.
После того как мы решили задачи отображения документации, следующий шаг — разобраться с инструментами для работы со спецификациями, включая генерацию кода и другие возможности.
Инструменты для работы с документацией
@asyncapi/parser
Самый главный инструмент — это NodeJS библиотека asyncapi/parser. С помощью этого парсера происходит загрузка, парсинг, валидация спецификации. При необходимости, мы можем на лету динамически изменять что-то в наших спецификациях (например, подставлять переменные). Помимо этого, @asyncapi/parser предоставляет интерфейсы для реализации других парсеров, например для avro, raml и openapi. Стоит обратить внимание на один момент: на текущий момент работа @asyncapi/parser со спецификацией версии 3.0 не до конца реализована. Поэтому в своих проектах мы до сих пор используем спецификацию версии 2.6.
@asyncapi/cli
Эта утилита тоже написана на NodeJS и позволяет удобно работать из консоли с AsyncAPI документацией.

Через asyncapi/cli можно сделать всё, что угодно — вызвать генераторы, валидаторы, создавать новую документацию и даже локально запустить AsyncAPI Studio.
AsyncAPI Generator
Самый главный генератор — это AsyncAPI Generator. Он генерирует готовый проект и поддерживает популярные языки и фреймворки. У него есть набор шаблонов для популярных стеков и языков (но, их не так много). Генератор вызывается с помощью NodeJS или @asyncapi/cli. При использовании @asyncapi/cli:
asyncapi generate fromTemplate @asyncapi/java-spring -i asyncapi.yml -o folder [--params]
AsyncAPI Generator — довольно сложный инструмент, для него не так-то легко написать полноценный генератор кода и самый большой его минус - он генерирует готовый проект, что далеко не всегда подходит в реальных проектах. Помимо этого, чтобы его использовать вам надо хорошо знать NodeJS. Так как он генерирует готовые проекты, мы не смогли найти для него применение в своих проектах, кроме одного случая.
Допустим, у вас налажено стандартное взаимодействие через Kafka: продюсер отправляет сообщения, а консьюмер их получает. При этом для валидации сообщений используется схема registry на основе JSON-схем, которая проверяет данные, отправленные продюсером и принимаемые консьюмером.

С помощью AsyncAPI вы можете автоматически сгенерировать код как для продюсера, так и для консьюмера, а также создать JSON-схемы для загрузки в schema registry. Первое, что мы должны сделать, это создать проект с такой иерархией

У нас есть два файла package.json и index.js. В index.js и package.json записываем несколько строк кода на JavaScript, чтобы заимпортить созданный для генератора SDK.
В package.json добавляем пару строк кода, чтобы импортировать библиотку SDK для генератора.
{
"name": "asyncapi-json-schema-template",
"generator": {
"renderer": "react"
},
"dependencies": {
"@asyncapi/generator-react-sdk": "^0.2.25"
}
}
Затем нам надо написать сам код генератора в файле index.js
export default function ({ asyncapi, params, originalAsyncAPI }) {
const schemas = {};
for (const [channelName, channel] of Object.entries(asyncapi.channels())){
const messages = [
...(channel.publish()?.messages() || []),
...(channel.subscribe()?.messages() || [])
];
messages.forEach((message, index) => {
if(!message.payload()) return;
const messageName = message.name() || `${channelName}_message_${index}`;
schemas[messageName] = message.payload().json();
})
}
return Object.entries(schemas).map(([name, schema]) => (
<File key={name} name={`${name}.schema.json`}>
<Text>{JSON.stringfy(schema, null, 2)}</Text>
</File>
));
}
Затем вызываем команду из консоли, которая сгенерирует JSON-схемы из AsyncAPI спецификации с помощью нашего шаблона.
asyncapi generate fromTemplate asyncapi.yml ./asyncapi-json-schema-template
AsyncAPI Modelina
Следующий инструмент генерирует только DTO-модели — без всяких обвязок, интерфейсов и прочего. Modelina поддерживает практически все популярные языки программирования: Typescript, C#, GO, Java, Javascript, Dart, Python, Rust, Kotlin, PHP, C++, Scala
Это достаточно гибкий инструмент. Если вам нужно что-то изменить в генерируемом коде, вы можете дописать свои фильтры и изменить готовый сгенерированный код. Также можно написать свои шаблоны.
Modelina вызывается с помощью NodeJS, достаточно выполнить код, описанный в официальной документации.
import {JavaGenerator, JAVA_COMMON PRESET } from '@asyncapi/modelina'
const generator = new JavaGenerator({
collectionType: "List",
presets: [
{
preset: JAVA_COMMON PRESET,
options: {
classToString: true
}
}
]
});
// const input = ...AsyncAPI document
const models = await generator.generate(input)
Если честно, нас этот код немножко напугал, потому что мы все-таки джависты. Поэтому для нас самым удобным способом оказался вызов Modelina с помощью @asyncapi/clue через консоль:
asyncapi generate models java ./asyncapi.yml -o ./generated-folder
Другие генераторы кода
Помимо этого, у нас есть другие генераторы кода, созданные сообществом:
Нам они не зашли, но для объективности я должен был их упомянуть.
После того как мы разобрались с инструментами для генерации кода, нам нужно сгенерировать код. Для этого нам нужно построить свой CI/CD.
Как мы построили CI/CD
Так должен выглядеть CI/CD в классическом случае.

У нас есть проект с документацией, нода, две утилиты (@asyncapi/cli и @asyncapi/modelina. На выходе мы получаем Java код идокументацию для отображения на UI. Но такой путь нас не особо устраивал, потому что нужны свои агенты на ноде, а у нас всё-таки Java-проекты. Поэтому мы пошли своим путём.
CI/CD – наш путь

У нас есть проект с документацией, где спецификации хранятся в виде иерархии файлов. На первом этапе нам необходимо сделать валидацию спецификаций (нужно же проверить, что мы написали все правильно) и bundle ("склеивание" всех частей в один файл). Шаг bundle нужен прежде всего для того, чтобы хорошо работали генераторы кода. Помимо этого, один файл спецификации проще переслать другой команде и удобнее использовать для отображения. Для валидации и объединения мы решили использовать gradle и собирать наш проект с документацией точно так же, как и любой другой.
Далее все спецификации упаковываются в архив, которому присваивается версия, соответствующая версии API всей системы. Этот архив мы кладем как артефакт в Nexus, чтобы удобно скачивать и использовать его, по сути, как обычную библиотеку.
Из этого архива создаётся Docker-образ, который разворачивается на тестовых контурах. Таким образом, у нас всегда под рукой удобное отображение спецификации (и OpenAPI, и AsyncAPI).
Архив также служит источником для кодогенерации: для OpenAPI мы генерируем фронтенд, сервер и клиенты, а для AsyncAPI — dto-модели сообщений.
После внедрения этой схемы мы полностью перестали думать о межсервисном взаимодействии. Процесс полностью автоматизирован: при изменении API мы обновляем спецификацию, пересобираем микросервисы, и они успешно продолжают работать.
CI/CD – Gradle
Разберём немного подробнее, как делаем сборку с помощью Gradle.
Мы подключаем плагин для NodeJS, который автоматически загружает нужную версию Node, либо указываем её вручную.
plugins {
id("com.github.node-gradle.node") version "7.0.2"
}
После этого настраиваем ноду для установки всех необходимых пакетов, требуемых для работы с AsyncAPI.
node {
download.set(true)
version.set("22.9.0")
allowInsecureProtocol.set(true)
nodeProjectDir.set(file("${project.rootDir}/.gradle/nodejs-modules"))
workDir.set(file("${project.rootDir}/.gradle/nodejs"))
npmWorkDir.set(file("${project.rootDir}/.gradle/npm"))
}
Автоматизируем процесс установки необходимых npm-зависимостей из Gradle-сценария.
tasks.register("asyncapiInstall", NpxTask::class.java) {
command="npm"
args = listOf("install", "@asyncapi/cli", "@asyncapi/modelina")
}
После этого Gradle полностью готов к вызову команд @asyncapi/cli.
CI/CD – Bundle AsyncAPI
Для выполнения операции bundle мы рекурсивно проходим по файловой структуре проекта и ищем все файлы с именем asyncapi.yml.
Затем для каждого файла asyncapi.yml мы создаем задачу, которая вызывает asyncapi/cli через NodeJS плагин и прокидываем ему параметры command.set("@asyncapi/cli")
и указываем, что надо сделать bundle
.
fileTree(buildSourcesDir).matching {
include("**/asyncapi.yaml", "**/asyncapi.yml", "**/asyncapi.json")
}.forEachIndexed { index, el ->
tasks.register<NpxTask>("generate-asyncapi-${index}", NpxTask::class) {
command.set("@asyncapi/cli")
workingDir.set(el.parentFile)
args.set(
listOf(
"bundle",
el.toURI().path,
"-o"
"asyncapi.yaml",
"-x"
)
)
}
}
На выходе получаем один файл со схлопнутой иерархией.
CI/CD – Modelina
Для Modelina аналогичный подход.
tasks.register("generateAsyncApi", NpxTask::class.java) {
command = "@asyncapi/cli"
args = listOf(
"generate",
"models",
"java",
"asyncapi.yml",
"-0",
layout.buildDirectory.file("generated/sources/asyncapi/").get().asFile.absolutePath,
"--packageName=${pkg}",
"--javaJackson",
"--javaArrayType=List",
"--javaIncludeComments",
)
}
Мы точно также берем @asyncapi/cli, указываем в параметрах генерацию моделей и передаём дополнительные опции, которые можем указать в рамках документации.
Выводы
Мы перестали воспринимать нашу систему как «чёрный ящик» в контексте асинхронных взаимодействий. Теперь не нужно собирать встречи на несколько часов, чтобы понять, какие данные мы отправляем в другую систему и что должны получить взамен. Не нужно ковыряться в исходниках и разбираться, как формируется json для отправки в кафку.
Для нас использование AsyncAPI сильно упростило исправление багов, расследование проблем или внедрение новых фич. Ведь достаточно просто открыть спецификацию, чтобы увидеть, что именно происходит в системе.
Совместное использование с OpenAPI дало нам возможность полностью покрыть описание API нашей системы. Как результат, мы создаём стабильные артефакты и больше не тратим много времени на правку багов.
6 и 7 ноября в Технопарке «Сколково» в Москве пройдёт профессиональная конференция для разработчиков высоконагруженных систем HighLoad++. Поговорим о том, как ещё налаживать обмен данными, чтобы автоматизировать рутинные процессы и посвящать больше времени творческим. Подробная информация на официальном сайте конференции.