インターフェース定義からSpringFrameworkのコードを自動生成 Part2

目次

始めに

インターフェース定義からSpringFrameworkのコードを自動生成することについて、前回は簡単なデモを行いましたが、こういう事ができるという紹介なだけで、実はあまり実用的ではなかったです。

例えば、前回はユーザー一覧を取得するだけでしたが、仮にほかのAPIを追加したいと思った場合

  • 定義yamlを修正
  • 再度ブランクプロジェクトを生成
  • 実装済みだった部分をコピペ

というのを繰り返すのはナンセンスです。特に最後の部分はいただけません。

ということで第二弾はもう少し進歩させてみましょう。

環境作成

使用するツールは前回と変わりません。引き続きSTSを使用します。

前回との違いとしては、OpenApiでブランクプロジェクトを作るのではなく、Spring Initializerでブランクプロジェクトを作成する点です。

ここではブランクプロジェクトから始めますが、今回ご紹介する方法であれば、すでに作成済みのプロジェクトでも自動生成を活用した開発が可能です。先にログ出力やユーティリティを先に設定してある基盤作成済みのプロジェクトにも導入可能ですので、使用の幅が広がると思います。

ビルドツールはgradleを使っていきます。

Spring Initializrで必要なものはSpringBootDev・Web。開発補助としてLombokです。

自動生成の設定~実行

さて早速設定していきます。

前回使用した定義yamlを使用しますので、プロジェクトルートに specs フォルダを作成し、格納しておきます。中身は前回から変えてません。

こちらはプロジェクト内に持っておきましょう。GitやSVNでまとめてバージョン管理できるメリットがあります。

※ちなみに、この時点で文字化けしていた場合は、ワークスペースの文字コードをUTF-8に設定する等しておくと良いです。

作成されているbuild.gradleにいろいろ追記します。

plugins {
	id 'org.springframework.boot' version '2.7.1'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	// api生成用のプラグイン
	id 'org.openapi.generator' version '4.3.1'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

ext {
	// 出力先を設定
	openApiOutputDir = "$rootDir/autogen"
}
// 出力したソースをビルドパスに追加
sourceSets.main.java.srcDirs += ["$openApiOutputDir/src/main/java"]

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	// 依存性を追加
	implementation 'org.openapitools:jackson-databind-nullable:0.2.1'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation group: 'org.springdoc', name: 'springdoc-openapi-ui', version: '1.6.9'
	compileOnly 'io.swagger:swagger-annotations:1.6.2'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

// openApiの設定を追加
openApiGenerate {
	generatorName = 'spring'
	inputSpec = "$rootDir/specs/openapi_demo.yaml"
	outputDir = "$openApiOutputDir"
	apiPackage = 'com.example.demo.api'
	modelPackage = 'com.example.demo.model'
	invokerPackage = "org.openapi.example.invoker"
	configOptions = [
		dateLibrary: 'java8',
		interfaceOnly: 'true',
		skipDefaultInterface: 'true'
	]
	systemProperties = [
		modelDocs: 'false'
	]
}

設定後は早速実行。以下のコマンドでソースを生成します。(もちろんGUI実行でもOKです)

gradlew openApiGenerate

STSのターミナルを使用するとこんな感じです。SUCCESSFUL となりました。

出力先は先ほどbuild.gradleで指定したディレクトリです。以下のような構成になったはずです。(うまく出なかったりエラーがある場合はリフレッシュやビルドなどを行ってください)

生成されたAPIを確認してみましょう。

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

(import略)
@javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-06-27T22:19:47.368861100+09:00[Asia/Tokyo]")

@Validated
@Api(value = "users", description = "the users API")
public interface UsersApi {

    /**
     * GET /users : ユーザー一覧取得
     * ユーザー一覧取得します。クエリパラメータに最大取得数を指定できます。
     *
     * @param max 最大取得数 (optional, default to 10)
     * @return OK (status code 200)
     */
    @ApiOperation(value = "ユーザー一覧取得", nickname = "getUsers", notes = "ユーザー一覧取得します。クエリパラメータに最大取得数を指定できます。", response = UserList.class, tags={  })
    @ApiResponses(value = { 
        @ApiResponse(code = 200, message = "OK", response = UserList.class) })
    @RequestMapping(value = "/users",
        produces = { "application/json" }, 
        method = RequestMethod.GET)
    ResponseEntity<UserList> getUsers(@ApiParam(value = "最大取得数", defaultValue = "10") @Valid @RequestParam(value = "max", required = false, defaultValue="10") Integer max);

}

interfaceOnly:ture と設定したため、前回の記事のAPIと違い default 実装がありません。それ以外は特に変化ないです。

実装~疎通確認

ここからが実装者のお仕事です。ベースとする自動生成されたインターフェースクラスには一切手を触れない状態で、継承した実装クラスにロジックを書いていきます。

いわゆるGapパターンというものですね。

UsersAPI実装としてUsersApiControllerを作っていきます。中身は前回と同じです。

package com.example.demo.api;

(import略)

@RestController
public class UsersApiController implements UsersApi {

	@Override
	public ResponseEntity<UserList> getUsers(@Valid 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;
	}
}

ポイントはアノテーションでRestControllerを設定したところです。

メインの実装を直したところで、application.properties の設定をします。ファイル自体はInitializrでresources配下に作成されているので、追記します。

server.port=8080

springdoc.api-docs.path=/api-docs
springdoc.swagger-ui.path=/swagger-ui.html

設定したら起動します。

疎通確認はcurlやブラウザでもよいのですが、実はちょっと便利UIを入れているので、以下にアクセスします。

http://localhost:8080/swagger-ui/index.html

Sweggerの画面ですね。

build.gradleに謎のライブラリ追加したり、application.properties に設定いれたりしたのはこのためです。パラメータや応答データがわかりやすくなると思います。

APIを選択し、右側の 「Try it out」から「Execute」を押すと

ちょっとスクロールした部分でレスポンスが入ってます。

ここまでは、まずは構築完了です。

修正対応

ここからが本題です。

仮に、以下のような仕様変更があったとします。

  • ユーザーのリストを一括で登録できるAPIが必要。
  • 応答で登録結果が確認できる。

まずは設計書であるyamlを修正します。

ここはGUIで直せるので、一旦こんな感じで直してみます。自分はStopLightを使用しました。

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: ユーザー一覧取得します。クエリパラメータに最大取得数を指定できます。
# ここから追記
    post:
      summary: ユーザー登録
      operationId: post-users
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ResultResponse'
              examples:
                正常終了:
                  value: 1
      description: ユーザのリストを新規登録します。登録結果を応答します。
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserList'
        description: 登録するユーザのリスト
    parameters: []

# ここまで追記
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'
# ここから追記
    ResultResponse:
      title: ResultResponse
      x-stoplight:
        id: rjge5xshdwsv9
      type: object
      x-examples:
        success:
          resultCode: 0
          message: 登録が正常に終了しました
      properties:
        resultCode:
          type: integer
        message:
          type: string
      description: 処理結果コードとメッセージ
# ここまで追記

そして再び自動生成してみましょう。

確認するとautogen配下は新たに生成されています。もともとこちらには実装段階で手を加えていないので、影響ありませんね。以下は定義したModelです。

再生成しても、UsersControllerは変更されていません。が、継承元のInterfaseにメソッドが増えたため、エラーが出ています。

今回、実装者はここを対応することとなります。

メソッドを追加します。

package com.example.demo.api;

(import略)

@RestController
public class UsersApiController implements UsersApi {

	@Override
	public ResponseEntity<UserList> getUsers(@Valid 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;
	}

	// 以下を追加
	@Override
	public ResponseEntity<ResultResponse> postUsers(@Valid UserList userList) {

		// 本来はDBに登録するイメージ
		userList.getUsers().forEach((user) -> System.out.println(user.toString()));

		ResultResponse result = new ResultResponse();
		result.setResultCode(0);
		result.setMessage("登録が正常に終了しました");

		return new ResponseEntity<ResultResponse>(result, HttpStatus.OK);
	}
}

疎通確認です。

サーバ再起動して、再び http://localhost:8080/swagger-ui/index.html にアクセスします。

PostのAPIが一つ増えています。

再び右側の 「Try it out」を押すと今回はリクエストデータを作成できます。

GUIの画面操作にした理由は、ここでポストデータを作るときに、先にテンプレートがあって便利だからです。

この程度の要素数ならば困りはしませんが、「画面のフォームに十数項目あって、それを一気にPostする」というのはよくあると思います。Jsonのキー名の間違いでうまくいかないなどのプチストレスが解消できます。

「Execute」を押して、レスポンスを確認します。

念のためサーバー側の出力をチェックします。

大丈夫そうですね。

こんな感じで、仕様が変化した場合、

定義を修正→コードを自動生成→対応するロジックを実装

というプロセスを回すことで継続的に開発が可能になります。

もちろん、単体テストのコードを用意しておいて、変更箇所以外は無影響であることを確認できれば完璧だと思います。

まとめ

今回はAPIを実用的なAPI自動生成について解説させていただきました。

アジャイル的な開発でも、ウォーターフォール開発でも、一回の設計でばっちり決まるようなものは少ないと思いますの変更に対するコストが低くできることは大きなメリットかなと思います。

フロント・バックで別チームが担当するとしても、yamlを一元管理しておけば、変更が頻繁であっても、それぞれ対応が取りやすいかなと思います。

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

関連記事

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

  2. 【Java】Maven, VSCode

  3. インターフェース定義からSpringFrameworkのコードを自動生…

  4. 【SpringBoot&MyBatis】SQLでJobPar…

最近の記事

  1. AWS
  2. AWS
  3. AWS
  4. flutter

制作実績一覧

  1. Checkeys