7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

springdoc-openapiを利用したAPIドキュメント自動生成、自動生成されたドキュメントからのAPIクライアントコード自動生成

Last updated at Posted at 2023-02-20
  • 本検証で利用した製品
    Spring Boot v3.0.2

APIドキュメントを先に書くのか、先にコードを書くのか?について色々と意見あります。
個人的にはドキュメントを書いて、整合性を保ったコードを書くのが現実的に難しいので、先にJava側で設計を固めてからの自動生成アプローチがよいと思っています。

  • RESTコントローラの作成
    文字列"HelloWorld"を返すだけの簡単なコントローラ
package com.example.demo;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;


@RestController
@RequestMapping(path="/")
public class TestController {


    @RequestMapping(path="/hello", method=RequestMethod.GET)
    public String hello() {
        return "HelloWorld";
    }

}

ブラウザで実行すると下記のように表示される。
image.png

springdocのセットアップ
pom.xmlを下記のように編集。
dependencyを追加。

<dependency>
	<groupId>org.springdoc</groupId>
	<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
	<version>2.0.2</version>
</dependency>

追加後のpom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.0.2</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springdoc</groupId>
			<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
			<version>2.0.2</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

application.properties

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

再起動後、上記で設定したパスでドキュメントをダウンロードしたり、ブラウザで見えるようになる。
HTMLで成形されたドキュメントも提供されており、この画面でAPIのテストができるようになっている。
「Try it out」ボタンがテスト用。

image.png

Try it outの画面例
image.png

api-docの参照例
image.png

以上がコードからAPIドキュメント自動生成のやり方。
pom.xmlに4行追加し、application.propertiesに2行追加しただけです。

次に、生成されたAPIドキュメントからAPIクライアントのソースコード自動生成のやり方。
openapi-generator-cli-xxx.jarをダウンロードし、javaコマンド一つでソースコードが自動生成される。

java -jar openapi-generator-cli-6.3.0.jar generate -i ./api-docs.yaml --api-package com.example.demo --model-package com.example.demo --invoker-package com.example.demo  --group-id com.example --artifact-id demo --artifact-version 0.0.1-SNAPSHOT -g java -p java8=true  --library resttemplate -o spring-openapi-generator-api-client

自動生成されたコードにはUNITテスト用のコードも含まれていた。
srcフォルダ内のソース一式をAPIクライアント用プロジェクトのsrcフォルダへコピーする。

APIをコールする側のソースコード

package com.example.demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import lombok.RequiredArgsConstructor;

@RestController
@RequestMapping(path="/")
@RequiredArgsConstructor
public class TestController {

	
	@Autowired
	private TestControllerApi testApi;

    @RequestMapping(path="/apiclient", method=RequestMethod.GET)
    public String hello() {
        String result = testApi.hello();
        return result;
    }
    
    @Bean
	public RestTemplate restTemplate() {
	    return new RestTemplate();
	}

}

image.png

上記のようにAPIで返すHelloWorldをそのまま表示できた。
APIとAPIクライアントで異なるポートで起動するためのSpring Bootの設定
application.properties

server.port=8081

APIクライアント側のpom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>3.0.2</version>
		<relativePath /> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>apiClient</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>apiClient</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>17</java.version>
	</properties>
	<url>https://github.com/openapitools/openapi-generator</url>
	<scm>
		<connection>scm:git:git@github.com:openapitools/openapi-generator.git</connection>
		<developerConnection>scm:git:git@github.com:openapitools/openapi-generator.git</developerConnection>
		<url>https://github.com/openapitools/openapi-generator</url>
	</scm>



	<dependencies>






		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>



		<dependency>
			<groupId>com.example</groupId>
			<artifactId>demo</artifactId>
			<version>0.0.1-SNAPSHOT</version>
			<scope>compile</scope>
		</dependency>



		<dependency>
			<groupId>com.google.code.findbugs</groupId>
			<artifactId>jsr305</artifactId>
			<version>3.0.2</version>
		</dependency>

		<!-- HTTP client: Spring RestTemplate -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
		</dependency>

		<!-- JSON processing: jackson -->
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-core</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-annotations</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.core</groupId>
			<artifactId>jackson-databind</artifactId>
		</dependency>
		<dependency>
			<groupId>com.fasterxml.jackson.jaxrs</groupId>
			<artifactId>jackson-jaxrs-json-provider</artifactId>
		</dependency>
		<dependency>
			<groupId>org.openapitools</groupId>
			<artifactId>jackson-databind-nullable</artifactId>
			<version>0.2.5</version>
		</dependency>

		<dependency>
			<groupId>org.openapitools</groupId>
			<artifactId>openapi-generator-core</artifactId>
			<version>6.3.0</version>
		</dependency>


		<dependency>
			<groupId>com.fasterxml.jackson.datatype</groupId>
			<artifactId>jackson-datatype-jsr310</artifactId>
		</dependency>
		<dependency>
			<groupId>jakarta.annotation</groupId>
			<artifactId>jakarta.annotation-api</artifactId>
			<scope>provided</scope>
		</dependency>

		<!-- test dependencies -->
		<dependency>
			<groupId>junit</groupId>
			<artifactId>junit</artifactId>
			<scope>test</scope>
		</dependency>


		<dependency>
			<groupId>javax.annotation</groupId>
			<artifactId>javax.annotation-api</artifactId>
			<version>1.3.2</version>
		</dependency>


	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

@Configurationの作成

package com.example.demo;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

@Configuration
public class TestIntegrationConfig {

    @Bean
    @Primary
    public TestControllerApi petApi() {
        return new TestControllerApi(apiClient());
    }
    
    @Bean
    public ApiClient apiClient() {
        return new ApiClient();
    }
}

Spring BootのMainクラス

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import(TestIntegrationConfig.class)
public class ApiClientApplication {

	public static void main(String[] args) {
		SpringApplication.run(ApiClientApplication.class, args);
	}

}

ここまでの手順にたどり着くまでに相応の時間を要したが、2つ目以降のAPIについては

■APIの作成(業務ロジックの複雑度による)
■APIドキュメントの作成(一瞬でコードとの乖離のないものが完成)
■コードgeneratorの実行と生成されたコードのコピー(数分)
@Configurationの作成(数分)
■API呼び出し部分のコード(method呼び出しだけなので数分)

実案件ではもっとスマートなやり方を確立するのでAPIをコールしたり、コンポジットする部分ただのmethod呼び出しであるため、API作成やテストに要する時間に対して誤差程度のものと評価できる。
某商用製品のようにGUIでポチポチやるよりも生産性は高い。

最後におまけ

API側のRESTコントローラの負荷試験結果(18159TPS)
image.png

APIクライアントからAPI呼び出し一回の負荷試験結果(5401TPS)
image.png

API呼び出し3回にしたときの負荷試験結果(2873TPS)
image.png

某商用製品の推奨する3階層アーキテクチャどおりに実装すると性能がでなくなるリスクがある。CPUを足せばコストにはねるので注意したい。

おまけ(少し複雑なケース)

ソース

package com.example.demo;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(path = "/")
public class TestController {

    @RequestMapping(path = "/hello", method = RequestMethod.GET)
    public String hello() {
        return "Hello!";
    }

    @GetMapping("/hello/{name}")
    public String hello(@PathVariable String name) {
        return "Hello! " + name;
    }

    @RequestMapping(path = "/array", method = RequestMethod.GET)
    public String[] array() {
        String[] result = new String[2];
        result[0] = "aaaa";
        result[1] = "bbbb";
        return result;
    }

    @RequestMapping(path = "/arrayvo", method = RequestMethod.GET)
    public TestVO[] arrayvo() {
        TestVO[] result = new TestVO[2];
        TestVO testVO1 = new TestVO();
        testVO1.setId("id1");
        testVO1.setName("name1");
        TestVO testVO2 = new TestVO();
        testVO2.setId("id2");
        testVO2.setName("name2");
        result[0] = testVO1;
        result[1] = testVO2;
        return result;
    }

}

image.png

api-docs

{
    "openapi": "3.0.1",
    "info": {
        "title": "OpenAPI definition",
        "version": "v0"
    },
    "servers": [
        {
            "url": "http://localhost:8080",
            "description": "Generated server url"
        }
    ],
    "paths": {
        "/hello": {
            "get": {
                "tags": [
                    "test-controller"
                ],
                "operationId": "hello",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "*/*": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/hello/{name}": {
            "get": {
                "tags": [
                    "test-controller"
                ],
                "operationId": "hello_1",
                "parameters": [
                    {
                        "name": "name",
                        "in": "path",
                        "required": true,
                        "schema": {
                            "type": "string"
                        }
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "*/*": {
                                "schema": {
                                    "type": "string"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/arrayvo": {
            "get": {
                "tags": [
                    "test-controller"
                ],
                "operationId": "arrayvo",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "*/*": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "$ref": "#/components/schemas/TestVO"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        },
        "/array": {
            "get": {
                "tags": [
                    "test-controller"
                ],
                "operationId": "array",
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "*/*": {
                                "schema": {
                                    "type": "array",
                                    "items": {
                                        "type": "string"
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "TestVO": {
                "type": "object",
                "properties": {
                    "id": {
                        "type": "string"
                    },
                    "name": {
                        "type": "string"
                    }
                }
            }
        }
    }
}

image.png

image.png

JUnitのテストコード自動生成

/*
 * OpenAPI definition
 * No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)
 *
 * The version of the OpenAPI document: v0
 * 
 *
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
 * https://openapi-generator.tech
 * Do not edit the class manually.
 */

package com.example.demo;

import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.web.client.RestClientException;

import static org.junit.Assert.assertEquals;

import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * API tests for TestControllerApi
 */
class TestControllerApiTest {

    private final TestControllerApi api = new TestControllerApi();

    /**
     * 
     *
     * 
     *
     * @throws RestClientException
     *                             if the Api call fails
     */
    @Test
    void helloTest() {

        String response = api.hello();
        assertEquals("Hello!", response);
    }

}

7
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?