Генерация HTTP клиентов для Spring Boot приложения по OpenAPI спецификации

от автора

Следуя этому пошаговом руководству, вы научитесь генерировать код HTTP клиентов для Spring Boot приложения по OpenAPI спецификации, используя плагин openapi-generator для Gradle.

OpenAPI стал стандартом для определения и документирования RESTful API на основе HTTP. Хотя Spring Boot не поддерживает OpenAPI из коробки, существует несколько сторонних проектов, которые заполняют этот пробел:

  • ePages-de/restdocs-api-spec — расширение для Spring REST Docs, которое добавляет функциональность генерации OpenAPI спецификаций 

  • Springdoc — генерирует OpenAPI спецификации в runtime, базируясь на Spring MVC/Webflux контроллерах и конфигурации проекта

  • Springwolf — похож на Springdoc, но для асинхронных API генерирует спецификации, совместимые с AsyncAPI для интеграции с Kafka/RabbitMQ/JMS/SQS и т.д.

Все три проекта охватывают серверную сторону API.

Поскольку OpenAPI — это хорошо определенная спецификация, она может использоваться также на клиентской стороне для генерации клиентского кода. Для этого случая существует проект, который воспринимается сообществом как стандарт де-факто: https://openapi-generator.tech/.

OpenAPI Generator генерирует клиентов для различных языков программирования и фреймворков. В зависимости от выбранного генератора он может сгенерировать клиентский или серверный код.

Сегодня я сосредоточусь исключительно на генерации клиентов для Spring Boot приложения. Также расскажу, как можно настроить генератор под свои нужды.

Настройка проекта

Начнем с пустого Spring Boot проекта, созданного с помощью start.spring.io, с выбранной зависимостью Spring Web и Gradle в качестве системы сборки.

Я собираюсь сгенерировать HTTP клиентов для Spring Petclinic Rest, поэтому я скопировал файл openapi.yml в корневую директорию моего проекта.

Установка OpenAPI генератора

OpenAPI генератор доступен как CLI, Maven или Gradle плагин. Я буду использовать Gradle, но конфигурацию легко адаптировать и для Maven плагина.

Настройте org.openapi.generator с последней версией в файле build.gradle:

plugins {     // ... id 'org.openapi.generator' version '7.7.0' }

После настройки плагина будут доступны новые Gradle task’и:

$ ./gradlew tasks ... OpenAPI Tools tasks ------------------- openApiGenerate - Generate code via Open API Tools Generator for Open API 2.0 or 3.x specification documents. openApiGenerators - Lists generators available via Open API Generators. openApiMeta - Generates a new generator to be consumed via Open API Generator. openApiValidate - Validates an Open API 2.0 or 3.x specification document. ...

Конфигурация

Конфигурируем генератор и библиотеку

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

На момент написания статьи, доступно три генератора под Java: java, java-helidon-client и java-micronaut-client. Означает ли это, что не существует специального клиента под Spring? На самом деле нет.

Каждый генератор имеет длинный список конфигурационных опций, и так случилось, что генератор java может быть использован для генерации версий клиентского кода, совместимых с разнообразными Java HTTP клиентами — от Feign, Retrofit, RestEasy, Vertx до нативного для Java HttpClient и RestTemplate, WebClient и RestClient, относящихся к Spring.

Важная информация

На момент написания данной статьи не существует опции генерации клиентского кода, использующего клиенты с @HttpExchange, базирующиеся на декларативных интерфейсах версий Spring 6+.

Выберем “современный” подход и создадим клиента, который базируются на  RestClient:

В build.gradle добавим следующий раздел:

openApiGenerate { generatorName.set('java') configOptions.set([ library: 'restclient' ]) }

Все возможные конфигурационные опции для генератора java можно найти в официальной документации.

Настраиваем расположение спецификации

OpenAPI Generator позволяет создавать код на основе спецификаций, хранящихся в локальных или удаленных JSON или YAML файлах.

Если у вас есть локальный файл спецификации, например, с именем petclinic-spec.yml, расположенный в той же директории, что и файл build.gradle, укажите путь к этому файлу в свойстве inputSpec, как показано ниже:

openApiGenerate { // ... inputSpec.set('petclinic-spec.yml') }

Для использования спецификации, расположенной удаленно, в OpenAPI Generator нужно указать URL-адрес вместо пути к локальному файлу. В этом случае, вместо свойства inputSpec, используйте remoteInputSpec, указав ссылку на удаленный файл.

openApiGenerate { // ... remoteInputSpec.set('http://some-service.net/v3/api-docs') }

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

Как только мы настроили спецификацию и генератор, можно вызвать ./gradlew openApiGenerate для генерации клиентского кода. Сгенерированный код появится в build/generate-resources/main, но его качество может оставлять желать лучшего.

Вообще я рассчитывал сгенерировать только классы модели, их маппинг на requests/responses и класс, представляющий API. Но генератор создал целый Maven/Gradle проект. В него вошли конфигурации CI, документация, Android Manifest и многое другое. Возможно, это не так важно для вас, ведь этот код вы не собираетесь сохранять в Git репозитории. Но для меня это проблема: слишком много лишних классов, которые не будут использоваться в classpath. Давайте упростим проект и оставим только действительно необходимый код.

Удаляем ненужный код

OpenAPI генератор поддерживает файл ignore, аналогичный .gitignore, который позволяет указать, какие файлы следует исключить при генерации проекта. Благодаря этому мы можем отфильтровать ненужные файлы и оставить только то, что действительно необходимо.

openApiGenerate {     // ... ignoreFileOverride.set(".openapi-generator-java-sources.ignore") }

Создадим файл .openapi-generator-java-sources.ignore в корневой директории проекта со следующим содержимым:

* **/* !**/src/main/java/**/*

Теперь, если вы снова сгенерируете проект с помощью команды ./gradlew openApiGenerate, вы увидите гораздо меньше лишних файлов:

Хотя все еще могут остаться некоторые классы, которые вы захотите исключить, я оставлю это на ваше усмотрение. Для меня такой результат вполне приемлем.

Конфигурируем имя пакета 

Возможно вы уже заметили, что классы генерируются в пакете org.openapitools.client. Чтобы изменить название пакета, настройте следующие свойства: 

openApiGenerate { // ... invokerPackage.set('com.myapp') modelPackage.set('com.myapp.model') apiPackage.set('com.myapp.api') }

Включаем сгенерированные исходники в проект 

По умолчанию Gradle не знает о расположении сгенерированного кода. Чтобы использовать его в вашем проекте, добавьте его в SourceSet:

sourceSets.main.java.srcDir "${buildDir}/generate-resources/main/src/main/java"

Теперь, когда вы запустите ./gradlew build, будет компилироваться как ваш собственный код, так и сгенерированный. Возможно, вас это удивит, но компиляция может завершится неудачно.

Исключаем модуль Jackson Nullable 

Сгенерированный код зависит от модуля jackson-databind-nullable. Так как мы убрали сгенерированные pom.xml и build.gradle файлы, и оставили только Java классы, у нас есть два варианта: добавить зависимость на этот модуль или настроить OpenAPI плагин так, чтобы он не использовал этот модуль. Я выбираю второй вариант.

openApiGenerate { configOptions.set([     // ... openApiNullable: 'false' ]) }

Настраиваем openApiGenerate для автоматического запуска перед каждой компиляцией

Я не хочу постоянно помнить о необходимости перезапускать генерацию классов после каждого изменения. Поэтому я настрою Gradle так, чтобы он автоматически перегенерировал классы при каждом запуске build или компиляции кода: 

tasks.named('compileJava') {     dependsOn(tasks.openApiGenerate) }

Что у нас получилось

Вот финальная версия файла build.gradle с учетом всех упомянутых изменений. Вы можете просто скопировать и вставить его: 

plugins { id 'java' id 'org.springframework.boot' version '3.3.2' id 'io.spring.dependency-management' version '1.1.6' id 'org.openapi.generator' version '7.7.0' }  group = 'com.example' version = '0.0.1-SNAPSHOT'  java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }  repositories { mavenCentral() }  dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' }  tasks.named('test') { useJUnitPlatform() }  tasks.named('compileJava') { dependsOn(tasks.openApiGenerate) }  sourceSets.main.java.srcDir "${buildDir}/generate-resources/main/src/main/java"  openApiGenerate { generatorName.set('java') configOptions.set([ library: 'restclient', openApiNullable: 'false' ]) inputSpec.set('petclinic-spec.yml') ignoreFileOverride.set(".openapi-generator-java-sources.ignore") invokerPackage.set('com.myapp') modelPackage.set('com.myapp.model') apiPackage.set('com.myapp.api') }

Настройка Spring-бинов

Сгенерированный код состоит из трех основных частей:

  1. ApiClient — низкоуровневый универсальный HTTP-клиент, который использует RestClient от Spring. Этот компонент не используется напрямую в нашем коде.

  2. PetApi, OwnerApi и другие — высокоуровневые клиенты, содержащие все операции для каждого OpenAPI тега.

  3. Модельные классы — классы, которые отображают JSON-запросы и ответы.

Ни один из этих классов не помечен Spring-аннотациями, такими как @Component или @Service, поэтому бины придётся создавать с помощью Java-конфигурации. 

Так как ApiClient использует RestClient, мы можем (и, скорее всего, должны) передавать автоматически сконфигурированный RestClient.Builder от Spring в конструктор ApiClient, чтобы воспользоваться преимуществами авто-конфигурации RestClient.

@Bean ApiClient apiClient(RestClient.Builder builder) {     var apiClient = new ApiClient(builder.build());     apiClient.setBasePath("http://localhost:9966/petclinic/api");     return apiClient; }  @Bean PetApi petApi(ApiClient apiClient) {     return new PetApi(apiClient); }

Устанавливаем базовый URL

Базовый URL можно установить как для RestClient с последующей передачей в ApiClient, так и напрямую для ApiClient через метод setBasePath. Более логичным с точки зрения Spring’а было бы установить URL для RestClient, но, к сожалению, этот вариант не будет работать. ApiClient не принимает базовый URL, установленный для RestClient, поэтому базовый путь должен быть установлен непосредственно для ApiClient.

Конфигурируем аутентификацию 

Если API требует аутентификации, возникает аналогичный вопрос: где именно производить её настройку. Ответ зависит от того, включает ли спецификация требования по аутентификации или нет.

Конфигурируем аутентификацию для ApiClient

Если файл API-спецификации содержит информацию об аутентификации, например, базовую аутентификацию:

openapi: 3.0.1 info:   title: Spring PetClinic # ... security:   - BasicAuth: [] # ... components:   securitySchemes:     BasicAuth:       type: http       scheme: basic # ...

То в этом случае ApiClient будет применять заголовки аутентификации только к тем операциям, которые требуют аутентификации:

@Bean ApiClient apiClient(RestClient.Builder builder) {     var apiClient = new ApiClient(builder.build());     apiClient.setUsername("admin");     apiClient.setPassword("admin");     apiClient.setBasePath("http://localhost:9966/petclinic/api");     return apiClient; }

Конфигурируем аутентификацию для RestClient

Если файл API спецификации не содержит информации об аутентификации, но API все равно требует аутентификации, то аутентификацию можно настроить на уровне RestClient:

@Bean ApiClient apiClient(RestClient.Builder builder) {     var restClient = builder             .requestInterceptor(new BasicAuthenticationInterceptor("admin", "admin"))             .build();     var apiClient = new ApiClient(restClient);     apiClient.setBasePath("http://localhost:9966/petclinic/api");     return apiClient; }

В таком случае заголовок аутентификации будет передаваться в ответ на все запросы, выполненные через ApiClient.

Плюсы и минусы

Наиболее очевидные преимущество генерации кода для клиента:

  • Не нужно писать код вручную, что экономит время и усилия

  • Автоматическая генерация кода уменьшает вероятность ошибок, таких как неправильные URL или некорректные данные 

  • Проще отслеживать изменения в API и адаптироваться к ним

Как обычно, существуют и некоторые недостатки — сгенерированный код абсолютно не похож на код, который я написал бы сам:

  • OpenAPI генерирует изменяемые (mutable) классы для запросов и ответов, хотя они должны быть неизменяемыми (immutable).

  • Генератор не использует Java records (на то есть причины и воркэраунд)

  • Все XXXApi классы имеют два метода для каждой операции: один возвращает класс представляющий данные, другой возвращает ResponseEntity<>. Это разумно для сгенерированного кода, но в тоже время загромождает API.

  • Это еще одна зависимость, которая может содержать баги, устареть или оказаться несовместимой с более новыми версиями Spring

Кроме того, начиная со Spring Framework 6, можно использовать Http Interface Clients, что значительно упрощает реализацию HTTP клиентов вручную.

Вывод

Хотя мне не очень нравится код, сгенерированный OpenAPI Generator, я считаю, что такой инструмент стоит иметь в арсенале. Вы можете найти полную версию проекта на GitHub, чтобы легко скопировать и вставить к себе его содержимое.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм — Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Ждем всех, присоединяйтесь


ссылка на оригинал статьи https://habr.com/ru/articles/833096/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *