Magicode logo
Magicode
4
8 min read

[OpenAPI][Spring-boot] RESTfulなAPIを自動生成する

https://cdn.magicode.io/media/notebox/8e23dc14-9d8b-4064-9010-ebb9ee680f4c.jpeg

概要

  • WebAPIの設計書としてOpenAPI Specification(以下、OpenAPI Spec)を採用する
    • フロントエンド、バックエンド共にOpenAPI Specからソースコードを自動生成する
    • 本記事はバックエンドのみを対象とする
  • 自動生成したクラスをSpring-bootのRESTfulなAPIとして組み込む

前提条件

OpenAPI Spec

周辺ツールの対応状況を鑑みてv3.0.0を前提とする。

Docker

OpenAPI Specからソースコードを自動生成ツールとしてDocker版のOpenAPI Generatorを使用するため。 CLIやGradleプラグイン等あるが、グローバルな環境を汚さずにフロントエンド、バックエンド共に使用するツールを揃えられるのでDocker版を使用する。

Java

Spring-bootでのバックエンドの開発においてもはやインストールされていて当たり前なものなので割愛。 なおバージョンは執筆時点で安定版として息の長いJava 17を採用。

Spring-boot

3.0.0は執筆時点ではSNAPSHOTなのでv2系最新の2.7.2を前提とする。

事前準備

プロジェクト作成

Spring Initializrを使用し、適当なプロジェクトを作成。
初手で必要なライブラリは以下の画像を参照。

RESTfulなAPIの自動生成

OpenAPI Specは 公式のサンプル を使用。
ポイントは2つ。
  • 自動生成されるファイルの出力先をGradleのビルドディレクトリとする
    • 一般的にビルドディレクトリ(Gradleであれば build ディレクトリ)は.gitignore対象とするため、リポジトリの管理対象外となる
    • 自動生成されるファイルは手動での改変を許可せず、インプット(今回であればOpenAPI Spec)が同じであれば、いつ・誰が・どこで自動生成してもアウトプットが同じとなるため変更管理が不要なファイルのため
    • 自動生成されるファイルを改変したい場合はGeneration-Gapパターン等を用いて、自動生成されるファイルそのものは改変されない仕組みを考えるべき
  • Javaのソースコードディレクトリを gen-src/main/java とする
    • 自動生成されたソースコードであることを明示的に分かりやすくするために変更する
    • 後述するbuild.gradleの設定によってコンパイル対象として検出させる
docker run --rm \
  -v "${PWD}/build/openapi:/out" \
  openapitools/openapi-generator-cli:v6.0.1 generate \
  --generator-name spring \
  --input-spec https://raw.githubusercontent.com/openapitools/openapi-generator/master/modules/openapi-generator/src/test/resources/3_0/petstore.yaml \
  --output /out \
  --additional-properties hideGenerationTimestamp=true \
  --additional-properties sourceFolder=gen-src/main/java \
  --additional-properties basePackage=com.sliber_kugel.openapi \
  --additional-properties apiPackage=com.sliber_kugel.openapi.adapter.controller \
  --additional-properties modelPackage=com.sliber_kugel.openapi.domain.model \
  --additional-properties skipDefaultInterface=true \
  --additional-properties oas3=true \
  --additional-properties dateLibrary=java8 \
  --additional-properties delegatePattern=false \
  --additional-properties interfaceOnly=true \
  --additional-properties openApiNullable=false \
  --additional-properties useTags=true \
  --additional-properties disallowAdditionalPropertiesIfNotPresent=false \
  --additional-properties useBeanValidation=true \
  --additional-properties serializableModel=true

build.gradleの修正

OpenAPI Generatorによって自動生成されたソースコードをコンパイルするのに必要なライブラリの依存関係への追加とコンパイル対象ディレクトリの追加を行う。
なお jakarta.validation-api については次のIssueが報告されており、最新のv3系の場合はコンパイルが出来ないためv2系の最新として2.0.2としている。
https://github.com/OpenAPITools/openapi-generator/issues/12603
// OpenAPI Generatorによって自動生成されるディレクトリをコンパイル対象として追加
sourceSets {
    main {
        java {
            srcDirs = ['src/main/java', "${buildDir}/openapi/gen-src/main/java"]
        }
    }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // 以下2つを追加
    implementation 'io.swagger.core.v3:swagger-annotations:2.2.2'
    implementation 'jakarta.validation:jakarta.validation-api:2.0.2'
}

本題

RestControllerの実装

ポイントは2つ。
  • 自動生成されたインターフェースを implements する
    • RESTfulなAPIのインターフェースが自動生成されているので実装クラスを実装する
  • @RestController アノテーションを付与する
    • 自動生成されたインターフェースをSpringのRESTfulなAPIとして認識させる必要があるためアノテーションを付与
package com.sliber_kugel.openapi.adapter.controller;

import com.sliber_kugel.openapi.domain.model.ModelApiResponse;
import com.sliber_kugel.openapi.domain.model.Pet;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;

@RestController
public class PetController implements PetApi {

    @Override
    public ResponseEntity<Pet> addPet(Pet pet) {
        // some implementation
        throw new UnsupportedOperationException();
    }

    @Override
    public ResponseEntity<Void> deletePet(Long petId, String apiKey) {
        // some implementation
        throw new UnsupportedOperationException();
    }

    @Override
    public ResponseEntity<List<Pet>> findPetsByStatus(List<String> status) {
        // some implementation
        throw new UnsupportedOperationException();
    }

    @Override
    public ResponseEntity<List<Pet>> findPetsByTags(List<String> tags) {
        // some implementation
        throw new UnsupportedOperationException();
    }

    @Override
    public ResponseEntity<Pet> getPetById(Long petId) {
        // some implementation
        throw new UnsupportedOperationException();
    }

    @Override
    public ResponseEntity<Pet> updatePet(Pet pet) {
        // some implementation
        throw new UnsupportedOperationException();
    }

    @Override
    public ResponseEntity<Void> updatePetWithForm(Long petId, String name, String status) {
        // some implementation
        throw new UnsupportedOperationException();
    }

    @Override
    public ResponseEntity<ModelApiResponse> uploadFile(Long petId, String additionalMetadata, MultipartFile file) {
        // some implementation
        throw new UnsupportedOperationException();
    }
}

Discussion

コメントにはログインが必要です。