これは、なにをしたくて書いたもの?
こちらのエントリーで、MicroProfile OpenAPIを使ってOpenAPIドキュメントを生成してみました。
WildFly 33とMicroProfile OpenAPI(SmallRye OpenAPI)でOpenAPIドキュメントを生成する - CLOVER🍀
今回はこのOpenAPIドキュメントから、Jakarta RESTful Web Services(JAX-RS)のサーバーサイドのソースコードを生成してみたいと思います。
OpenAPI GeneratorでJAX-RSのソースコードを生成する
OpenAPIドキュメントから、ソースコードを生成するツールといえばOpenAPI Generatorがよく使われていると思います。
Hello from OpenAPI Generator | OpenAPI Generator
現在のバージョンは7.9.0で、現時点でリストアップされているGeneratorを見てみます。
Generators List | OpenAPI Generator
JAX-RSの名前が入っているものは、これだけあります。
- サーバーサイド
- jaxrs-cxf
- jaxrs-cxf-cdi
- jaxrs-cxf-extended
- jaxrs-jersey
- jaxrs-resteasy
- jaxrs-resteasy-eap
- jaxrs-spec
- libraryでQuarkus、Thorntail、Open Liberty、Helidon、Kumuluzeeを指定可能
- クライアントサイド
JAX-RSの実装を決めたところで、どうすれば…?という気分になります…。
これだけ種類が多いのは、OpenAPI GeneratorのもとになったSwagger Codegenの頃からのようです。OpenAPI Generatorで追加された
JAX-RS向けのGeneratorもあるようですが。
今回はWildFlyを使おうと思うので実装はRESTEasyになるのですが、それでもjaxrs-spec、jaxrs-resteasy、jaxrs-resteasy-eapの3択になります。
それぞれのhelpTextを見ても、なにが違うのかがよくわかりません…。
- jaxrs-spec … Generates a Java JAXRS Server according to JAXRS 2.0 specification.
- jaxrs-resteasy … Generates a Java JAXRS-Resteasy Server application.
- jaxrs-resteasy-eap … Generates a Java JAXRS-Resteasy Server application.
jaxrs-specにlibraryがあるところを踏まえると、どのGeneratorもjaxrs-specのlibraryも一種にしてもよかったのではと思うのですが、
事情や経緯がありそうな気はします。
ちなみに、RESTEasyをターゲットにした場合にGeneration Gapパターン的なinterfaceOnlyを設定可能なのはjaxrs-specのみのようです。
※delegatePatternはないようです
※他にinterfaceOnlyを設定できるのはjaxrs-cxf-cdiのみです
いったんそれぞれのGeneratorで生成して、雰囲気を見てみるとしましょう。
環境
今回の環境はこちら。
$ java --version openjdk 21.0.4 2024-07-16 OpenJDK Runtime Environment (build 21.0.4+7-Ubuntu-1ubuntu222.04) OpenJDK 64-Bit Server VM (build 21.0.4+7-Ubuntu-1ubuntu222.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.9 (8e8579a9e76f7d015ee5ec7bfcdc97d260186937) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.4, vendor: Ubuntu, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-122-generic", arch: "amd64", family: "unix"
WildFlyは33.0.2.Finalを使います。
準備
まずはベースになるものを用意します。
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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>xxxxx</groupId> <artifactId>xxxxx</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>war</packaging> <properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.4</version> <scope>provided</scope> </dependency> <!-- あとで --> </dependencies> <build> <finalName>ROOT</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.4.0</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> <plugin> <groupId>org.wildfly.plugins</groupId> <artifactId>wildfly-maven-plugin</artifactId> <version>5.0.1.Final</version> <executions> <execution> <id>package</id> <goals> <goal>package</goal> </goals> </execution> </executions> <configuration> <overwrite-provisioned-server>true</overwrite-provisioned-server> <discover-provisioning-info> <version>33.0.2.Final</version> </discover-provisioning-info> </configuration> </plugin> <plugin> <groupId>org.openapitools</groupId> <artifactId>openapi-generator-maven-plugin</artifactId> <version>7.9.0</version> <executions> <execution> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec> <generatorName><!-- あとで --></generatorName> <configOptions> <!-- あとで --> </configOptions> </configuration> </plugin> </plugins> </build> </project>
依存関係は、Generatorと合わせて後で説明します。
OpenAPI GeneratorはMavenプラグインとして使います。
<plugin> <groupId>org.openapitools</groupId> <artifactId>openapi-generator-maven-plugin</artifactId> <version>7.9.0</version> <executions> <execution> <goals> <goal>generate</goal> </goals> </execution> </executions> <configuration> <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec> <generatorName><!-- あとで --></generatorName> <configOptions> <!-- あとで --> </configOptions> </configuration> </plugin>
設定方法はこちらを見ていけばよいのですが、generateをexecutionに設定するとビルド時にOpenAPIドキュメントからソースコードを
生成してくれます。
WildFlyはWildFly Glowでプロビジョニングすることにします。
Jacksonが入っているのは作成したアプリケーションの都合ですね。
OpenAPIドキュメントは、こちらのエントリーで作成したものをsrc/main/resources配下に置いておきました。
WildFly 33とMicroProfile OpenAPI(SmallRye OpenAPI)でOpenAPIドキュメントを生成する - CLOVER🍀
src/main/resources/openapi.yaml
--- openapi: 3.0.3 info: title: My Sample REST API version: 0.0.1 servers: - url: http://localhost:8080 description: My Sample REST API Server description tags: - name: book - name: people paths: /books: get: tags: - book summary: 登録された書籍をすべて返却する description: 登録された書籍を価格の降順にソートしてすべて返却する operationId: findAllBooks responses: "200": description: 登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す content: application/json: schema: type: array items: $ref: '#/components/schemas/BookResponse' /books/{isbn13}: get: tags: - book summary: 指定されたISBNに対応する書籍を取得する description: 指定されたISBNに対応する書籍を取得する operationId: findBookByIsbn13 parameters: - name: isbn13 in: path description: ISBN required: true schema: maxLength: 14 minLength: 14 type: string example: 123-4567890123 responses: "200": description: 指定された書籍が取得できたことを表す content: application/json: schema: $ref: '#/components/schemas/BookResponse' "404": description: 指定された書籍が存在しなかったことを表す put: tags: - book summary: 指定されたISBNに書籍を登録する description: 指定されたISBNに対応する書籍を登録する operationId: registerBook parameters: - name: isbn13 in: path description: ISBN required: true schema: maxLength: 14 minLength: 14 type: string example: 123-4567890123 requestBody: description: 登録する書籍データ content: application/json: schema: $ref: '#/components/schemas/BookRequest' required: true responses: "200": description: 書籍が登録できたことを表す content: application/json: schema: $ref: '#/components/schemas/BookResponse' "400": description: バリデーションでNGになったことを表す delete: tags: - book summary: 指定されたISBNに対応する書籍を削除する description: 指定されたISBNに対応する書籍を削除する operationId: deleteBookByIsbn13 parameters: - name: isbn13 in: path description: ISBN required: true schema: maxLength: 14 minLength: 14 type: string example: 123-4567890123 responses: "204": description: 書籍が削除されていることを表す /people: get: tags: - people summary: 登録されている人をすべて返却する description: 登録された人をIDの昇順にソートしてすべて返却する operationId: findAllPeople responses: "200": description: 登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す content: application/json: schema: type: array items: $ref: '#/components/schemas/PersonResponse' post: tags: - people summary: 人を登録する description: 人を登録する operationId: registerPerson requestBody: description: 登録する人のデータ content: application/json: schema: $ref: '#/components/schemas/PersonRequest' required: true responses: "200": description: 人が登録できたことを表す content: application/json: schema: $ref: '#/components/schemas/PersonResponse' "400": description: バリデーションでNGになったことを表す /people/{id}: get: tags: - people summary: 指定されたIDに対応する人を取得する description: 指定されたIDに対応する人を取得する operationId: findPersonById parameters: - name: id in: path description: ID required: true schema: format: int32 minimum: 0 exclusiveMinimum: true type: integer example: 1 responses: "200": description: 指定された人が取得できたことを表す content: application/json: schema: $ref: '#/components/schemas/PersonResponse' "404": description: 指定された人が存在しなかったことを表す put: tags: - people summary: 指定されたIDに対応する人を更新する description: 指定されたIDに対応する人を更新する operationId: updatePerson parameters: - name: id in: path description: ID required: true schema: format: int32 minimum: 0 exclusiveMinimum: true type: integer example: 1 requestBody: description: 更新する人のデータ content: application/json: schema: $ref: '#/components/schemas/PersonRequest' required: true responses: "200": description: 人が更新できたことを表す content: application/json: schema: $ref: '#/components/schemas/PersonResponse' "400": description: バリデーションでNGになったことを表す delete: tags: - people summary: 指定されたIDに対応する人を削除する description: 指定されたIDに対応する人を削除する operationId: deletePersonById parameters: - name: id in: path description: ID required: true schema: format: int32 minimum: 0 exclusiveMinimum: true type: integer example: 1 responses: "204": description: 人が削除されていることを表す components: schemas: BookRequest: description: 登録する書籍に関するリクエスト required: - title - price - publishDate type: object properties: title: description: 登録する書籍のタイトル maxLength: 100 minLength: 1 type: string example: Javaの本 price: format: int32 description: 登録する書籍の価格 minimum: 0 exclusiveMinimum: true type: integer example: 1500 publishDate: format: date description: 登録する書籍の出版日 type: string example: 2024-10-13 BookResponse: description: 登録されている書籍 type: object properties: isbn13: description: ISBN type: string example: 123-4567890123 title: description: 書籍のタイトル type: string example: Javaの本 price: format: int32 description: 書籍の価格 type: integer example: 1500 publishDate: format: date description: 書籍の出版日 type: string example: 2024-10-13 PersonRequest: description: 登録する人に関するリクエスト required: - firstName - lastName - age type: object properties: firstName: description: 登録する人の名 maxLength: 10 minLength: 1 type: string example: カツオ lastName: description: 登録する人の姓 maxLength: 10 minLength: 1 type: string example: 磯野 age: format: int32 description: 登録する人の年齢 minimum: 0 type: integer example: 11 PersonResponse: description: 登録されている人 type: object properties: id: format: int32 description: ID type: integer example: 1 firstName: description: 名 type: string example: カツオ lastName: description: 姓 type: string example: 磯野 age: format: int32 description: 年齢 type: integer example: 11
では、各Generatorでソースコードを生成してみます。
ここから先は、まずはOpenAPI Generator Maven Pluginと依存関係に着目して書いていきます。
依存関係のスタートラインはJakarta EEのWeb ProfileとJackson Databindです。
<dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.4</version> <scope>provided</scope> </dependency>
jaxrs-spec Generatorで生成してみる
まずはjaxrs-spec Generatorから。
Documentation for the jaxrs-spec Generator | OpenAPI Generator
OpenAPI Generator Maven Pluginはこのように設定。
<configuration> <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec> <generatorName>jaxrs-spec</generatorName> <configOptions> <useJakartaEe>true</useJakartaEe> <dateLibrary>java8</dateLibrary> <invokerPackage>org.littlewings.openapi</invokerPackage> <apiPackage>org.littlewings.openapi.api</apiPackage> <modelPackage>org.littlewings.openapi.model</modelPackage> </configOptions> </configuration>
generatorName以外は、jaxrs-spec、jaxrs-resteasy、jaxrs-resteasy-eapのいずれでも共通の設定です。以降はこれを基準にしていきます。
ビルド。
$ mvn clean compile
結果は、target/generated-sources/openapiディレクトリ内に出力されます。
$ tree target/generated-sources/openapi
target/generated-sources/openapi
├── README.md
├── pom.xml
└── src
├── gen
│ └── java
│ └── org
│ └── littlewings
│ └── openapi
│ ├── RestApplication.java
│ ├── RestResourceRoot.java
│ ├── api
│ │ ├── BooksApi.java
│ │ └── PeopleApi.java
│ └── model
│ ├── BookRequest.java
│ ├── BookResponse.java
│ ├── PersonRequest.java
│ └── PersonResponse.java
└── main
└── openapi
└── openapi.yaml
10 directories, 11 files
JAX-RSのApplicationのサブクラス。@ApplicationPathに設定される値は、OpenAPIドキュメントのserversのurlのコンテキストパスの
部分が反映されるようです。
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/RestApplication.java
package org.littlewings.openapi; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath(RestResourceRoot.APPLICATION_PATH) public class RestApplication extends Application { }
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/RestResourceRoot.java
package org.littlewings.openapi; public class RestResourceRoot { public static final String APPLICATION_PATH = ""; }
自動生成されたソースコードをすべて見るのもなんなので、JAX-RSリソースクラスとリクエストのモデルをひとつずつ見ていきましょう。
JAX-RSリソースクラス。
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApi.java
package org.littlewings.openapi.api; import org.littlewings.openapi.model.BookRequest; import org.littlewings.openapi.model.BookResponse; import jakarta.ws.rs.*; import jakarta.ws.rs.core.Response; import io.swagger.annotations.*; import java.io.InputStream; import java.util.Map; import java.util.List; import jakarta.validation.constraints.*; import jakarta.validation.Valid; /** * Represents a collection of functions to interact with the API endpoints. */ @Path("/books") @Api(description = "the books API") @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-10-14T17:58:52.123839482+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public class BooksApi { @DELETE @Path("/{isbn13}") @ApiOperation(value = "指定されたISBNに対応する書籍を削除する", notes = "指定されたISBNに対応する書籍を削除する", response = Void.class, tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 204, message = "書籍が削除されていることを表す", response = Void.class) }) public Response deleteBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13) { return Response.ok().entity("magic!").build(); } @GET @Produces({ "application/json" }) @ApiOperation(value = "登録された書籍をすべて返却する", notes = "登録された書籍を価格の降順にソートしてすべて返却する", response = BookResponse.class, responseContainer = "List", tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 200, message = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す", response = BookResponse.class, responseContainer = "List") }) public Response findAllBooks() { return Response.ok().entity("magic!").build(); } @GET @Path("/{isbn13}") @Produces({ "application/json" }) @ApiOperation(value = "指定されたISBNに対応する書籍を取得する", notes = "指定されたISBNに対応する書籍を取得する", response = BookResponse.class, tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 200, message = "指定された書籍が取得できたことを表す", response = BookResponse.class), @ApiResponse(code = 404, message = "指定された書籍が存在しなかったことを表す", response = Void.class) }) public Response findBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13) { return Response.ok().entity("magic!").build(); } @PUT @Path("/{isbn13}") @Consumes({ "application/json" }) @Produces({ "application/json" }) @ApiOperation(value = "指定されたISBNに書籍を登録する", notes = "指定されたISBNに対応する書籍を登録する", response = BookResponse.class, tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 200, message = "書籍が登録できたことを表す", response = BookResponse.class), @ApiResponse(code = 400, message = "バリデーションでNGになったことを表す", response = Void.class) }) public Response registerBook(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13,@Valid @NotNull BookRequest bookRequest) { return Response.ok().entity("magic!").build(); } }
メソッドの戻り値がResponseなので、レスポンスの型をOpenAPIドキュメントの定義で強制できませんね。ただ、Responseを使わないと
レスポンスヘッダーの設定ができなかったりするので微妙なところです。
モデル。
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/model/BookRequest.java
package org.littlewings.openapi.model; import io.swagger.annotations.ApiModel; import io.swagger.annotations.ApiModelProperty; import java.time.LocalDate; import jakarta.validation.constraints.*; import jakarta.validation.Valid; import io.swagger.annotations.*; import java.util.Objects; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonValue; import com.fasterxml.jackson.annotation.JsonTypeName; /** * 登録する書籍に関するリクエスト **/ @ApiModel(description = "登録する書籍に関するリクエスト") @JsonTypeName("BookRequest") @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-10-19T00:03:10.237263859+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public class BookRequest { private String title; private Integer price; private LocalDate publishDate; /** * 登録する書籍のタイトル **/ public BookRequest title(String title) { this.title = title; return this; } @ApiModelProperty(example = "Javaの本", required = true, value = "登録する書籍のタイトル") @JsonProperty("title") @NotNull @Size(min=1,max=100)public String getTitle() { return title; } @JsonProperty("title") public void setTitle(String title) { this.title = title; } /** * 登録する書籍の価格 * minimum: 0 **/ public BookRequest price(Integer price) { this.price = price; return this; } @ApiModelProperty(example = "1500", required = true, value = "登録する書籍の価格") @JsonProperty("price") @NotNull @Min(0)public Integer getPrice() { return price; } @JsonProperty("price") public void setPrice(Integer price) { this.price = price; } /** * 登録する書籍の出版日 **/ public BookRequest publishDate(LocalDate publishDate) { this.publishDate = publishDate; return this; } @ApiModelProperty(example = "Sun Oct 13 09:00:00 JST 2024", required = true, value = "登録する書籍の出版日") @JsonProperty("publishDate") @NotNull public LocalDate getPublishDate() { return publishDate; } @JsonProperty("publishDate") public void setPublishDate(LocalDate publishDate) { this.publishDate = publishDate; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } BookRequest bookRequest = (BookRequest) o; return Objects.equals(this.title, bookRequest.title) && Objects.equals(this.price, bookRequest.price) && Objects.equals(this.publishDate, bookRequest.publishDate); } @Override public int hashCode() { return Objects.hash(title, price, publishDate); } @Override public String toString() { StringBuilder sb = new StringBuilder(); sb.append("class BookRequest {\n"); sb.append(" title: ").append(toIndentedString(title)).append("\n"); sb.append(" price: ").append(toIndentedString(price)).append("\n"); sb.append(" publishDate: ").append(toIndentedString(publishDate)).append("\n"); sb.append("}"); return sb.toString(); } /** * Convert the given object to string with each line indented by 4 spaces * (except the first line). */ private String toIndentedString(Object o) { if (o == null) { return "null"; } return o.toString().replace("\n", "\n "); } }
Bean Validationのアノテーションですが、この後でもとになったエントリーのテストコードを実行して気づいたのですが
exclusiveMinimum: trueが無視されているようです。なので、0を許容しないはずはずのリクエストパラメーターのいくつかが0を
許容するようになってしまっています。
このコードをビルドするには、以下の依存関係を追加する必要があります。
<dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.14</version> <optional>true</optional> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.15.4</version> <scope>provided</scope> </dependency>
こちらのページを見ると、io.swagger.core.v3:swagger-annotationsを追加すればよさそうに見えるのですが、jaxrs-specで生成される
ソースコードはSwagger v3 annotationsには対応していないようです。
Plugins / Maven / Dependencies
また、この2つの依存関係はjaxrs-resteasy、jaxrs-resteasy-eapでも必要になります。
interfaceOnlyにしてみる
今回の自動生成結果ではJAX-RSリソースクラスに関してもクラスそのものが生成されたので、OpenAPIドキュメントの定義が変わったりして
自動生成をもう1度行うとソースコードのマージをすることになって困ります。
jaxrs-specではinterfaceOnlyをtrueにすると、JAX-RSリソースに関してはインターフェースを生成するようになります。
<configuration> <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec> <generatorName>jaxrs-spec</generatorName> <configOptions> <useJakartaEe>true</useJakartaEe> <dateLibrary>java8</dateLibrary> <invokerPackage>org.littlewings.openapi</invokerPackage> <apiPackage>org.littlewings.openapi.api</apiPackage> <modelPackage>org.littlewings.openapi.model</modelPackage> <interfaceOnly>true</interfaceOnly> </configOptions> </configuration>
結果はこちら。
target/generated-sources/openapi
├── README.md
├── pom.xml
└── src
├── gen
│ └── java
│ └── org
│ └── littlewings
│ └── openapi
│ ├── RestApplication.java
│ ├── RestResourceRoot.java
│ ├── api
│ │ ├── BooksApi.java
│ │ └── PeopleApi.java
│ └── model
│ ├── BookRequest.java
│ ├── BookResponse.java
│ ├── PersonRequest.java
│ └── PersonResponse.java
└── main
└── openapi
└── openapi.yaml
10 directories, 11 files
パッと見では結果は変わりませんが、JAX-RSリソースについては生成結果がインターフェースになっています。
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApi.java
package org.littlewings.openapi.api; import org.littlewings.openapi.model.BookRequest; import org.littlewings.openapi.model.BookResponse; import jakarta.ws.rs.*; import jakarta.ws.rs.core.Response; import io.swagger.annotations.*; import java.io.InputStream; import java.util.Map; import java.util.List; import jakarta.validation.constraints.*; import jakarta.validation.Valid; /** * Represents a collection of functions to interact with the API endpoints. */ @Path("/books") @Api(description = "the books API") @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-10-14T18:13:42.623922339+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public interface BooksApi { /** * 指定されたISBNに対応する書籍を削除する * * @param isbn13 ISBN * @return 書籍が削除されていることを表す */ @DELETE @Path("/{isbn13}") @ApiOperation(value = "指定されたISBNに対応する書籍を削除する", notes = "指定されたISBNに対応する書籍を削除する", tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 204, message = "書籍が削除されていることを表す", response = Void.class) }) void deleteBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13); /** * 登録された書籍を価格の降順にソートしてすべて返却する * * @return 登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す */ @GET @Produces({ "application/json" }) @ApiOperation(value = "登録された書籍をすべて返却する", notes = "登録された書籍を価格の降順にソートしてすべて返却する", tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 200, message = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す", response = BookResponse.class, responseContainer = "List") }) List<BookResponse> findAllBooks(); /** * 指定されたISBNに対応する書籍を取得する * * @param isbn13 ISBN * @return 指定された書籍が取得できたことを表す * @return 指定された書籍が存在しなかったことを表す */ @GET @Path("/{isbn13}") @Produces({ "application/json" }) @ApiOperation(value = "指定されたISBNに対応する書籍を取得する", notes = "指定されたISBNに対応する書籍を取得する", tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 200, message = "指定された書籍が取得できたことを表す", response = BookResponse.class), @ApiResponse(code = 404, message = "指定された書籍が存在しなかったことを表す", response = Void.class) }) BookResponse findBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13); /** * 指定されたISBNに対応する書籍を登録する * * @param isbn13 ISBN * @param bookRequest 登録する書籍データ * @return 書籍が登録できたことを表す * @return バリデーションでNGになったことを表す */ @PUT @Path("/{isbn13}") @Consumes({ "application/json" }) @Produces({ "application/json" }) @ApiOperation(value = "指定されたISBNに書籍を登録する", notes = "指定されたISBNに対応する書籍を登録する", tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 200, message = "書籍が登録できたことを表す", response = BookResponse.class), @ApiResponse(code = 400, message = "バリデーションでNGになったことを表す", response = Void.class) }) BookResponse registerBook(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13,@Valid @NotNull BookRequest bookRequest); }
あとは、このインターフェースを実装したJAX-RSリソースクラスを作成すればよいことになります。
ところで、メソッドの戻り値の型がJAX-RSのResponseではなく具体的な型になっています。レスポンスヘッダーを扱いたい場合などは
このままだと困ることになるので、そういう場合はreturnResponseをtrueにすると
<configuration> <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec> <generatorName>jaxrs-spec</generatorName> <configOptions> <useJakartaEe>true</useJakartaEe> <dateLibrary>java8</dateLibrary> <invokerPackage>org.littlewings.openapi</invokerPackage> <apiPackage>org.littlewings.openapi.api</apiPackage> <modelPackage>org.littlewings.openapi.model</modelPackage> <interfaceOnly>true</interfaceOnly> <returnResponse>true</returnResponse> </configOptions> </configuration>
生成されるのはインターフェースのままで、メソッドの戻り値の型がJAX-RSのResponseになります。
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApi.java
package org.littlewings.openapi.api; import org.littlewings.openapi.model.BookRequest; import org.littlewings.openapi.model.BookResponse; import jakarta.ws.rs.*; import jakarta.ws.rs.core.Response; import io.swagger.annotations.*; import java.io.InputStream; import java.util.Map; import java.util.List; import jakarta.validation.constraints.*; import jakarta.validation.Valid; /** * Represents a collection of functions to interact with the API endpoints. */ @Path("/books") @Api(description = "the books API") @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaJAXRSSpecServerCodegen", date = "2024-10-14T18:16:23.044032831+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public interface BooksApi { /** * 指定されたISBNに対応する書籍を削除する * * @param isbn13 ISBN * @return 書籍が削除されていることを表す */ @DELETE @Path("/{isbn13}") @ApiOperation(value = "指定されたISBNに対応する書籍を削除する", notes = "指定されたISBNに対応する書籍を削除する", tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 204, message = "書籍が削除されていることを表す", response = Void.class) }) Response deleteBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13); /** * 登録された書籍を価格の降順にソートしてすべて返却する * * @return 登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す */ @GET @Produces({ "application/json" }) @ApiOperation(value = "登録された書籍をすべて返却する", notes = "登録された書籍を価格の降順にソートしてすべて返却する", tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 200, message = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す", response = BookResponse.class, responseContainer = "List") }) Response findAllBooks(); /** * 指定されたISBNに対応する書籍を取得する * * @param isbn13 ISBN * @return 指定された書籍が取得できたことを表す * @return 指定された書籍が存在しなかったことを表す */ @GET @Path("/{isbn13}") @Produces({ "application/json" }) @ApiOperation(value = "指定されたISBNに対応する書籍を取得する", notes = "指定されたISBNに対応する書籍を取得する", tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 200, message = "指定された書籍が取得できたことを表す", response = BookResponse.class), @ApiResponse(code = 404, message = "指定された書籍が存在しなかったことを表す", response = Void.class) }) Response findBookByIsbn13(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13); /** * 指定されたISBNに対応する書籍を登録する * * @param isbn13 ISBN * @param bookRequest 登録する書籍データ * @return 書籍が登録できたことを表す * @return バリデーションでNGになったことを表す */ @PUT @Path("/{isbn13}") @Consumes({ "application/json" }) @Produces({ "application/json" }) @ApiOperation(value = "指定されたISBNに書籍を登録する", notes = "指定されたISBNに対応する書籍を登録する", tags={ "book" }) @ApiResponses(value = { @ApiResponse(code = 200, message = "書籍が登録できたことを表す", response = BookResponse.class), @ApiResponse(code = 400, message = "バリデーションでNGになったことを表す", response = Void.class) }) Response registerBook(@PathParam("isbn13") @Size(min=14,max=14) @ApiParam("ISBN") String isbn13,@Valid @NotNull BookRequest bookRequest); }
なお、interfaceOnlyもreturnResponseもjaxrs-resteasy、jaxrs-resteasy-eapでは指定できません。
jaxrs-resteasy
次は、jaxrs-resteasyです。
Documentation for the jaxrs-resteasy Generator | OpenAPI Generator
OpenAPI Generator Maven Pluginの設定はこうしました。
<configuration> <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec> <generatorName>jaxrs-resteasy</generatorName> <configOptions> <useJakartaEe>true</useJakartaEe> <dateLibrary>java8</dateLibrary> <invokerPackage>org.littlewings.openapi</invokerPackage> <apiPackage>org.littlewings.openapi.api</apiPackage> <modelPackage>org.littlewings.openapi.model</modelPackage> </configOptions> </configuration>
生成結果はこうなりました。
target/generated-sources/openapi
target/generated-sources/openapi
├── README.md
├── build.gradle
├── pom.xml
├── settings.gradle
└── src
├── gen
│ └── java
│ └── org
│ └── littlewings
│ └── openapi
│ ├── JacksonConfig.java
│ ├── RFC3339DateFormat.java
│ ├── RestApplication.java
│ ├── StringUtil.java
│ ├── api
│ │ ├── ApiException.java
│ │ ├── ApiOriginFilter.java
│ │ ├── ApiResponseMessage.java
│ │ ├── BooksApi.java
│ │ ├── BooksApiService.java
│ │ ├── LocalDateProvider.java
│ │ ├── NotFoundException.java
│ │ ├── OffsetDateTimeProvider.java
│ │ ├── PeopleApi.java
│ │ └── PeopleApiService.java
│ └── model
│ ├── BookRequest.java
│ ├── BookResponse.java
│ ├── PersonRequest.java
│ └── PersonResponse.java
└── main
├── java
│ └── org
│ └── littlewings
│ └── openapi
│ └── api
│ └── impl
│ ├── BooksApiServiceImpl.java
│ └── PeopleApiServiceImpl.java
└── webapp
└── WEB-INF
├── jboss-web.xml
└── web.xml
17 directories, 26 files
jaxrs-specとはだいぶ雰囲気が変わりました。
web.xmlなども生成されていますね。
target/generated-sources/openapi/src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:j2ee="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <filter> <filter-name>ApiOriginFilter</filter-name> <filter-class>org.littlewings.openapi.api.ApiOriginFilter</filter-class> </filter> <filter-mapping> <filter-name>ApiOriginFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> </web-app>
target/generated-sources/openapi/src/main/webapp/WEB-INF/jboss-web.xml
<jboss-web> <context-root></context-root> </jboss-web>
Applicationのサブクラスは、少し簡素になりました。
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/RestApplication.java
package org.littlewings.openapi; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("") public class RestApplication extends Application { }
生成されるJavaソースコードは大量に増えているのですが、Serviceというものが増えているのがポイントでしょうか。
└── src
├── gen
│ └── java
│ └── org
│ └── littlewings
│ └── openapi
│ ├── JacksonConfig.java
│ ├── RFC3339DateFormat.java
│ ├── RestApplication.java
│ ├── StringUtil.java
│ ├── api
│ │ ├── ApiException.java
│ │ ├── ApiOriginFilter.java
│ │ ├── ApiResponseMessage.java
│ │ ├── BooksApi.java
│ │ ├── BooksApiService.java
│ │ ├── LocalDateProvider.java
│ │ ├── NotFoundException.java
│ │ ├── OffsetDateTimeProvider.java
│ │ ├── PeopleApi.java
│ │ └── PeopleApiService.java
│ └── model
│ ├── BookRequest.java
│ ├── BookResponse.java
│ ├── PersonRequest.java
│ └── PersonResponse.java
JAX-RSリソースクラスの定義を見ると、Serviceクラスをインジェクションして委譲する実装になっています。
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApi.java
package org.littlewings.openapi.api; import org.littlewings.openapi.model.*; import org.littlewings.openapi.api.BooksApiService; import io.swagger.annotations.ApiParam; import io.swagger.jaxrs.*; import org.littlewings.openapi.model.BookRequest; import org.littlewings.openapi.model.BookResponse; import java.util.Map; import java.util.List; import org.littlewings.openapi.api.NotFoundException; import java.io.InputStream; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.*; import jakarta.inject.Inject; import jakarta.validation.constraints.*; import jakarta.validation.Valid; @Path("/books") @io.swagger.annotations.Api(description = "the books API") @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", date = "2024-10-14T18:22:37.979502016+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public class BooksApi { @Inject BooksApiService service; @DELETE @Path("/{isbn13}") @io.swagger.annotations.ApiOperation(value = "指定されたISBNに対応する書籍を削除する", notes = "指定されたISBNに対応する書籍を削除する", response = Void.class, tags={ "book", }) @io.swagger.annotations.ApiResponses(value = { @io.swagger.annotations.ApiResponse(code = 204, message = "書籍が削除されていることを表す", response = Void.class) }) public Response deleteBookByIsbn13( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@Context SecurityContext securityContext) throws NotFoundException { return service.deleteBookByIsbn13(isbn13,securityContext); } @GET @Produces({ "application/json" }) @io.swagger.annotations.ApiOperation(value = "登録された書籍をすべて返却する", notes = "登録された書籍を価格の降順にソートしてすべて返却する", response = BookResponse.class, responseContainer = "List", tags={ "book", }) @io.swagger.annotations.ApiResponses(value = { @io.swagger.annotations.ApiResponse(code = 200, message = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す", response = BookResponse.class, responseContainer = "List") }) public Response findAllBooks(@Context SecurityContext securityContext) throws NotFoundException { return service.findAllBooks(securityContext); } @GET @Path("/{isbn13}") @Produces({ "application/json" }) @io.swagger.annotations.ApiOperation(value = "指定されたISBNに対応する書籍を取得する", notes = "指定されたISBNに対応する書籍を取得する", response = BookResponse.class, tags={ "book", }) @io.swagger.annotations.ApiResponses(value = { @io.swagger.annotations.ApiResponse(code = 200, message = "指定された書籍が取得できたことを表す", response = BookResponse.class), @io.swagger.annotations.ApiResponse(code = 404, message = "指定された書籍が存在しなかったことを表す", response = Void.class) }) public Response findBookByIsbn13( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@Context SecurityContext securityContext) throws NotFoundException { return service.findBookByIsbn13(isbn13,securityContext); } @PUT @Path("/{isbn13}") @Consumes({ "application/json" }) @Produces({ "application/json" }) @io.swagger.annotations.ApiOperation(value = "指定されたISBNに書籍を登録する", notes = "指定されたISBNに対応する書籍を登録する", response = BookResponse.class, tags={ "book", }) @io.swagger.annotations.ApiResponses(value = { @io.swagger.annotations.ApiResponse(code = 200, message = "書籍が登録できたことを表す", response = BookResponse.class), @io.swagger.annotations.ApiResponse(code = 400, message = "バリデーションでNGになったことを表す", response = Void.class) }) public Response registerBook( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@ApiParam(value = "登録する書籍データ" ,required=true) @NotNull @Valid BookRequest bookRequest,@Context SecurityContext securityContext) throws NotFoundException { return service.registerBook(isbn13,bookRequest,securityContext); } }
Serviceはインターフェースですね。戻り値は一律JAX-RSのResponseです。
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApiService.java
package org.littlewings.openapi.api; import org.littlewings.openapi.api.*; import org.littlewings.openapi.model.*; import org.littlewings.openapi.model.BookRequest; import org.littlewings.openapi.model.BookResponse; import java.util.List; import org.littlewings.openapi.api.NotFoundException; import java.io.InputStream; import jakarta.validation.constraints.*; import jakarta.validation.Valid; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", date = "2024-10-14T18:22:37.979502016+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public interface BooksApiService { Response deleteBookByIsbn13(String isbn13,SecurityContext securityContext) throws NotFoundException; Response findAllBooks(SecurityContext securityContext) throws NotFoundException; Response findBookByIsbn13(String isbn13,SecurityContext securityContext) throws NotFoundException; Response registerBook(String isbn13,BookRequest bookRequest,SecurityContext securityContext) throws NotFoundException; }
実装クラスはスケルトンが生成されるようなので、こちらを取り込んで修正していく使い方をイメージしているような気がします。
target/generated-sources/openapi/src/main/java/org/littlewings/openapi/api/impl/BooksApiServiceImpl.java
package org.littlewings.openapi.api.impl; import org.littlewings.openapi.api.*; import org.littlewings.openapi.model.*; import org.littlewings.openapi.model.BookRequest; import org.littlewings.openapi.model.BookResponse; import java.util.List; import org.littlewings.openapi.api.NotFoundException; import java.io.InputStream; import jakarta.validation.constraints.*; import jakarta.validation.Valid; import jakarta.enterprise.context.RequestScoped; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; @RequestScoped @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", date = "2024-10-14T18:22:37.979502016+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public class BooksApiServiceImpl implements BooksApiService { public Response deleteBookByIsbn13(String isbn13,SecurityContext securityContext) throws NotFoundException { // do some magic! return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); } public Response findAllBooks(SecurityContext securityContext) throws NotFoundException { // do some magic! return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); } public Response findBookByIsbn13(String isbn13,SecurityContext securityContext) throws NotFoundException { // do some magic! return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); } public Response registerBook(String isbn13,BookRequest bookRequest,SecurityContext securityContext) throws NotFoundException { // do some magic! return Response.ok().entity(new ApiResponseMessage(ApiResponseMessage.OK, "magic!")).build(); } }
ちなみに、この実装クラスが生成されるのはtarget/generated-sources/openapi/src/main/java配下です。
インターフェースやモデルが生成されていたのはtarget/generated-sources/openapi/src/gen/java配下で、こちらは修正しないソースコードの
位置づけかなと思います。
モデルはjaxrs-specと大差なかったので省略。
その他のクラスをいくつか。
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/JacksonConfig.java
package org.littlewings.openapi; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import jakarta.ws.rs.ext.ContextResolver; import jakarta.ws.rs.ext.Provider; import java.io.IOException; @Provider public class JacksonConfig implements ContextResolver<ObjectMapper> { private final ObjectMapper objectMapper; public JacksonConfig() throws Exception { objectMapper = new ObjectMapper() .registerModule(new JavaTimeModule()) .setDateFormat(new RFC3339DateFormat()); } public ObjectMapper getContext(Class<?> arg0) { return objectMapper; } }
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/RFC3339DateFormat.java
package org.littlewings.openapi; import com.fasterxml.jackson.databind.util.StdDateFormat; import java.text.DateFormat; import java.text.FieldPosition; import java.text.ParsePosition; import java.util.Date; import java.util.GregorianCalendar; import java.util.TimeZone; public class RFC3339DateFormat extends DateFormat { private static final long serialVersionUID = 1L; private static final TimeZone TIMEZONE_Z = TimeZone.getTimeZone("UTC"); private final StdDateFormat fmt = new StdDateFormat() .withTimeZone(TIMEZONE_Z) .withColonInTimeZone(true); public RFC3339DateFormat() { this.calendar = new GregorianCalendar(); } @Override public Date parse(String source, ParsePosition pos) { return fmt.parse(source, pos); } @Override public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) { return fmt.format(date, toAppendTo, fieldPosition); } @Override public Object clone() { return this; } }
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/StringUtil.java
package org.littlewings.openapi; @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", date = "2024-10-14T18:22:37.979502016+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public class StringUtil { /** * Check if the given array contains the given value (with case-insensitive comparison). * * @param array The array * @param value The value to search * @return true if the array contains the value */ public static boolean containsIgnoreCase(String[] array, String value) { for (String str : array) { if (value == null && str == null) return true; if (value != null && value.equalsIgnoreCase(str)) return true; } return false; } /** * Join an array of strings with the given separator. * <p> * Note: This might be replaced by utility method from commons-lang or guava someday * if one of those libraries is added as dependency. * </p> * * @param array The array of strings * @param separator The separator * @return the resulting string */ public static String join(String[] array, String separator) { int len = array.length; if (len == 0) return ""; StringBuilder out = new StringBuilder(); out.append(array[0]); for (int i = 1; i < len; i++) { out.append(separator).append(array[i]); } return out.toString(); } }
Jackson向けのクラスが生成されていますね。
このGeneratorの特徴(?)は、apiパッケージ配下に生成されるクラスが多いことですね。
こういうのもできたりします。
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/ApiResponseMessage.java
package org.littlewings.openapi.api; import jakarta.xml.bind.annotation.XmlTransient; @jakarta.xml.bind.annotation.XmlRootElement @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyServerCodegen", date = "2024-10-14T18:22:37.979502016+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public class ApiResponseMessage { public static final int ERROR = 1; public static final int WARNING = 2; public static final int INFO = 3; public static final int OK = 4; public static final int TOO_BUSY = 5; int code; String type; String message; public ApiResponseMessage(){} public ApiResponseMessage(int code, String message){ this.code = code; switch(code){ case ERROR: setType("error"); break; case WARNING: setType("warning"); break; case INFO: setType("info"); break; case OK: setType("ok"); break; case TOO_BUSY: setType("too busy"); break; default: setType("unknown"); break; } this.message = message; } @XmlTransient public int getCode() { return code; } public void setCode(int code) { this.code = code; } public String getType() { return type; } public void setType(String type) { this.type = type; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } }
JAXBが使われたりしていますが…。
そんなわけもあって、このソースコードをビルドするには以下の依存関係が必要です。
<dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.14</version> <optional>true</optional> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.15.4</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-jaxrs</artifactId> <version>1.6.14</version> <optional>true</optional> </dependency> <dependency> <groupId>jakarta.xml.bind</groupId> <artifactId>jakarta.xml.bind-api</artifactId> <version>4.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.4</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.15.4</version> <scope>provided</scope> </dependency>
swagger-jaxrsにもSwagger v3版があるのですが、古いバージョンを使っています。
jaxrs-resteasy-eap
最後はjaxrs-resteasy-eapです。名前からすると、JBoss EAPまたはWildFlyにデプロイすることを前提にしたGeneratorになるのでしょうか?
※その割にはjaxrs-resteasyでもjboss-web.xmlが生成されたりしていますが
Documentation for the jaxrs-resteasy-eap Generator | OpenAPI Generator
<configuration> <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec> <generatorName>jaxrs-resteasy-eap</generatorName> <configOptions> <useJakartaEe>true</useJakartaEe> <dateLibrary>java8</dateLibrary> <invokerPackage>org.littlewings.openapi</invokerPackage> <apiPackage>org.littlewings.openapi.api</apiPackage> <modelPackage>org.littlewings.openapi.model</modelPackage> </configOptions> </configuration>
自動生成結果はこちらです。
target/generated-sources/openapi
├── README.md
├── build.gradle
├── pom.xml
├── settings.gradle
└── src
├── gen
│ └── java
│ └── org
│ └── littlewings
│ └── openapi
│ ├── api
│ │ ├── BooksApi.java
│ │ └── PeopleApi.java
│ └── model
│ ├── BookRequest.java
│ ├── BookResponse.java
│ ├── PersonRequest.java
│ └── PersonResponse.java
└── main
├── java
│ └── org
│ └── littlewings
│ └── openapi
│ ├── JacksonConfig.java
│ ├── RestApplication.java
│ └── api
│ └── impl
│ ├── BooksApiServiceImpl.java
│ └── PeopleApiServiceImpl.java
└── webapp
└── WEB-INF
├── jboss-web.xml
└── web.xml
17 directories, 16 files
jaxrs-resteasyとはだいぶ結果が変わりました…。
jboss-web.xmlはjaxrs-resteasyと同じでしたが、web.xmlは異なる結果になりました。
target/generated-sources/openapi/src/main/webapp/WEB-INF/web.xml
<?xml version="1.0" encoding="ISO-8859-1"?> <web-app version="2.4" xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:j2ee="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"> <context-param> <param-name>resteasy.providers</param-name> <param-value>org.littlewings.openapi.JacksonConfig</param-value> </context-param> </web-app>
Applicationのサブクラスも実装側のディレクトリーに配置されるようになっているのと、定義もけっこう変わっています。
target/generated-sources/openapi/src/main/java/org/littlewings/openapi/RestApplication.java
package org.littlewings.openapi; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; import java.util.Set; import java.util.HashSet; import org.littlewings.openapi.api.impl.BooksApiServiceImpl; import org.littlewings.openapi.api.impl.PeopleApiServiceImpl; @ApplicationPath("") public class RestApplication extends Application { public Set<Class<?>> getClasses() { Set<Class<?>> resources = new HashSet<Class<?>>(); resources.add(BooksApiServiceImpl.class); resources.add(PeopleApiServiceImpl.class); return resources; } }
Serviceの実装クラスをJAX-RSリソースクラスとして追加していますね。
Apiという名前のクラスはどうなったのでしょうか?
target/generated-sources/openapi/src/gen/java/org/littlewings/openapi/api/BooksApi.java
package org.littlewings.openapi.api; import org.littlewings.openapi.model.*; import io.swagger.annotations.ApiParam; import io.swagger.jaxrs.*; import org.littlewings.openapi.model.BookRequest; import org.littlewings.openapi.model.BookResponse; import java.util.List; import java.util.Map; import java.io.InputStream; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; import jakarta.ws.rs.*; import jakarta.validation.constraints.*; import jakarta.validation.Valid; @Path("/books") @io.swagger.annotations.Api(description = "the books API") @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyEapServerCodegen", date = "2024-10-14T18:37:10.861562623+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public interface BooksApi { @DELETE @Path("/{isbn13}") @io.swagger.annotations.ApiOperation(value = "指定されたISBNに対応する書籍を削除する", notes = "指定されたISBNに対応する書籍を削除する", response = Void.class, tags={ "book", }) @io.swagger.annotations.ApiResponses(value = { @io.swagger.annotations.ApiResponse(code = 204, message = "書籍が削除されていることを表す", response = Void.class) }) public Response deleteBookByIsbn13( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@Context SecurityContext securityContext); @GET @Produces({ "application/json" }) @io.swagger.annotations.ApiOperation(value = "登録された書籍をすべて返却する", notes = "登録された書籍を価格の降順にソートしてすべて返却する", response = BookResponse.class, responseContainer = "List", tags={ "book", }) @io.swagger.annotations.ApiResponses(value = { @io.swagger.annotations.ApiResponse(code = 200, message = "登録されている書籍の件数に関わらず、書籍を返却したことを表す。0件の場合は空の配列を返す", response = BookResponse.class, responseContainer = "List") }) public Response findAllBooks(@Context SecurityContext securityContext); @GET @Path("/{isbn13}") @Produces({ "application/json" }) @io.swagger.annotations.ApiOperation(value = "指定されたISBNに対応する書籍を取得する", notes = "指定されたISBNに対応する書籍を取得する", response = BookResponse.class, tags={ "book", }) @io.swagger.annotations.ApiResponses(value = { @io.swagger.annotations.ApiResponse(code = 200, message = "指定された書籍が取得できたことを表す", response = BookResponse.class), @io.swagger.annotations.ApiResponse(code = 404, message = "指定された書籍が存在しなかったことを表す", response = Void.class) }) public Response findBookByIsbn13( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@Context SecurityContext securityContext); @PUT @Path("/{isbn13}") @Consumes({ "application/json" }) @Produces({ "application/json" }) @io.swagger.annotations.ApiOperation(value = "指定されたISBNに書籍を登録する", notes = "指定されたISBNに対応する書籍を登録する", response = BookResponse.class, tags={ "book", }) @io.swagger.annotations.ApiResponses(value = { @io.swagger.annotations.ApiResponse(code = 200, message = "書籍が登録できたことを表す", response = BookResponse.class), @io.swagger.annotations.ApiResponse(code = 400, message = "バリデーションでNGになったことを表す", response = Void.class) }) public Response registerBook( @Size(min=14,max=14) @PathParam("isbn13") String isbn13,@ApiParam(value = "登録する書籍データ" ,required=true) @NotNull @Valid BookRequest bookRequest,@Context SecurityContext securityContext); }
こちらはインターフェースのようです。
Serviceの実装クラスを見ると、インターフェースとして生成されたApiを実装する形態なっているようです。
target/generated-sources/openapi/src/main/java/org/littlewings/openapi/api/impl/BooksApiServiceImpl.java
package org.littlewings.openapi.api.impl; import org.littlewings.openapi.api.*; import org.littlewings.openapi.model.*; import org.littlewings.openapi.model.BookRequest; import org.littlewings.openapi.model.BookResponse; import java.util.List; import java.io.InputStream; import jakarta.validation.constraints.*; import jakarta.validation.Valid; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.SecurityContext; @jakarta.annotation.Generated(value = "org.openapitools.codegen.languages.JavaResteasyEapServerCodegen", date = "2024-10-14T18:37:10.861562623+09:00[Asia/Tokyo]", comments = "Generator version: 7.9.0") public class BooksApiServiceImpl implements BooksApi { public Response deleteBookByIsbn13(String isbn13,SecurityContext securityContext) { // do some magic! return Response.ok().build(); } public Response findAllBooks(SecurityContext securityContext) { // do some magic! return Response.ok().build(); } public Response findBookByIsbn13(String isbn13,SecurityContext securityContext) { // do some magic! return Response.ok().build(); } public Response registerBook(String isbn13,BookRequest bookRequest,SecurityContext securityContext) { // do some magic! return Response.ok().build(); } }
Jackson向けの設定も、実装側のディレクトリーに置かれるようになりました。
target/generated-sources/openapi/src/main/java/org/littlewings/openapi/JacksonConfig.java
package org.littlewings.openapi; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.ext.ContextResolver; import jakarta.ws.rs.ext.Provider; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; @Provider @Produces(MediaType.APPLICATION_JSON) public class JacksonConfig implements ContextResolver<ObjectMapper> { private static final Logger LOG = LoggerFactory.getLogger(JacksonConfig.class); private ObjectMapper objectMapper; public JacksonConfig() throws Exception { this.objectMapper = new ObjectMapper(); this.objectMapper.registerModule(new JavaTimeModule()); // sample to convert any DateTime to readable timestamps //this.objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true); } public ObjectMapper getContext(Class<?> objectType) { return objectMapper; } }
jaxrs-resteayよりもファイルが減ったのはいいのですが、ちょっと不思議な生成結果のような気がします…。
jaxrs-resteasy-eapで生成したソースコードをビルドするのに追加が必要な依存関係は、こちらになります。
<dependency> <groupId>io.swagger</groupId> <artifactId>swagger-annotations</artifactId> <version>1.6.14</version> <optional>true</optional> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-annotations</artifactId> <version>2.15.4</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.swagger</groupId> <artifactId>swagger-jaxrs</artifactId> <version>1.6.14</version> <optional>true</optional> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.16</version> <optional>true</optional> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.4</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> <version>2.15.4</version> <scope>provided</scope> </dependency>
なぜかSLF4Jに依存しているのですが…JBoss Loggingに
jaxrs-specを使ってREST APIを実装する
と、ここまでいろいろと試してみましたが、今回は1番シンプルそう(?)なjaxrs-specを使うことにします。
interfaceOnlyはtrueにして、sourceFolderはデフォルトのsrc/gen/javaからsrc/main/javaにしておきました。
<configuration> <inputSpec>${project.basedir}/src/main/resources/openapi.yaml</inputSpec> <generatorName>jaxrs-spec</generatorName> <configOptions> <!-- デフォルトは src/gen/java --> <sourceFolder>src/main/java</sourceFolder> <useJakartaEe>true</useJakartaEe> <dateLibrary>java8</dateLibrary> <invokerPackage>org.littlewings.openapi</invokerPackage> <apiPackage>org.littlewings.openapi.api</apiPackage> <modelPackage>org.littlewings.openapi.model</modelPackage> <interfaceOnly>true</interfaceOnly> <returnResponse>false</returnResponse> <generateBuilders>true</generateBuilders> </configOptions> </configuration>
sourceFolderをsrc/main/javaにしておくと、自動生成されたソースコードをそのままビルド対象に含めることができます。
生成されるソースコードはすべて修正しないものになるので、これでいいかなと。
あとはモデルのインスタンスを構築するためのビルダーを生成する、generateBuildersをtrueにしておきました。
実装したJAX-RSリソースクラス。
src/main/java/org/littlewings/openapi/api/BooksApiImpl.java
package org.littlewings.openapi.api; import java.util.Comparator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import jakarta.enterprise.context.ApplicationScoped; import org.littlewings.openapi.model.BookRequest; import org.littlewings.openapi.model.BookResponse; @ApplicationScoped public class BooksApiImpl implements BooksApi { private ConcurrentMap<String, BookResponse> store = new ConcurrentHashMap<>(); @Override public void deleteBookByIsbn13(String isbn13) { store.remove(isbn13); } @Override public List<BookResponse> findAllBooks() { return store.values().stream().sorted(Comparator.comparing(BookResponse::getPrice).reversed()).toList(); } @Override public BookResponse findBookByIsbn13(String isbn13) { return store.get(isbn13); } @Override public BookResponse registerBook(String isbn13, BookRequest bookRequest) { return store.compute(isbn13, (i, before) -> BookResponse.builder() .isbn13(i) .title(bookRequest.getTitle()) .price(bookRequest.getPrice()) .publishDate(bookRequest.getPublishDate()) .build() ); } }
src/main/java/org/littlewings/openapi/api/PeopleApiImpl.java
package org.littlewings.openapi.api; import java.util.Comparator; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import jakarta.enterprise.context.ApplicationScoped; import org.littlewings.openapi.model.PersonRequest; import org.littlewings.openapi.model.PersonResponse; @ApplicationScoped public class PeopleApiImpl implements PeopleApi { private ConcurrentMap<Integer, PersonResponse> store = new ConcurrentHashMap<>(); @Override public void deletePersonById(Integer id) { store.remove(id); } @Override public List<PersonResponse> findAllPeople() { return store.values().stream().sorted(Comparator.comparing(PersonResponse::getId)).toList(); } @Override public PersonResponse findPersonById(Integer id) { return store.get(id); } @Override public PersonResponse registerPerson(PersonRequest personRequest) { String identity = personRequest.getFirstName() + ":" + personRequest.getLastName(); if (store.values().stream().filter(r -> identity.equals(r.getFirstName() + ":" + r.getLastName())).findFirst().isEmpty()) { return store.compute(store.size() + 1, (i, before) -> PersonResponse.builder() .id(i) .firstName(personRequest.getFirstName()) .lastName(personRequest.getLastName()) .age(personRequest.getAge()) .build() ); } return store.values().stream().filter(r -> identity.equals(r.getFirstName() + ":" + r.getLastName())).findFirst().get(); } @Override public PersonResponse updatePerson(Integer id, PersonRequest personRequest) { return store.compute(id, (i, before) -> PersonResponse.builder() .id(i) .firstName(personRequest.getFirstName()) .lastName(personRequest.getLastName()) .age(personRequest.getAge()) .build() ); } }
JAX-RSリソースクラスに関しては、こちらで作成したものをOpenAPI Generatorで生成されたモデルを利用するように修正したものです。
WildFly 33とMicroProfile OpenAPI(SmallRye OpenAPI)でOpenAPIドキュメントを生成する - CLOVER🍀
Jacksonの設定。最初にJackson Databindの依存関係を入れていたのは、これが理由ですね。
src/main/java/org/littlewings/openapi/provider/ObjectMapperProvider.java
package org.littlewings.openapi.provider; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.ws.rs.Consumes; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.ext.ContextResolver; import jakarta.ws.rs.ext.Provider; @Provider @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public class ObjectMapperProvider implements ContextResolver<ObjectMapper> { @Override public ObjectMapper getContext(Class<?> type) { return new ObjectMapper().findAndRegisterModules(); } }
テストについてもこちらで用意したものを使いましたが、exclusiveMinimum: trueが考慮されていないので0を境界にするテストは
通りませんでした。
WildFly 33とMicroProfile OpenAPI(SmallRye OpenAPI)でOpenAPIドキュメントを生成する - CLOVER🍀
とりあえず、雰囲気はわかったのでこんなところでよいでしょう。
おわりに
OpenAPI GeneratorでJAX-RS(RESTEasy)のサーバーサイドのソースコードを生成してみました。
JAX-RS向けのGeneratorはたくさんあるし、使ってみるとSwagger v3 annotationに移行していなかったり、バリデーションで境界値を踏んだりと
いろいろありましたが、Generatorは大量にあるのでメンテナンスも大変だろうなぁと…。
とはいえ、JAX-RSのGeneratorはたくさんあるので使い分けくらいはもう少し説明してくれてもいいのではないかと…。
次に使う時は、それほど迷わないでしょう、きっと。