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

Синхронное взаимодействие

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

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

В отличие от синхронного, асинхронное взаимодействие — сложное. У него отсутствует прямой запрос-ответ. Вместо этого обмен сообщениями происходит через очереди, топики и другие механизмы публикации-подписки.

У нас есть publisher, который отправляет сообщение через месседж-брокер, и кто-то это сообщение слушает. В таких случаях OpenAPI уже не применим, и приходится пользоваться другими способами:

  1. Изучение исходного кода — надёжный, классический подход, проверенный временем. При такой интеграции можно запросить доступ к другой системе, написать пару писем безопасникам с обоснованием, зачем нам это надо, и подождать недельку, пока дадут доступ. И только тогда можно будет посмотреть в исходники и реализовать корректную интеграцию с другой системой.

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

  3. Написать в телегу тимлиду другой команды в пятницу вечером, а лучше поставить встречу человек на 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 Studio - аналог Swagger Editor
AsyncAPI Studio - аналог Swagger Editor

Есть официальный сайт с документацией и описанием всех утилит.

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

Пример описания спецификации AsyncAPI
Пример описания спецификации AsyncAPI

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

Сравнение стандартов OpenAPI 3.0 и AsyncAPI 2.0
Сравнение стандартов OpenAPI 3.0 и AsyncAPI 2.0

Если коротко, то очень похожи по структуре, но различаются по терминологии и моделям. Например, в 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), образовав следующую иерархию:

Разбивка спецификации AsyncAPI на несколько файлов
Разбивка спецификации AsyncAPI на несколько файлов

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

Иерархия проекта с документацией для нескольких сервисов
Иерархия проекта с документацией для нескольких сервисов

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

Структура проекта с "общими" схемами
Структура проекта с "общими" схемами

Эти общие объекты доменной модели мы импортируем через файл common.yaml для того, чтобы скрыть длинный относительный путь до "common-схем" и всегда видеть, какие общие объекты в каких сервисах используются. Это очень удобно при рефакторинга и поддержки системы.

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

Добавили OpenAPI в проект с документацией
Добавили OpenAPI в проект с документацией

Дальше у нас встает вопрос - а как нам отобразить документацию? У нас есть проект с документацией и множество yml-файлов, отдельно Swagger UI и AsyncAPI Studio. Несмотря на то, что платные решения могли бы упростить задачу, мы посчитали их избыточными для наших нужд. Потратив пару вечеров и много чашек кофе, мы взяли Swagger UI React Component, AsyncAPI React Component, дизайн систему Admiral и написали собственное решение.

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

UI для отображения спецификаций OpenAPI и AsyncAPI
UI для отображения спецификаций OpenAPI и AsyncAPI

Здесь есть список микросервисов и «переключалка» между 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 для взаимодействия через Kafka Schema Registry
Использование AsyncAPI для взаимодействия через Kafka Schema Registry

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

Иерархия собственного шаблона для AsyncAPI Generator
Иерархия собственного шаблона для AsyncAPI Generator

У нас есть два файла 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

Другие генераторы кода

Помимо этого, у нас есть другие генераторы кода, созданные сообществом:

MultiAPI Generator

ZenWave SDK

jsonschema2pojo

Нам они не зашли, но для объективности я должен был их упомянуть.

После того как мы разобрались с инструментами для генерации кода, нам нужно сгенерировать код. Для этого нам нужно построить свой 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++. Поговорим о том, как ещё налаживать обмен данными, чтобы автоматизировать рутинные процессы и посвящать больше времени творческим. Подробная информация на официальном сайте конференции.

Комментарии (0)