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) существует:

При использовании генератора кода не удалось сгенерировать только отдельную часть приложения - приложение генерируется целиком. Протокол 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

Итак, у нас появилась цель - воссоздать API First для приложений, использующих брокеры сообщений. Одним из наиболее распространенных решений для асинхронного общения между сервисами является Kafka. Решили начать с реализации этих идей именно для неё.

Для того чтобы начать использовать подход API First необходимо:

  1. Инициировать спецификацию. Этот шаг необходим, если вы хотите начать использовать подход API First в уже разрабатываемом долго продукте.

  2. Внедрить генерацию кода по спецификации в процесс разработки.

  3. Использовать сгенерированный код.

AxenAPI состоит из двух частей:

  1. AxenAPI Gradle plugin - gradle плагин, позволяющий генерировать код по спецификации интерфейса, реализованного с помощью брокера сообщений.

  2. AxenAPI library - библиотека, позволяющая генерировать контроллеры для ваших kafka-listeners. Далее, с помощью уже имеющихся средств (например, https://springdoc.org/) можно сгенерировать спецификацию OpenAPI.

Основным шагом при использования подхода API First является генерация кода по спецификации, поэтому сначала поговорим о плагине, позволяющим генерировать код.

AxenAPI Gradle plugin. Генерация кода

Для того чтобы обеспечить полный цикл API First необходимо генерировать по спецификации код сервера и клиента. Это дает гарантию, что ваше приложение реализует данное в спецификации API. Рассмотрим генерацию кода сервера. За несколько шагов вы можете избавить себя от рутины и сгенерировать код моделей и обработчиков событий (handlers), а не писать их самостоятельно.

AxenAPI Gradle plugin. Подключение

  1. Подключите плагин:

plugins {
  //....
    id 'axenapi-generator-plugin' version '1.0.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"
      }
    }
  }
}

  1. Добавьте настройки для плагина в 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
}
  1. Добавьте зависимость шага компиляции от шага генерации кода. Для этого вставьте в build.gradle следующие строки:

compileJava {   dependsOn "generateKafka"}
  1. Соберите ваш проект - выполните 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 каталогу. Рекомендуемое значение "src/main/java"

listenerPackage *

String

Нет значения по умолчанию

package, в который попадут сгеренированные client/listeners

modelPackage *

String

Нет значения по умолчанию

package, к который попадут сгеренированные модели (Data Transfer Object)

useSpring3

Boolean

false

Если true, то генерация будет происзодить для springboot 3.1. Если false, то для spring boot 2.7

kafkaClient

Boolean

false

Если true, будет генерироваться клиент (producer сообщений), false - интерфейсы сервера (consumer)

interfaceOnly

Boolean

true

Влияет только на генерацию клинета. Если true - то будут сгенерированы классы реализации отправки сообщений в kafka. Если false - только интерфейсы.

resultWrapper

String

""

Класс, в который будет обернуто возвращаемое значение. Необходимо описать полный путь к классу.

securityAnnotation

String

""

Класс аннотации, который выставляется при генерации сервера при использовании в consumer авторизации. Если ничего не указано, то security-аннотации не будут ставится.

sendBytes

Boolean

true

Если стоит true, то не будет отправлять header с маппингом типов на наименование headers. Если false - то будет.

useAutoconfig

Boolean

true

Если true, то при генерации клиента будет сгенерированы файлы для автоконфигурации.

generateMessageId

Boolean

false

Если true, то сгенерированный клиент будет автоматически проставлять header kafka_messageId (или другое наименование из параметра messageIdName). Значение - случайный UUID.

generateCorrelationId

Boolean

false

Если true, то сгенерированный клиент будет автоматически проставлять header kafka_correlationId (или другое наименование из параметра correlationIdName). Значение - случайный UUID.

messageIdName

String

"kafka_messageId"

Наименование header, в который положится значение messageId (если generateMessageId = true)

correlationIdName

String

"kafka_correlationId"

Наименование header, в который положится значение correlationId (если generateCorrelationId = true)

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. Это делается буквально за несколько шагов:

  1. Подключите нужные зависимости. Для этого необходимо прописать следующие зависимости в gradle.build:

    //	swagger for kafka
    annotationProcessor "org.axenix:axenapi:1.0.0"
    implementation ("org.axenix:axenapi:1.0.0")
  1. Добавьте в зависимости аннотации Swagger (этот шаг можно пропустить, если вы подключите Swagger-UI):

implementation 'io.swagger.core.v3:swagger-annotations:2.2.10'
  1. В application.yml вашего приложения добавьте:

axenapi.kafka.swagger.enabled: true
  1. Соберите проект. Во время сборки проекта сгенерируются контроллеры для ваших listeners.

  2. Добавьте Swager-UI в ваше приложение, чтобы увидеть результат - это самый простой способ  воспользоваться и наглядно увидеть ваше API и все используемые в нем модели:

implementation 'org.springdoc:springdoc-openapi-ui:1.6.13' 
  1. Запустите приложение.

  2. Затем откройте http://<host>:<port>/swagger-ui/index.html в вашем браузере, и вы увидите Swagger-UI, в котором присутствуют Post методы для отправки сообщений в соответствующий топик и группу (информация о топике и группе есть в url метода).

  1. Разделите спецификацию и 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).

Будем благодарны за обратную связь и помощь в развитии продукта. 

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


  1. MaxLevs
    18.10.2023 04:54

    А как же AsyncApi?


    1. MaxLevs
      18.10.2023 04:54

      1. SashaVolushkova Автор
        18.10.2023 04:54

        Добрый день. Про AsyncAPI написано в статье. Мы его рассматривали. Но у него для нас есть критичные недостатки: инфраструктура вокруг него вся на JS и это еще один DSL.


        1. inscriptios
          18.10.2023 04:54
          +2

          У вас какая-то странная получилась история. Вроде бы и есть объяснение странностей, но от этого история не перестает быть странной. Складывается ощущение, что вы не до конца поняли что такое AsyncAPI.

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

          Но это он и есть https://www.asyncapi.com/. Инструмент под названием "спецификация".

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

          Нет, уже есть спецификация — AsyncAPI. Если вы считаете, что она "неполноценна" или что-то подобное, так присоединитесь и помогите https://github.com/asyncapi?type=source#-contribute-to-asyncapi, вместо того, чтобы изобретать велосипед.

          Как мы знаем, OpenAPI - это нотация для описания http интерфейсов. Соответственно, вся инфраструктура вокруг OpenAPI спецификации построена для http. Для использования OpenAPI инфраструктуры необходимы доработки.

          Да, они и были сделаны в AsyncAPI https://www.asyncapi.com/docs/tutorials/getting-started/coming-from-openapi, а вы все еще изобретаете велосипед.

          AsyncApi - протокол для описания асинхронного общения между приложениями. Протокол позволяет проектировать приложения в Event-Driven Architecture. У OpenAPI и у AsyncAPI есть много общего (например, описание моделей).

          Нет, это не "протокол", это "спецификация", аналогичная OpenAPI, но для асинхронных API.

          Ну, и еще одно:

          Мы его рассматривали. Но у него для нас есть критичные недостатки: инфраструктура вокруг него вся на JS и это еще один DSL.

          Какая еще инфраструктура? О чем вы? Ну и не так все плохо с Java https://www.asyncapi.com/tools?pricing=free&langs=Java, в том числе с генерацией кода из дока на основе этой спецификации https://www.asyncapi.com/tools?pricing=free&langs=Java&categories=Code+Generators.

          P.S. А, это я непонял — вы продвигаете "AxenAPI - разработка Axenix". Тогда все понятно)


          1. SashaVolushkova Автор
            18.10.2023 04:54
            +1

            генераторы кода для Async API на node JS - по крайней мере то что я нашла на их сайте. Соответственно для сборки проекта с генерацией кода в инфраструктуру для сборки Java приложений надо тянуть ноду (не хочу). Я хочу такой же процесс сборки - плагин (в моем случае gradle plugin). Надо посмотреть внимательнее на плагин, который вы прислали (спасибо).

            Так же, когда речь идет о переходе на api-first, то изучение двух DSL станет большим препятствием. Мы решили немного приблизить спецификацию асинхронного общения к OpenAPI


  1. Russover
    18.10.2023 04:54

    Расскажу про свой опыт использования данной практики в разработке. На текущем месте где я работаю, чуть меньше года как была введена инженерная практика api first. Нам тоже говорили про все эти плюсы и говорили, что это будет круто, всем будет удобно этим пользоваться, но по факту, оказалось нафиг это никому не нужно и во многие сервисы просто были добавлены api.yaml кто-то json только лишь для прохождения проверки quality gates. И так с какими же проблемами мы столкнулись.

    1. Проблема с generic типами для java/kotlin моделей. В запросах и ответах мы везде пользуемся обобщенными типами, используемая библиотека openapi-generator не предполагает работу с такими данными типа val data: T? = null. Да мы это обошли через костыли путем переопределения шаблонов и классов генерации, но по итогу в описанных схемах у нас одни данные, а на выходе сгенерированные данные другие уже в обернутых классах с обобщенными типами.

    2. Переход на сгенерированные данные. Тут начинается боль перехода с data классов на java классы, для универсальности мы генерируем данные на java, так как не все пишут на kotlin. Проекты которые уже были написаны давно используют свою устоявшуюся модель, то при переходе на сгенерированные данные и где в задаче у нас есть расширение модели путем добавления новых данных в openapi спецификацию, по цепочке ведет к большому переписыванию на новые модели. Отсутствие крайне полезной функции copy()

    3. Миф о том что можно заслать свое api в другую команду и они это заиспользуют, уменьшает время простоя команды. Нет, это так не сработает, у всех сервисов есть свои нюансы ведения моделей. В большом энтерпрайзе никто этого делать не будет. Есть документация в вики-системах, это куда быстрее и удобнее найти актуальную модель используемая теми или иными сервисами.

    4. Каждая команда начинает генерировать свои модели, хотя по составу они за частую идентичны с тем что используется в соседней команде. Тут куда эффективнее и проще иметь одну common библиотеку, куда все команды будут закидывать актуальные изменения.

    Итого: Для проектов которые уже давно существуют, переходить на технологию api first это будет еще одна проблема в вашем коде :) Когда это следует использовать, ну к примеру в не большом проекте с не большим количеством сервисов 2-4, который только начинает развитие, возможно это и привнесет какие-то плюсы, но скорее всего больше минусов. Не забывайте, что из коробки не сможете пользоваться обобщенными типами, придется попотеть чтобы научиться это делать. Рекомендую ли я пользоваться api firstom'ом, однозначно нет, на презентациях и в статьях на эту тему всегда красиво пишут про данную практику, но на деле это не так, по своему опыту внедрения данной технологии очень много ресурсов ушло чтобы все это дело как-то заработало с тем что есть :)


    1. SashaVolushkova Автор
      18.10.2023 04:54
      +1

      Переход на какой либо процесс - это всегда нетривиальная задача. У меня был опыт гибрида: мы клиенту (js+react) отсылали спеку (сложную с обобщёнными типами), а сами эту спеку создавали автоматически по моделям java. В итоге клиент перешел на генерацию кода, а мы (бек) нет. Но это нас вполне устроило. Немного смягчит сценарий перехода.

      По поводу крупный или не крупный проект: постановка вопроса от размера проекта абсолютно не зависит. Ломать что-то работающее всегда сложно. Прекрасно видела систему из множества сервисов (ты даж не знал, кто твои клиенты) и единственное чем обменивались была подобная спецификация.

      По поводу коммон библиотек: распространение коммон библиотек у нас привело к тому, что в одном общем классе было 100500 групп валидаций. Да и как только коммон библиотека меняет версию начинается очень большая беготня (или если версии где-то разошлись). Если используются спеки эта проблема пропадает (при полной поддержке обратной совместимости).

      С дженериками при неудачном стечении обстоятельств действительное были проблемы (сейчас уже лучше - начиная с 3.1).


      1. Russover
        18.10.2023 04:54

        Про генерацию кода на бэке и передачу сгенерированных интерфейсов для фронта, это все таки задача не много про другое. Я делал на стороне бэка ровно такую же задачу, на этапе сборки мы это все по красоте упаковывали в npm артефакт и фронт мог спокойно забрать нужную версию. Но все таки тут есть зависимость одной команды от другой и такой подход сложно назвать api first'ом.

        Если же все таки и нужно как-то быть в одном информационном поле, то на мой взгляд нет ничего проще и удобнее использовать генерацию на основе json/xsd/wsdl и т.д. схем.


        1. SashaVolushkova Автор
          18.10.2023 04:54

          Генерация кода - это хорошее дополнение к процессу.


  1. SashaVolushkova Автор
    18.10.2023 04:54

    API First как процесс не имеет ничего общего с генерацией кода. Это процесс при котором сначала разрабатывается API. Потом всё остальное. Поэтому не рекомендовать процесс я не могу :-).

    Генерация кода - это хорошее дополнение к процессу. Почему бы не сгенерировать код, если команда определилась как будет описывать API . В нашем случае то самое единое информационное поле - это спецификации в OpenAPI формате (имхо: wsdl немного архаично смотрится, json scheme не содержит всю информацию об API).

    Мы не упаковывали npm - мы именно отдавали спеку. На мой взгляд, это удачный пример мягкого перехода на описанные процессы генерации кода. Никто не мешает поочередно переходить на использование спецификации в качестве источника информации об API.