インターフェース定義からSpringFrameworkのコードを自動生成してAPIサーバの実装

始めに

初めまして、isubの中野と申します。主にjavaで開発を行っています。

最近のシステム構築の手法としては、バックエンドはAPIサーバーがあり、フロントからはURLを叩くことでデータを取得するというのが多いのではないかと思います。最初にインターフェース仕様を定義して、それを元にフロント・バックエンドが別々に開発するという形です。
私が参画している現場でもそのような開発を行うということで、OpenAPIというAPIの仕様について学習する機会がありました。

その中でインターフェースの定義からJava-SpringFrameworkのコードを自動生成できることを知ったので、内容をまとめました。
 ※OpenAPIとは?→APIの仕様。デファクトスタンダードらしいです。わかりやすい説明→https://qiita.com/teinen_qiita/items/e440ca7b1b52ec918f1b

まとめたこと

①インターフェースの定義からコードを自動生成する。
→OpenAPI Generatorを使用し、SpringFrameworkプロジェクトを作成する。

②自動生成されたプロジェクトを起動する。
→STSを使用し自動生成されたプロジェクトをAPIサーバとして起動する。

③ロジックを実装する。
→ロジックを実装し、APIの出力を確認する。

使用したツール

・OpenAPI Generator(自動生成ツール)
 https://github.com/OpenAPITools/openapi-generator 
 ※今回はJavaのコードを作成するつもりなので、Javaの実行環境がある状態を前提としています。Java環境がない場合、Dockerでも実行可能なようです。
・STS(SpringFramewrok総合開発環境)
 https://spring.io/tools

やってみよう!

前準備

今回はインターフェースの仕様をOpenAPIとし、yamlで作成しました。
ゼロからyamlですべて書くのはハードルが高いので、StopLightというGUIエディタで作成しています。
よくある(?)ユーザ一覧を取得するようなAPIを想定しています。エンドポイントを叩くと「ユーザのid・苗字・名前をJsonで取得する」という簡単なものです。取得数はデフォルトで10人分として、クエリパラメータで設定する出来るようにしています。

openapi_demo.yaml

openapi: 3.0.0
info:
  title: openapi_demo
  version: '1.0'
  description: OpenAPIのデモ
servers:
  - url: 'http://localhost:8080'
paths:
  /users:
    get:
      summary: ユーザー一覧取得
      tags: []
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/UserList'
      operationId: get-users
      parameters:
        - schema:
            type: integer
            default: 10
          in: query
          name: max
          description: 最大取得数
      description: ユーザー一覧取得します。クエリパラメータに最大取得数を指定できます。
components:
  schemas:
    User:
      title: User
      description: ユーザ
      type: object
      properties:
        id:
          type: integer
        familyName:
          type: string
        givenName:
          type: string
    UserList:
      title: UserList
      description: ユーザリスト
      type: object
      properties:
        users:
          type: array
          items:
            $ref: '#/components/schemas/User'

①インターフェースの定義からコードを自動生成する。

作成した定義ファイルからCLIでSpringFrameworkのプロジェクトを自動生成します。

作成した定義ファイルと同じ階層にダウンロードしたOpenAPI Generatorのjarを配置した状態で、以下のコマンドを実行します。OpenAPI Generatorは5.4.0版を使用しています。

java -jar openapi-generator-cli-5.4.0.jar generate ^
-i ./openapi_demo.yaml ^
-g spring ^
-o openapi_demo ^
--group-id com.example ^
--artifact-id sample-api-generated ^
--artifact-version 0.0.1-SNAPSHOT ^
--api-package com.example.api.api ^
--model-package com.example.api.model

いろいろなオプションが入っていますが、基本の部分は以下の3点です。

オプション内容
-i定義ファイルのパス
-g出力するソースの言語/FW。
今回はSpringを指定していますが、対応する言語/FWはたくさんあります。
 → https://xn--openapopenapi-generator-du3v.tech/docs/generators/
-o出力先ディレクトリのパス

実行すると -o で指定したフォルダにソースが作成されます。

自動生成されたsrc配下は以下のようになっています。

src
└─main
    ├─java
    │  ├─com
    │  │  └─example
    │  │      └─api
    │  │          ├─api
    │  │          │      ApiUtil.java
    │  │          │      UsersApi.java
    │  │          │      UsersApiController.java  ←開発者がロジックを実装するクラス
    │  │          │      
    │  │          └─model
    │  │                  User.java
    │  │                  UserList.java
    │  │                  
    │  └─org
    │      └─openapitools
    │          │  OpenAPI2SpringBoot.java
    │          │  RFC3339DateFormat.java
    │          │  
    │          └─configuration
    │                  HomeController.java
    │                  
    └─resources
            application.properties
            openapi.yaml

②自動生成されたプロジェクトを起動する。

出力されたソースをSTSに取り込み、実行してみます。

「プロジェクトをインポート…」をクリック

「既存のMavenプロジェクト」を選択し、「次へ(N) >」をクリック

先ほど出力したプロジェクトをルート・ディレクトリに設定して「完了 (F)」をクリック

STSによりプロジェクトのビルドがかかり、ソースが展開されます。(ちょっと時間かかるかも)

SprinBootアプリケーションとして実行します。

起動したら、ブラウザで「 http://localhost:8080/ 」にアクセスしてみます。

早速かっこいい画面が出てきました。

これは定義ファイルから自動でDocを起こしてくれるためです。

さらに、画面上で「Get-users」部分トグルを開き、右の方に「Try it out」とあるのでクリックします。

そして、Executeとクリックすると下部に実行結果が出てきます。

今はまだ何もロジックを実装していないので、コード501「Not Implemented」、つまり「実装されてないよ」って怒られます。

実装されてないのになぜデータが返ってくるかというと、インターフェースである UsersApi.java クラスでdefaultメソッドが定義されているためです。

ソースを見てみましょう。

com.example.api.api.UsersApi.java
インターフェースの受け口はこのクラスです。501を返したのはこの中のdefault実装部分となります。

/**
 * NOTE: This class is auto generated by OpenAPI Generator (<https://openapi-generator.tech>) (5.4.0).
 * <https://openapi-generator.tech>
 * Do not edit the class manually.
 */
package com.example.api.api;

// (importを略)

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-05-15T13:50:28.660241+09:00[Asia/Tokyo]")
@Validated
@Tag(name = "users", description = "the users API")
public interface UsersApi {

    default Optional<NativeWebRequest> getRequest() {
        return Optional.empty();
    }

    /**
     * GET /users : ユーザー一覧取得
     * ユーザー一覧取得します。クエリパラメータに最大取得数を指定できます。
     *
     * @param max 最大取得数 (optional)
     * @return OK (status code 200)
     */
    @Operation(
        operationId = "getUsers",
        summary = "ユーザー一覧取得",
        responses = {
            @ApiResponse(responseCode = "200", description = "OK", content = @Content(mediaType = "application/json", schema = @Schema(implementation =  UserList.class)))
        }
    )
    @RequestMapping(
        method = RequestMethod.GET,
        value = "/users",
        produces = { "application/json" }
    )
    default ResponseEntity<UserList> getUsers(
        @Parameter(name = "max", description = "最大取得数", schema = @Schema(description = "")) @Valid @RequestParam(value = "max", required = false, defaultValue = "10") Integer max
    ) {
        getRequest().ifPresent(request -> {
            for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
                if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
                    String exampleString = "{ \\"users\\" : [ { \\"familyName\\" : \\"familyName\\", \\"givenName\\" : \\"givenName\\", \\"id\\" : 0 }, { \\"familyName\\" : \\"familyName\\", \\"givenName\\" : \\"givenName\\", \\"id\\" : 0 } ] }";
                    ApiUtil.setExampleResponse(request, "application/json", exampleString);
                    break;
                }
            }
        });
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

}

③ロジックを実装する。

UserApiの実装クラスである UsersApiController.java にロジックを追記します。
抽象クラスのUserApiのメソッドをオーバーライドして、DBから指定件数のレコードをセレクトするイメージのダミーデータを取得します。

package com.example.api.api;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.context.request.NativeWebRequest;

import com.example.api.model.User;
import com.example.api.model.UserList;

import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.Schema;

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import javax.annotation.Generated;
import javax.validation.Valid;

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-05-15T13:50:28.660241+09:00[Asia/Tokyo]")
@Controller
@RequestMapping("${openapi.openapiDemo.base-path:}")
public class UsersApiController implements UsersApi {

	private final NativeWebRequest request;

	@Autowired
	public UsersApiController(NativeWebRequest request) {
		this.request = request;
	}

	@Override
	public Optional<NativeWebRequest> getRequest() {
		return Optional.ofNullable(request);
	}

  // 以下を追記
	@Override
	public ResponseEntity<UserList> getUsers(
			@Parameter(name = "max", description = "最大取得数", schema = @Schema(description = "")) @Valid @RequestParam(value = "max", required = false, defaultValue = "10") Integer max) {

		// 本来はDBにselectのクエリを投げるイメージ
		List<User> users = selectUsers(max);
		UserList userList = new UserList();
		userList.users(users);
		return new ResponseEntity<UserList>(userList, HttpStatus.OK);
	}

	private List<User> selectUsers(int count) {
		List<User> users = new ArrayList<User>();
		for (int i = 0; i < count; i++) {
			User user = new User();
			user.setId(i);
			user.setFamilyName("familyName : " + i);
			user.setGivenName("givenName : " + i);
			users.add(user);
		}
		return users;
	}
}

作成後、サーバーを再起動してブラウザからもう一度リクエストを投げてみると、

ステータス200となり、Response bodyにユーザー情報が入ったJsonが出力されました。

まとめ

インターフェースの定義から自動生成しAPIサーバーの実装まで行いました。

自動生成によるメリットとしては、

  • 定義から直接起こすので項目の過不足がない。
  • 実装者がロジック作成に専念できる。
  • 記述量が減る。

といった点があると思います。

さらに、今回は触れていませんが、OpenAPIの仕様に沿って定義を作成しておくことで、Mockサーバーが簡単に作成できるというものもあります。
ただ、今回はトライアルとしてかなり簡素なものですので恩恵が少ないですし、この方法だと仕様変更でインターフェースの定義が変わった場合、対応が大変というデメリットがあります。

次回はそのあたりを対応して、まとめたいと思います。

以上です。ありがとうございました。

関連記事

  1. 【Java】Maven, VSCode

  2. 【SpringBoot】MavenとVSCodeでAPI開発(Json…

最近の記事

  1. flutter
  2. flutter
  3. flutter

制作実績一覧

  1. Checkeys