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

* 本検証で利用した製品
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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/6dfea780-3b2e-f81d-2133-29fbd524a3c6.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/fb45150e-3024-ccdb-cb21-c9f697859ee1.png)

Try it outの画面例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/349f7746-2055-d1af-cee1-495f53881c4b.png)

api-docの参照例
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/101cbd2f-d5ab-680f-d925-1c398aa3613f.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/b56cf728-7241-6b05-72f0-c5825d1db5c6.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/7f04f91c-d116-ab1a-720e-46656fe4728a.png)

APIクライアントからAPI呼び出し一回の負荷試験結果（5401TPS）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/be6b91ec-63be-6912-ba8e-6ead1dfa71fe.png)

API呼び出し３回にしたときの負荷試験結果（2873TPS）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/0931269d-3788-6ef9-7d3f-feaf1c8039d5.png)

某商用製品の推奨する３階層アーキテクチャどおりに実装すると性能がでなくなるリスクがある。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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/e49ca30d-99a3-7438-d578-7d86ffe50112.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/bb6dae20-5728-28d4-4f60-cd090a87ff4d.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/325048/e0420823-ca5c-42be-2795-4226e5ed2347.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);
    }

}

```