見出し画像

Spring Boot+OpenAPI+Gradleで始めるJavaのスキーマ駆動Web API開発

今回はOpenAPIを使った開発手法として、Spring Boot(Java)でスキーマ駆動Web API開発を行う流れをご紹介します。

今回の構成

  • フレームワークとしてSpringBoot

  • Java 17

  • ビルドツールはGradle

Spring Boot

Spring Bootとは

Spring Bootは、Javaプログラミング言語を使用してWebアプリケーションやマイクロサービスを開発するためのフレームワークです。Spring Bootは、JavaのWebアプリケーションフレームワークとして著名なSpring Frameworkの一部であり、Springのコンポーネントや機能を簡素化し、開発者に対してより迅速なアプリケーション開発を可能にするために作られています。

公式ドキュメントからその機能を引用します。

・スタンドアロン Spring アプリケーションの作成
・Tomcat、Jetty、Undertow の直接組み込み (WAR ファイルのデプロイ不要)
・標準的な「スターター」依存関係の提供によりビルド構成を簡素化
・Spring とサードパーティのライブラリを可能な限り自動的に構成
・メトリクス、ヘルスチェック、環境別設定切り替えなどの本番対応機能を提供
・コード生成や XML 設定は一切不要

https://spring.pleiades.io/projects/spring-boot

つまり、Spring BootはSpring Frameworkの上に構築されたメタフレームワークということになります。

Spring Bootをはじめる

では、Spring Bootのプロジェクトを作っていきましょう。

Spring InitializrというWebサービスで必要事項を指定していくだけで、簡単にプロジェクトの雛型をダウンロードできます。
今回は以下の設定でプロジェクトを作ります。

  • Project:Gradle - Groovy

  • Language:Java

  • Spring Boot:3.2.0

  • Project Metadata:デフォルト値のままでOK(PackagingはJar、Javaは17)

  • Dependencies:LombokとSpring Webを追加

このURLを開くと、上記のプロジェクト設定がロードされます。

ページ下部の「EXPLORE」ボタンを押下すると、プロジェクトの内容をプレビューできます。
ソースコード、Gradle関係のファイル、gitigoreなどが生成されることがわかります。

「DOWNLOAD」ボタンを押下でダウンロードされたzipファイルを展開します。
任意のIDEにインポートしてプロジェクトを開きましょう。
ここではIntellij IDEAを使います。
以後、プロジェクトのルートフォルダーを<PROJ_ROOT>と表記します。

お馴染みのHello Worldを実装してみましょう。
<PROJ_ROOT>\src\main\java\com\example\demo\DemoApplication.javaを開き、メソッドhelloを追加します。
合わせて、クラスDemoApplication.javaにアノテーション@RestControllerをつけます。

@SpringBootApplication
@RestController
public class DemoApplication {

    ・・・

	@GetMapping("/hello")
	public String hello(@RequestParam(value = "name", defaultValue = "World") String name) {
		return String.format("Hello %s!", name);
	}
}

gradlew bootRunを実行します。
アプリケーションが起動したら、ブラウザからhttp://localhost:8080/helloにアクセスします。
ブラウザにHello World!が表示されれば成功です。

たったこれだけでWeb APIの実装ができましたね。

Spring Bootアプリケーションの公開

通常はこの後warにパッケージングして、Tomcatなどのサーブレットコンテナにデプロイして初めてWeb APIのアプリケーションとして公開できます。
Spring Bootの凄いところは、ビルドしたjarを実行するだけでWeb APIのアプリケーションができてしまうことです。

gradlew bootJarを実行して生成されたjarファイルをjavaコマンドで実行してみましょう。
先程と同じようにブラウザからアクセスするとHello World!が表示されていると思います。
高速なアプリケーション開発ができる事がお分かりになりましたでしょうか?

OpenAPIのスキーマ定義を追加

それでは、ここにOpenAPIのスキーマ定義を持ち込んできましょう。
今回はOpenAPIのペットショップの例を使用します。
https://github.com/OAI/OpenAPI-Specification/blob/main/examples/v3.0/petstore.yaml
<PROJ_ROOT>\specsフォルダーを作成して、このyamlをダウンロード、配置します。

コードの自動生成

OpenAPI Generatorを使うと、OpenAPIスキーマファイルからクライアントやサーバーのソースコード、ドキュメントが生成できます。

以下のページに現在対応されている生成先が列挙されています。
サーバーのJavaだけに着目しても、SpringだけではなくPlay FrameworkやJAXRSなど様々なフレームワーク向けのソースコードが生成できることがわかります。

今回は行いませんが、クライアントとしてTypescript系のソースコード生成もできるので、Spring Boot+React/Typescriptのような構成で作ることもできそうですね。また、クライアントとしてJMeterのjmxファイル生成もできるので、JMeterを使った負荷テストとかにも活用できそうです。

自動生成する手段は複数ありますが、今回はビルドツールとしてGradleを使用するので、Gradleから操作を行えるOpenAPI Generator Gradle Pluginを使用します。

<PROJ_ROOT>\build.gradleに設定を追加していきましょう。
まずはpluginsにorg.openapi.generatorを追加します。

plugins {
    ・・・
	id "org.openapi.generator" version "6.6.0"
}

続いてbuild.gradleの末尾にプラグインの設定を追加します。

openApiGenerate {
	generatorName = "spring"
	inputSpec = "$rootDir/specs/petstore.yaml".toString()
	outputDir = layout.buildDirectory.dir("generated").get().asFile.path
	apiPackage = "com.example.demo.api"
	invokerPackage = "com.example.demo.invoker"
	modelPackage = "com.example.demo.model"
	generateModelTests = false
	generateApiTests = false
	generateModelDocumentation = false
	generateApiDocumentation = false
	// https://openapi-generator.tech/docs/generators/spring
	configOptions = [
			dataLibrary          : "java8",
			documentationProvider: "springdoc",
			interfaceOnly        : "true",
			skipDefaultInterface : "true",
			useSpringBoot3 : "true",
			useJakartaEe : "true",
	]
}

compileJava.dependsOn tasks.openApiGenerate

sourceSets {
	main {
		java {
			srcDir "${openApiGenerate.outputDir.get()}/src/main/java"
		}
	}
}

自動生成したファイルは、Generation Gapパターンで取り扱います。
自動生成したファイルを直接編集するのではなく、インターフェースを自動生成してそれをimplementsするクラスを作ります。

このパターンを強制するために、自動生成したファイル群は<PROJ_ROOT>/build/generated配下に配置して、gitなどのバージョン管理対象外にします。
また、ビルド時にはソースコードが参照できるようにsourceSetsの設定、およびJavaコンパイル前に必ず自動生成を行うためにタスクの依存関係を設定します。

では、gradlew openApiGenerateで自動生成してみましょう。

PetsApi.javaというインターフェースクラスが生成されました。
ただ、見つからない依存ライブラリがあるため、ビルドができないようです。

自動生成されたコードの依存ライブラリ解決

依存ライブラリが見つからないのは、Mavenプロジェクトとして自動生成されているからです。
今回は、Mavanの設定ファイルpom.xmlから必要なライブラリをbuild.gradleにコピーすることでこの問題の解決を図ります。

<PROJ_ROOT>\build.gradleに以下を追加します。

dependencies {
	・・・

	// OpenAPI Generatorの生成コードで必要なライブラリ
	// 生成されたpom.xmlから抽出した
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation 'com.fasterxml.jackson.core:jackson-annotations'
	implementation 'com.fasterxml.jackson.core:jackson-databind'
	implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
	implementation 'org.openapitools:jackson-databind-nullable:0.2.6'
	implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
}

ここで、spring-boot-starter-validationとJackson関係のライブラリはSpring Bootがライブラリバージョンの管理を行っているため、バージョンを明示する必要がないことに注意してください。
参考までに、Spring Bootが管理している依存関係とそのバージョンは以下から確認することができます。

IDEからGradleプロジェクトをリロードすると、先ほどのビルドエラーは解決しているはずです。
gradlew buildでビルドができることを確認しておきましょう。

APIを実装する

あとはAPIを実装していくだけです。
今回は/pets/{petId}だけをサンプル実装します。

<PROJ_ROOT>\src\main\java\com\example\demo\api\PetStoreException.javaと<PROJ_ROOT>\src\main\java\com\example\demo\api\PetsController.javaを新規追加します。

package com.example.demo.api;

import com.example.demo.model.Error;
import lombok.Getter;

@Getter
public class PetStoreException extends RuntimeException {
    private final int code;

    public PetStoreException(int code, String msg) {
        super(msg);
        this.code = code;
    }

    public Error toError() {
        return new Error(code, getMessage());
    }
}
package com.example.demo.api;

import com.example.demo.model.Error;
import com.example.demo.model.Pet;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;
import java.util.Map;
import java.util.Objects;

@RestController
public class PetsController implements PetsApi {
    private static final Map<Long, Pet> PETS = Map.of(
            1L, new Pet(1L, "Dog"),
            2L, new Pet(2L, "Cat"),
            3L, new Pet(3L, "Hamster")
    );

    @Override
    public ResponseEntity<Void> createPets() {
        return null;
    }

    @Override
    public ResponseEntity<List<Pet>> listPets(Integer limit) {
        return null;
    }

    @Override
    public ResponseEntity<Pet> showPetById(String petId) {
        Long id;
        try {
            id = Long.parseLong(petId);
        } catch (NumberFormatException e) {
            throw new PetStoreException(400, "Invalid ID supplied");
        }

        Pet pet = PETS.get(id);
        if (Objects.isNull(pet)) {
            throw new PetStoreException(404, "Pet not found");
        }
        return ResponseEntity.ok(pet);
    }

    @ExceptionHandler(PetStoreException.class)
    public ResponseEntity<Error> handleException(PetStoreException e) {
        return ResponseEntity.status(e.getCode()).body(e.toError());
    }
}

gradlew bootRunを実行します。
アプリケーションが起動したら、ブラウザからhttp://localhost:8080/pets/1にアクセスしてレスポンスが返ってくることを確認しましょう。

あくまでサンプル実装なので実装内容はさておき、このような流れで他のAPIも実装を進めればよさそうですね。

まとめ

Spring Boot+OpenAPI+Gradleという組み合わせで行う、Javaのスキーマ駆動Web API開発の流れを説明しました。
サーバーエンドをJavaで実装する機会はまだまだ多いと思います。この記事が参考になれば幸いです。

以前のエンジニアテックブログでも、TakさんがOpenAPIに関する記事を書かれています。そこでは、FastAPI(Python)のソースコードからコードファーストでOpenAPIのスキーマファイルを定義するやり方が紹介されていますので、ぜひこちらも合わせてご覧ください。

この記事が気に入ったらサポートをしてみませんか?