これは、なにをしたくて書いたもの?
Quarkus 0.20から、Jackson向けのExtensionが追加されていたようで。
こちらをちょっと試してみようかな、と。
Quarkus Jackson Extension
Jacksonについては、多くの質問があったみたいですね。
Quarkus loves standards. That’s why we started by supporting JSON-B as our JSON serialization library.
We had a lot of users asking for Jackson support and, while you could use Jackson with Quarkus, it wasn’t as easy as for JSON-B.
Quarkusは標準を好むのでJSON-Bを起点にしたけれど、Jacksomも追加しましたよ、と。
Jackson support · Issue #2578 · quarkusio/quarkus · GitHub
Jacksonをスタンドアロンに使うための「jackson」Extensionと、RESTEasyと合わせて使うための「resteasy-jackson」Extensionの
2つがあります。
Quarkusのガイド上も、JSON-B以外の選択肢として、Jacksonが書かれた内容になっています。
Quarkus - Writing JSON REST Services
今回は、「resteasy-jackson」を見ていこうと思います。
環境
今回の環境は、こちら。
$ java -version openjdk version "1.8.0_222" OpenJDK Runtime Environment (build 1.8.0_222-8u222-b10-1ubuntu1~18.04.1-b10) OpenJDK 64-Bit Server VM (build 25.222-b10, mixed mode) $ mvn -version Apache Maven 3.6.2 (40f52333136460af0dc0d7232c0dc0bcf0d9e117; 2019-08-28T00:06:16+09:00) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 1.8.0_222, vendor: Private Build, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "4.15.0-62-generic", arch: "amd64", family: "unix" $ $GRAALVM_HOME/bin/native-image --version GraalVM Version 19.1.1 CE
利用するQuarkusのバージョンは、0.22.0です。
準備
プロジェクトの作成。Extensionには、「resteasy-jackson」を指定します。
$ mvn io.quarkus:quarkus-maven-plugin:0.22.0:create \ -DprojectGroupId=org.littlewings \ -DprojectArtifactId=resteasy-jackson \ -DprojectVersion=0.0.1-SNAPSHOT \ -Dextensions="resteasy-jackson"
作成するプロジェクトのアーティファクトIDまで、「resteasy-jackson」にしたのは紛らわしかったですね…。
pom.xmlには、以下の依存関係が追加されます。
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-jackson</artifactId> </dependency>
サンプルコードの作成
書籍を題材にした、簡単なREST APIを書いてみます。
書籍クラスの定義。
src/main/java/org/littlewings/quarkus/resteasyjackson/Book.java
package org.littlewings.quarkus.resteasyjackson; public class Book { private String isbn; private String title; private int price; // getter/setterは省略 }
こちらを扱う、JAX-RSリソースクラス。
src/main/java/org/littlewings/quarkus/resteasyjackson/BookResource.java
package org.littlewings.quarkus.resteasyjackson; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; @Path("book") public class BookResource { Map<String, Book> books = new ConcurrentHashMap<>(); @GET @Produces(MediaType.APPLICATION_JSON) public List<Book> books() { return books .values() .stream() .sorted(Comparator.comparing(Book::getPrice).reversed()) .collect(Collectors.toList()); } @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public Book book(@PathParam("isbn") String isbn) { return books.get(isbn); } @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.TEXT_PLAIN) public String register(Book book) { books.put(book.getIsbn(), book); return "OK!!"; } }
また、REST JSON Serviceのドキュメントを読んでいると、リソースクラスの引数、戻り値に対象となるクラスを使わない場合、
すなわちResponseを使う場合について注意書きがあるので、こちらも含めておきましょう。
ちなみにこれは、Jacksonに限った話ではありません。JSON-Bを使った場合も同じです。
もうひとつ、書籍用のクラスを用意。
src/main/java/org/littlewings/quarkus/resteasyjackson/FixedBook.java
package org.littlewings.quarkus.resteasyjackson; import io.quarkus.runtime.annotations.RegisterForReflection; @RegisterForReflection public class FixedBook { private String isbn; private String title; private int price; public static FixedBook create(String isbn, String title, int price) { FixedBook book = new FixedBook(); book.setIsbn(isbn); book.setTitle(title); book.setPrice(price); return book; } // getter/setterは省略 }
ポイントは、@RegisterForReflectionアノテーションが付与されていること。この効果については、また後ほど記述します。
@RegisterForReflection public class FixedBook {
JAX-RSリソースクラスについては、メソッドの引数や戻り値には書籍クラスを含めない形で作成します。要は、Responseを使います、と。 src/main/java/org/littlewings/quarkus/resteasyjackson/FixedBookResource.java
package org.littlewings.quarkus.resteasyjackson; import java.util.Arrays; import java.util.Comparator; import java.util.List; import java.util.Map; import java.util.stream.Collectors; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; @Path("fixed-book") public class FixedBookResource { Map<String, FixedBook> books = Arrays .asList( FixedBook.create("978-4774183169", "パーフェクト Java EE", 3456), FixedBook.create("978-4798124605", "Beginning Java EE 6", 3891), FixedBook.create("978-4798140926", "Java EE 7徹底入門", 4104) ) .stream() .collect( Collectors .toMap( FixedBook::getIsbn, book -> book ) ); @GET @Produces(MediaType.APPLICATION_JSON) public Response books() { List<FixedBook> sortedBooks = books .values() .stream() .sorted(Comparator.comparing(FixedBook::getPrice).reversed()) .collect(Collectors.toList()); return Response.ok(books).build(); } @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public Response book(@PathParam("isbn") String isbn) { FixedBook book = books.get(isbn); return Response.ok(book).build(); } }
とりあえず、ここまでで最低限確認したいところまでは作ってみました。
確認してみる
まずは、JARにパッケージングして動かしてみましょう。
$ mvn package && java -jar target/resteasy-jackson-0.0.1-SNAPSHOT-runner.jar
データの登録。
$ curl -XPUT -H 'Content-Type: application/json' http://localhost:8080/book -d '{"isbn": "978-4774183169", "title": "パーフェクト Java EE", "price": 3456}' OK!! $ curl -XPUT -H 'Content-Type: application/json' http://localhost:8080/book -d '{"isbn": "978-4798124605", "title": "Beginning Java EE 6", "price": 3891}' OK!! $ curl -XPUT -H 'Content-Type: application/json' http://localhost:8080/book -d '{"isbn": "978-4798140926", "title": "Java EE 7徹底入門", "price": 4104}' OK!!
データの取得。
$ curl localhost:8080/book [{"isbn":"978-4798140926","title":"Java EE 7徹底入門","price":4104},{"isbn":"978-4798124605","title":"Beginning Java EE 6","price":3891},{"isbn":"978-4774183169","title":"パーフェクト Java EE","price":3456}] $ curl localhost:8080/book/978-4774183169 {"isbn":"978-4774183169","title":"パーフェクト Java EE","price":3456}
Responseを使ったパターンの確認。
$ curl localhost:8080/fixed-book {"978-4798124605":{"isbn":"978-4798124605","title":"Beginning Java EE 6","price":3891},"978-4798140926":{"isbn":"978-4798140926","title":"Java EE 7徹底入門","price":4104},"978-4774183169":{"isbn":"978-4774183169","title":"パーフェクト Java EE","price":3456}} $ curl localhost:8080/book/978-4774183169 {"isbn":"978-4774183169","title":"パーフェクト Java EE","price":3456}
ネイティブイメージにして、実行。
$ mvn package -P native && ./target/resteasy-jackson-0.0.1-SNAPSHOT-runner
結果は同じなので、割愛。
@RegisterForReflectionアノテーションを付けなかったら?
ところで、@RegisterForReflectionアノテーションをクラスの宣言に付与しなかったら、どうなるんでしょう?
@RegisterForReflection public class FixedBook {
@RegisterForReflectionアノテーションを付与しなかった場合、ネイティブイメージにしてResponseを使うリソースクラスの
エンドポイントを呼び出した時に、Serializerがないと言われてエラーになります。
$ curl localhost:8080/fixed-book $ curl localhost:8080/book/978-4774183169 Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: No serializer found for class org.littlewings.quarkus.resteasyjackson.FixedBook and no properties discovered to create BeanSerializer (to avoid exception, disable SerializationFeature.FAIL_ON_EMPTY_BEANS) (through reference chain: java.util.HashMap["978-4798124605"])
こちらのように、メソッドの引数や戻り値に対象の型を宣言している場合は、問題なく実行できます。
@GET @Produces(MediaType.APPLICATION_JSON) public List<Book> books() { return books .values() .stream() .sorted(Comparator.comparing(Book::getPrice).reversed()) .collect(Collectors.toList()); } @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public Book book(@PathParam("isbn") String isbn) { return books.get(isbn); } @PUT @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.TEXT_PLAIN) public String register(Book book) { books.put(book.getIsbn(), book); return "OK!!"; }
これは、ドキュメントにも書いてありますが、QuarkusがRESTエンドポイントをビルド時に解析してリフレクションの情報を
登録しているからですね。
As mentioned above, the issue is that Quarkus was not able to determine the Legume class will require some reflection by analyzing the REST endpoints.
このあたりかなぁ、と。
Responseを使う場合や、引数に対象の型が現れない場合はこれができないので、代わりに@RegisterForReflectionアノテーションを
付与して、ビルド時に解析してリフレクションの情報を登録してもらいます。
こちらですね。
ネイティブイメージにして実行する場合は、このあたりにも気をつけましょう、と。
ObjectMapperをカスタマイズする
ところで、RESTEasy Jackson Extensionが提供するデフォルトのObjectMapperは、デフォルトの状態です。
@DefaultBean @Singleton @Produces public ObjectMapper objectMapper() { // in the future we can do a lot more here return new ObjectMapper(); }
これをカスタマイズする場合は、ドキュメントに従い@SingletonであるObjectMapperを作成する、Producerを用意します。
例えば、Pretty Printするような場合、こんな感じに。 src/main/java/org/littlewings/quarkus/resteasyjackson/ObjectMapperProducer.java
package org.littlewings.quarkus.resteasyjackson; import javax.enterprise.context.ApplicationScoped; import javax.enterprise.inject.Produces; import javax.inject.Singleton; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; @ApplicationScoped public class ObjectMapperProducer { @Singleton @Produces public ObjectMapper objectMapper() { return new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); } }
これで、デフォルトで提供されるObjectMapperを差し替えることができます。
最初、なにも考えずに@ApplicationScopedとかにしていたら、動かなくなってハマりました…。
確認。
$ curl localhost:8080/fixed-book { "978-4798124605" : { "isbn" : "978-4798124605", "title" : "Beginning Java EE 6", "price" : 3891 }, "978-4798140926" : { "isbn" : "978-4798140926", "title" : "Java EE 7徹底入門", "price" : 4104 }, "978-4774183169" : { "isbn" : "978-4774183169", "title" : "パーフェクト Java EE", "price" : 3456 } }
Jackson Extensionは?
ところで、RESTEasyがつかない方の、Jackson Extensionはどのようなものなのでしょう?
ソースコードを見ると、runtimeの方はJacksonへの依存関係が定義されているだけです。
https://github.com/quarkusio/quarkus/tree/0.22.0/extensions/jackson/runtime
deploymentの方では、JaxbAnnotationIntrospectorとSqlDateSerializerのリフレクションの情報として登録しています。
このくらいの内容みたいですね。