これは、なにをしたくて書いたもの?
QuarkusのExtensionに、OpenAPIおよびSwagger UI向けのものがあるようなので試しておきたいなと思いまして。
Using OpenAPI and Swagger UI - Quarkus
もうちょっと言うと、ドキュメントに書かれているのは通常のRESTEasyのものなので、RESTEasy Reactiveと組み合わせても動くのかな?
というのが知りたかったことです。
結論を言うとあっさりと動いてしまったので、ついでにOpenAPI、Swagger事情に疎いこともあって、このあたりも調べてみました。
OpenAPIとSwagger
そもそも、OpenAPIとは?ということで。
こちらは、OpenAPI仕様の標準化を行っているOpenAPI Initiative(OAI)のサイトです。OpenAPI仕様は、こちらからたどることができます。
Home 2024 - OpenAPI Initiative
今回参照するOpenAPIのドキュメントは、Quarkusが使用しているSmallRye OpenAPIが参照しているOpenAPI 3.0.3を見ることにします。
このドキュメントによると、OpenAPI仕様というのは以下の定義になります。
The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows both humans and computers to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection.
OpenAPI Specification / Introduction
RESTful APIへの言語非依存のインターフェースを定義するための標準が、OpenAPI仕様です。
人およびコンピューターの両方が、ソースコードやドキュメントにアクセスすることなく、またネットワークトラフィックのインスペクションを
実施することで、サービスを検出したり機能を理解できたりします。
また、OpenAPI定義をドキュメント生成ツールを使ってAPIを表示したり、様々な言語向けのサーバーやクライアントのソースコードを生成、
テストケースの作成などの、ツールまわりで使用されることも視野に入っています。
An OpenAPI definition can then be used by documentation generation tools to display the API, code generation tools to generate servers and clients in various programming languages, testing tools, and many other use cases.
一方で、Swaggerという単語もあった気がします。
API Documentation & Design Tools for Teams | Swagger
OpenAPIとSwaggerの関係は、こちらを見るとわかります。
OpenAPI Specification / Appendix A: Revision History
まずはこちら。
2.0 2015-12-31 Donation of Swagger 2.0 to the OpenAPI Initiative
そして、OpenAPI 3.0.0のリリース。
3.0.0 2017-07-26 Release of the OpenAPI Specification 3.0.0
つまり、OpenAPI 3はSwaggerがOpenAPIに寄贈されてできたものになります。
GitHub - smallrye/smallrye-open-api: SmallRye implementation of Eclipse MicroProfile OpenAPI
現在のSwaggerは、OpenAPI仕様周辺のツールやサービスを提供するという役割みたいですね。
Swaggerが提供するツールは、こちら。
OSSではSwagger Codegen、Swagger Editor、Swagger UIがあります。商用サービスとしては、SwaggerHub、SwaggerHub Enterprise、
Swagger Inspectorですね。
一方で、OAIとは直接の関連はないものの、OpenAPIに関するツールを作成しているGitHub Organizationもあります。
openapi-generatorなどがあります。
これで、なんとなくOpenAPIとSwaggerの言葉の関係がざっくりわかりました。
Eclipse MicroProfile OpenAPI
次に、Eclipse MicroProfile OpenAPIについて。QuarkusのExtensionで使われているSmallRye OpenAPIは、Eclipse MicroProfile OpenAPIの
実装です。
このエントリーを書いている時点でのSmallRye OpenAPIのバージョンは2.1.17で、対応するEclipse MicroProfile OpenAPIのバージョンは2.0
なので、ドキュメントは2.0のものをリンクに記載しておきます。
Eclipse MicroProfile OpenAPI仕様は、JAX-RSアプリケーションからOpenAPI 3で記述されたAPI定義ドキュメントを作成するためのAPIおよび
プログラミングモデルを提供することを目的にしています。
This MicroProfile specification, called OpenAPI 1.0, aims to provide a set of Java interfaces and programming models which allow Java developers to natively produce OpenAPI v3 documents from their JAX-RS applications.
設定に関しては、Eclipse MicroProfile Configを使用して行います。
MicroProfile OpenAPI Specification / Configuration
OpenAPIドキュメントを生成は、アプリケーションに付与されているJAX-RSのアノテーションを元にして行われます。
MicroProfile OpenAPI Specification / Documentation Mechanisms
JAX-RSアノテーションの情報だけで足りない場合は、Eclipse MicroProfile OpenAPIが提供するアノテーションで情報を追加するか、
作成済みのOpenAPIドキュメントファイルを取り込むこともできます。
Eclipse MicroProfile OpenAPIが提供するアノテーションは、こちら。
MicroProfile OpenAPI Specification / Documentation Mechanisms / Annotations
作成済みのOpenAPIドキュメントファイルを使う場合は、こちら。
MicroProfile OpenAPI Specification / Documentation Mechanisms / Static OpenAPI files
すでに存在するOpenAPIドキュメントファイルを使用する場合は、以下の2種類の選択を取ることができます。
- 完成しているOpenAPIドキュメントファイルを使用する …
mp.openapi.scan.disable
をtrue
に設定する - 部分的に記述されているOpenAPIドキュメントファイルを使用する … アプリケーション開発者が、アノテーションやAPI、フィルターなどで拡張する必要がある
サンプルについては、Wikiに書かれています。
Home · eclipse/microprofile-open-api Wiki · GitHub
SmallRye OpenAPI
最後に、SmallRye OpenAPIについて。SmallRye OpenAPIは、Eclipse MicroProfile OpenAPIの実装です。
GitHub - smallrye/smallrye-open-api: SmallRye implementation of Eclipse MicroProfile OpenAPI
今回使用するのは2.1.17で、すでに述べていますがこのバージョンはEclipse MicroProfile OpenAPI 2.0に沿っています。
https://github.com/smallrye/smallrye-open-api/blob/2.1.17/pom.xml#L22
SmallRye OpenAPIには、JAX-RS、Spring Framework、Vert.x向けの拡張があるようです。
https://github.com/smallrye/smallrye-open-api/tree/2.1.17/extension-jaxrs
https://github.com/smallrye/smallrye-open-api/tree/2.1.17/extension-spring
https://github.com/smallrye/smallrye-open-api/tree/2.1.17/extension-vertx
Quarkus SmallRye OpenAPI Extension
そして、SmallRye OpenAPIおよびSwagger UIを組み込んだQuarkusのExtensionが、SmallRye OpenAPI Extensionです。
Using OpenAPI and Swagger UI - Quarkus
SmallRye OpenAPIはJAX-RS向けの拡張があるようでしたが、Quarkusはビルド時にスキャンする仕組みを自前で持っているようです。
https://github.com/quarkusio/quarkus/tree/2.6.3.Final/extensions/smallrye-openapi
情報はざっくりこれくらいにして、最後に動かしてみましょう。
環境
今回の環境は、こちらです。
$ java --version openjdk 17.0.1 2021-10-19 OpenJDK Runtime Environment (build 17.0.1+12-Ubuntu-120.04) OpenJDK 64-Bit Server VM (build 17.0.1+12-Ubuntu-120.04, mixed mode, sharing) $ mvn --version Apache Maven 3.8.4 (9b656c72d54e5bacbed989b64718c159fe39b537) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 17.0.1, vendor: Private Build, runtime: /usr/lib/jvm/java-17-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-97-generic", arch: "amd64", family: "unix"
アプリケーションを作成する
では、アプリケーションを作成してみます。
まず、今回のお題にExtensionはquarkus-smallrye-openapi
になります。
$ mvn io.quarkus.platform:quarkus-maven-plugin:2.6.3.Final:create \ -DprojectGroupId=org.littlewings \ -DprojectArtifactId=openapi-swaggerui-example \ -DprojectVersion=0.0.1-SNAPSHOT \ -Dextensions="resteasy-reactive,resteasy-reactive-jackson,quarkus-smallrye-openapi"
あとは、RESTEasy Reactiveと組み合わせても大丈夫か確認したいので、resteasy-reactive
も入れておきます。
選択されたExtension。
[INFO] ----------- [INFO] selected extensions: - io.quarkus:quarkus-resteasy-reactive - io.quarkus:quarkus-smallrye-openapi - io.quarkus:quarkus-resteasy-reactive-jackson [INFO] applying codestarts... [INFO] 📚 java 🔨 maven 📦 quarkus 📝 config-properties 🔧 dockerfiles 🔧 maven-wrapper 🚀 resteasy-reactive-codestart
プロジェクト内へ移動。
$ cd openapi-swaggerui-example
生成されたソースコードは削除しておきます。
$ rm src/main/java/org/littlewings/* src/test/java/org/littlewings/*
リクエストやレスポンスに使うクラス。お題は書籍にしています。
src/main/java/org/littlewings/quarkus/openapi/Book.java
package org.littlewings.quarkus.openapi; public class Book { String isbn; String title; int price; public static Book create(String isbn, String title, int price) { Book book = new Book(); book.setIsbn(isbn); book.setTitle(title); book.setPrice(price); return book; } // getter/setterは省略 }
RESTEasy Reactiveを使用した、JAX-RSリソースクラス。
src/main/java/org/littlewings/quarkus/openapi/BookResource.java
package org.littlewings.quarkus.openapi; import java.net.URI; import java.util.Comparator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import org.jboss.resteasy.reactive.RestPath; import org.jboss.resteasy.reactive.RestQuery; import org.jboss.resteasy.reactive.RestResponse; @Path("book") public class BookResource { ConcurrentMap<String, Book> store = new ConcurrentHashMap<>(); @GET @Produces(MediaType.APPLICATION_JSON) public Multi<Book> find() { return Multi .createFrom() .iterable( store .values() .stream() .sorted(Comparator.<Book, Integer>comparing(b -> b.getPrice()).reversed()) .collect(Collectors.toList()) ); } @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public Uni<Book> findByIsbn(@RestPath String isbn) { return Uni.createFrom().item(store.get(isbn)); } @GET @Path("query") @Produces(MediaType.APPLICATION_JSON) public Uni<Book> findByIsbnWithQuery(@RestQuery String isbn) { return Uni.createFrom().item(store.get(isbn)); } @PUT @Path("{isbn}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Uni<RestResponse<?>> create(@RestPath String isbn, Book book) { return Uni .createFrom() .item(store.putIfAbsent(isbn, book)) .onItem() .transform(b -> RestResponse.created(URI.create("/book/" + book.isbn))); } @DELETE @Path("{isbn}") public Uni<Book> delete(@RestPath String isbn) { return Uni .createFrom() .item(store.remove(isbn)); } }
@RestPath
や@RestQuery
などがOpenAPIドキュメントに反映されるか、確認したいところですね。
動作確認は、テストコードで行っておきます(アプリケーション自体の動作確認は省略します)。
src/test/java/org/littlewings/quarkus/openapi/BookResourceTest.java
package org.littlewings.quarkus.openapi; import java.net.URL; import java.util.List; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.http.ContentType; import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestMethodOrder; import static io.restassured.RestAssured.given; import static org.hamcrest.Matchers.*; @QuarkusTest @TestHTTPEndpoint(BookResource.class) @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class BookResourceTest { @TestHTTPEndpoint(BookResource.class) @TestHTTPResource URL url; List<Book> books = List.of( Book.create("978-4873119038", "Real World HTTP 第2版 ―歴史とコードに学ぶインターネットとウェブ技術", 3960), Book.create("978-4798167015", "Web APIの設計", 4180), Book.create("978-4297119256", "Web配信の技術―HTTPキャッシュ・リバースプロキシ・CDNを活用する", 3586) ); @Test @Order(1) public void put() { books .stream() .forEach(book -> given() .contentType(ContentType.JSON) .body(book) .when() .put(book.getIsbn()) .then() .statusCode(Response.Status.CREATED.getStatusCode()) .header(HttpHeaders.LOCATION, url + "/" + book.getIsbn()) ); } @Test @Order(2) public void findAll() { given() .when() .get() .then() .statusCode(Response.Status.OK.getStatusCode()) .body("$", hasSize(3)) .body( "title", contains( "Web APIの設計", "Real World HTTP 第2版 ―歴史とコードに学ぶインターネットとウェブ技術", "Web配信の技術―HTTPキャッシュ・リバースプロキシ・CDNを活用する" ) ); } @Test @Order(3) public void find() { books.forEach(book -> given() .pathParam("isbn", book.getIsbn()) .when() .get("{isbn}") .then() .statusCode(Response.Status.OK.getStatusCode()) .body("title", is(book.getTitle())) ); books.forEach(book -> given() .queryParam("isbn", book.getIsbn()) .when() .get("query") .then() .statusCode(Response.Status.OK.getStatusCode()) .body("title", is(book.getTitle())) ); } @Test @Order(4) public void delete() { books.forEach(book -> given() .pathParam("isbn", book.getIsbn()) .when() .delete("{isbn}") .then() .statusCode(Response.Status.OK.getStatusCode()) ); given() .when() .get() .then() .statusCode(Response.Status.OK.getStatusCode()) .body("$", empty()); } }
パッケージングして
$ mvn package
起動。
$ java -jar target/quarkus-app/quarkus-run.jar
この時点で、OpenAPIドキュメントを確認することができます。
Using OpenAPI and Swagger UI / Expose OpenAPI Specifications
パスは、/q/openapi
です。
$ curl localhost:8080/q/openapi --- openapi: 3.0.3 info: title: openapi-swaggerui-example API version: 0.0.1-SNAPSHOT paths: /book: get: tags: - Book Resource responses: "200": description: OK content: application/json: schema: type: array items: $ref: '#/components/schemas/Book' /book/query: get: tags: - Book Resource parameters: - name: isbn in: query schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Book' /book/{isbn}: get: tags: - Book Resource parameters: - name: isbn in: path required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Book' put: tags: - Book Resource parameters: - name: isbn in: path required: true schema: type: string requestBody: content: application/json: schema: $ref: '#/components/schemas/Book' responses: "200": description: OK content: application/json: schema: type: object delete: tags: - Book Resource parameters: - name: isbn in: path required: true schema: type: string responses: "200": description: OK content: application/json: schema: $ref: '#/components/schemas/Book' components: schemas: Book: type: object properties: isbn: type: string title: type: string price: format: int32 type: integer
@RestPath
や@RestQuery
で表現したものも含めて、反映されていますね。
@PUT
はデータ登録時に201を返すように実装しているのですが、それはアノテーションには表現されていないので仕方ないでしょう。
OpenAPIドキュメントを返すパスは、quarkus.smallrye-openapi.path
で変更できます。
この時、パスの先頭に/
を付与するかで動作が変わり、/
を付与すると絶対パス(/[指定したパス]
)になり、
src/main/resources/application.properties
quarkus.smallrye-openapi.path=/swagger
/
を付与しない場合は/q/[指定したパス]
というように/q
からの相対パスとして扱われます。
src/main/resources/application.properties
quarkus.smallrye-openapi.path=swagger
今回は、/
から指定しておきます。
再度ビルドして、起動。
$ mvn package $ java -jar target/quarkus-app/quarkus-run.jar
パスが変わっていることを確認。
$ curl -s localhost:8080/swagger | head -n 10 --- openapi: 3.0.3 info: title: openapi-swaggerui-example API version: 0.0.1-SNAPSHOT paths: /book: get: tags: - Book Resource
次に、Swagger UIを有効にしてみましょう。
Using OpenAPI and Swagger UI / Use Swagger UI for development
こちらは、開発用の位置づけです。
Swagger UIを有効にするには、dev(開発)モードで起動するか(テストモードでもOKのようです)
$ mvn compile quarkus:dev
productionモードでも有効にする場合は、quarkus.swagger-ui.always-include
をtrue
に設定します。
src/main/resources/application.properties
quarkus.swagger-ui.always-include=true
この場合は、再度パッケージングして起動。
$ mvn package $ java -jar target/quarkus-app/quarkus-run.jar
/q/swagger-ui/
にアクセスすると、Swagger UIが確認できます。
パスを変更する場合は、quarkus.swagger-ui.path
で指定します。パスの先頭に/
を付与するかで動作が変わるのはquarkus.smallrye-openapi.path
と
同じ話です。
src/main/resources/application.properties
quarkus.swagger-ui.always-include=true quarkus.swagger-ui.path=/my-swagger-ui
/
を付与しない場合は/q/[指定したパス]
となります。
src/main/resources/application.properties
quarkus.swagger-ui.always-include=true quarkus.swagger-ui.path=my-swagger-ui
今回は、こんなところにしておきましょう。
まとめ
QuarkusのOpenAPI、Swagger UIのExtensionを、RESTEasy Reactiveと組み合わせて使ってみました。
割とあっさりと使えたのですが、合わせてOpenAPIやEclipse MicroProfile OpenAPIなどの情報も自分なりに調べられたので、良しとしましょう。
OpenAPIを取り巻くツール等については、また別途触ってみたいかなと思います。