Pull to refresh

Генерация контрактов OpenApi или прикладной API first: гайд по генерации в Spring Boot приложении

Level of difficultyMedium
Reading time7 min
Views8.3K

В этой статье будет описана только техника хранения спецификаций и конкретные шаги по их подключению в приложении. Для упрощения здесь не будут описаны некоторые принципы важные для использования в production коммерческих проектов. Здесь совсем чуть-чуть про то почему пошли в эту историю.

Ввиду некоторых проблем с доступом к документации openapi на сайте я буду оставлять ссылки на документацию в Github.

1. Подготовка репозитория

Создаём репозиторий в котором будут располагаться наши спецификации и укладываем нужные файлы в следующую структуру:

repository/
  spring-boot-openapi-generation/
    1.0.0/
      api.yaml
      models/
        components.yaml
        some-endpoint.yaml

repository/ - корневая директория, может иметь любое подходящее название. Здесь мы будем складывать все наши сервисы.

spring-boot-openapi-generation/ - сервис, над API которого мы сейчас работаем.

1.0.0/ - конкретная версия API. По мере доработок сервиса рядом будут создаваться 1.0.1 и последующие версии API.

api.yaml - файл первого уровня контракта, в котором описаны эндпоинты, теги и id.

models/ - директория, в которой будут располагаться реализация моделей из api.yaml

components.yaml - файл второго уровня контракта, в котором описаны модели запросов и ответов конкретных эндпоинтов.

some-endpoint.yaml - файл третьего уровня контракта, в котором развёрнуто описаны все используемые в конкретных моделях запросов и ответов поля.

При этом api.yaml, some-endpoint.yaml и components.yaml являются обычными файлами openapi спецификации. Разделение на несколько уровней обусловлено желанием снизить сложность работы с API. Так разные файлы дают возможность работать отдельно с эндпоинтами, моделями и полями.

1.1 Первый уровень API - эндпоинты

api.yaml - файл первого уровня контракта, в котором описаны эндпоинты, теги и id.

openapi: 3.0.3
info:
  title: API сервиса spring-boot-openapi-generation
  version: 1.0.1

Информация о версии openapi, описание спецификации и версия которую присваиваем мы. Для удобства работы с файлами рекомендуется держать version одинаковыми в соответствующих some-endpoint.yaml и components.yaml файлах.
Здесь можно написать заполнить разные поля о самой спецификации.

servers:
  - url: http://localhost:8080
  - url: http://url-development.ru/spring-boot-openapi-generation
  - url: http://url-preproduction.ru/spring-boot-openapi-generation
  - url: http://url-production.ru/spring-boot-openapi-generation

При генерации сервера в качестве базового url контроллера будет использоваться первый из перечисленных серверов. Чтобы избежать лишних конфигураций стоит использовать конструкцию из примера и получить '/' в качестве базового пути.
Для клиента первый url будет генерироваться как адрес по умолчанию.

tags:
  - name: SpringBootOpenApiGeneration
    description: Spring boot openapi generation service API

Тэги используются как часть имени генерируемого контроллера и клиента. Рекомендуется указывать camel case название класса чтобы получать привычные названия классов на выходе.

paths:
  /api/v1/some-endpoint:
    post:
      tags:
        - SpringBootOpenApiGeneration
      summary: Описание логики спрятанной за эндпоинтом
      operationId: someEndpoint

Для того, чтобы метод сгенерировался в нужном контроллере необходимо указать tags и в самом методе.
operationId здесь это название java методов которые будут генерироваться в сервере и клиенте. Пишем их сразу в camelCase.

requestBody:
  $ref: 'models/some-endpoint.yaml#/components/requestBodies/SomeEndpointRequest'
responses:
  '200':
    description: Логика успешно выполнена
  '400':
    description: Нарушение контракта
    content:
      application/json:
        schema:
          $ref: 'models/some-endpoint.yaml#/components/schemas/SomeEndpointErrorResponse'
  '500':
    description: Какая-то страшная внутренняя ошибка
    content:
      application/json:
        schema:
          $ref: 'models/some-endpoint.yaml#/components/schemas/SomeEndpointErrorResponse'

При описании моделей запросов и ответов мы указываем только ссылки на них.

1.2 Второй уровень API - модели

some-endpoint.yaml - файл второго уровня контракта, в котором описаны модели запросов и ответов конкретных эндпоинтов. На каждый эндпоинт создаётся отдельный файл.

openapi: 3.0.3
info:
  title: Модели запросов и ответов эндпоинта some-endpoint
  version: 1.0.1
paths:
  /:

Описание файла. Из интересного здесь - ограничение спецификации. Она не может считаться валидной без указания хотя бы одного пути.

components:
  schemas:
    SomeEndpointErrorResponse:
      description: Ответ при ошибке
      type: object
      properties:
        errorMessage:
          $ref: 'components.yaml#/components/schemas/errorMessage'
  requestBodies:
    SomeEndpointRequest:
      description: Тело запроса на выполнение какой-то логики
      required: true
      content:
        application/json:
          schema:
            required:
               - id
               - code
            type: object
            properties:
              id:
                $ref: 'components.yaml#/components/schemas/id'
              code:
                $ref: 'components.yaml#/components/schemas/code'

Описываем наши объекты, показываем что в них существуют поля, и оставляем ссылки на них.

1.3 Третий уровень контракта - поля

components.yaml - файл третьего уровня контракта, в котором развёрнуто описаны все используемые в конкретных моделях поля.

components:
  schemas:
    id:
      description: Какой-нибудь важный идентификатор
      type: string
      example: "1234"
    code:
      description: Не менее важный код
      type: string
      example: "4321"
    errorMessage:
      description: Описание ошибки при обращении к сервису
      type: string
      example: "Сервис временно недоступен"

Подробно описываем наши поля, не забываем про примеры, валидацию и другие важные атрибуты

2. Генерация сервера и клиента

Немного про генерацию со стороны OpenApi. Существуют разные генераторы, список которых можно посмотреть здесь. Из этого списка мы будем пользоваться spring генератором, описание которого можно посмотреть здесь. Хоть этот генератор и находится в категории servers, мы будем использовать его для генерации и серверной и клиентской части.

Можно подсматривать в пример проекта где уже настроена генерация.

Для подключения спецификации и генерации по ней мы идём в build.gradle, где подключаем плагин и добавляем следующие зависимости:

'org.springdoc:springdoc-openapi-ui:1.6.6',
'io.swagger.core.v3:swagger-annotations:2.2.14',
'org.openapitools:openapi-generator-gradle-plugin:7.0.1',
'jakarta.validation:jakarta.validation-api'

Делаем поправку на актуальные версии, не забываем подключить spring-web и spring-openfeign.

Далее настраиваем генерацию начиная с этой строки.

sourceSets {
    main {
        java {
            srcDirs += "$buildDir/generated"
        }
    }
}

Подключаем сгенерированный код в исходники.

//def authorizationToken = System.properties["specification_repository_authorization_token"]

//auth.set("Authorization:Bearer $authorizationToken")

Эти строки закомментированы ввиду того что подключение к репозиторию со спецификациями в Github происходит без дополнительной авторизации. Но если вы работает с приватными ресурсами - эти строки помогут вам настроить доступ к файлам.

tasks.register('generate spring-boot-openapi-generation-service 1.0.0 server', GenerateTask) {
    generatorName.set("spring")
    remoteInputSpec.set("https://raw.githubusercontent.com/SaintCheshire/specifications/main/repository/spring-boot-openapi-generation/1.0.0/api.yaml")
//    auth.set("Authorization:Bearer $authorizationToken")
    outputDir.set("$buildDir/generated")
    ignoreFileOverride.set(".openapi-generator-ignore")

Регистрируем задание на генерацию клиента и указываем GenerateTask из подключенного плагина.

generatorName.set("spring") - указываем что мы хотим использовать генератор для spring приложения.

remoteInputSpec.set("https://raw.githubusercontent.com/SaintCheshire/specifications/main/repository/spring-boot-openapi-generation/1.0.0/api.yaml") - указываем где лежит входная точка в конкретную версию API нашего сервиса. Нужно обратить внимание что ссылка указывается на raw представление нашей спецификации.

outputDir.set("$buildDir/generated") - указываем директорию куда будут складываться сгенерированные классы (та самая которую мы включили в исходники)

ignoreFileOverride.set(".openapi-generator-ignore") - ссылка на важный файлик, который поможет игнорировать сгенерированные Application классы

configOptions.set([
            library                             : "spring-boot",
            invokerPackage                      : "saint.cheshire.specifications.spring_boot_openapi_generation.v1_0_0.server",
            apiPackage                          : "saint.cheshire.specifications.spring_boot_openapi_generation.v1_0_0.server.api",
            modelPackage                        : "saint.cheshire.specifications.spring_boot_openapi_generation.v1_0_0.server.model",
            configPackage                       : "saint.cheshire.specifications.spring_boot_openapi_generation.v1_0_0.server.configuration",
            basePackage                         : "saint.cheshire.specifications.spring_boot_openapi_generation.v1_0_0.server",
            useOptional                         : "true",
            openApiNullable                     : "false",
            interfaceOnly                       : "false",
            sourceFolder                        : "",
            additionalModelTypeAnnotations      : "@lombok.Builder(toBuilder = true)\n@lombok.RequiredArgsConstructor\n@lombok.AllArgsConstructor\n@com.fasterxml.jackson.annotation.JsonIgnoreProperties(ignoreUnknown=true)",
            generatedConstructorWithRequiredArgs: "false",
            useTags                             : "true"
    ])

Настройки самого генератора.

library: "spring-boot" - указываем что мы хотим сгенерировать именно серверную часть

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

additionalModelTypeAnnotations и generatedConstructorWithRequiredArgs - говорим что не нужно генерировать RequiredArgs конструктор, и вешаем свой набор аннотаций на модели.

useTags: "true" - важный параметр, который определяет что в качестве имён классов и методов будут использоваться те самые теги

При генерации клиента свойства задания указываются аналогично. Самое важное - указать library: "spring-cloud", что позволит нам генерировать уже клиента.

3. Подключение сервера и клиента

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

Чтобы подключить клиентскую часть, необходимо добавить в конфигурацию нашего приложения обнаружение feign клиентов:

После этого остаётся использовать сгенерированный feign как обычный spring компонент.

Tags:
Hubs:
Total votes 4: ↑3 and ↓1+4
Comments9

Articles