これは、なにをしたくて書いたもの?
以前、RESTEasyとVert.xでこんなエントリを書きました。
RESTEasy+Vert.x(Embedded Container)で遊ぶ - CLOVER🍀
こちらはVertxJaxrsServer
にResteasyDeployment
でデプロイするやり方でしたが、今回はVert.xにHandler
を設定する
やり方でやってみようと思います。
ドキュメントでいくと、ここですね。
Vert.x can also embed a RESTEasy deployment, making easy to use Jax-RS annotated controller in Vert.x applications:
環境
今回の環境は、こちら。
$ java --version openjdk 11.0.9.1 2020-11-04 OpenJDK Runtime Environment (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04) OpenJDK 64-Bit Server VM (build 11.0.9.1+1-Ubuntu-0ubuntu1.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.9.1, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-54-generic", arch: "amd64", family: "unix"
RESTEasyは、4.5.8.Finalを使います。
お題
足し算をするだけの、すごく単純なJAX-RSリソースを書きます。
前回と変えるのは、Vert.xとのインテグレーション方法と、もう少しVert.xで扱うリソースに踏み込んでみようかなと。
準備
Maven依存関係は、まずはこちら。
<dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-vertx</artifactId> <version>4.5.8.Final</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jackson2-provider</artifactId> <version>4.5.8.Final</version> </dependency> <dependency> <groupId>io.vertx</groupId> <artifactId>vertx-core</artifactId> <version>3.9.4</version> </dependency>
JSONも少し扱うので、Jackson2 Providerを足しています。
また、依存関係は後で少し増やします。
vertx-core
については、自分で依存関係を追加する必要があります。RESTEasyが指している依存関係は3.7.1なのですが、
今回はなにも考えずに最新版を追加…。
https://github.com/resteasy/Resteasy/blob/4.5.8.Final/resteasy-dependencies-bom/pom.xml#L34
アプリケーションの雛形
まずは、ざっくりmain
メソッドを持ったクラスを書きます。
src/main/java/org/littlewings/resteasy/vertx/App.java
package org.littlewings.resteasy.vertx; import io.vertx.core.Vertx; import io.vertx.core.http.HttpServer; import org.jboss.logging.Logger; import org.jboss.resteasy.plugins.server.vertx.VertxRequestHandler; import org.jboss.resteasy.plugins.server.vertx.VertxResteasyDeployment; import org.jboss.resteasy.spi.Registry; import org.jboss.resteasy.spi.ResteasyDeployment; public class App { public static void main(String... args) { Logger logger = Logger.getLogger(App.class); Vertx vertx = Vertx.vertx(); HttpServer server = vertx.createHttpServer(); ResteasyDeployment deployment = new VertxResteasyDeployment(); deployment.start(); Registry registry = deployment.getRegistry(); registry.addPerRequestResource(/* JAX-RSリソースクラスを追加 */); server.requestHandler(new VertxRequestHandler(vertx, deployment)); server.listen(8080); logger.infof("start server."); } }
VertxRequestHandler
は、VertxJaxrsServer
の内部でも使われているものになります。
ドキュメント通り、HttpServer#requestHandler
にVertxRequestHandler
をResteasyDeployment
とともに設定すればOK…
と思いきや、少し落とし穴があって。
ここです。
ResteasyDeployment deployment = new VertxResteasyDeployment();
deployment.start();
この形態でやる時には、ResteasyDeployment#start
を呼んでいないとRegistry#addPerRequestResource
や、これを使わない方法で
JAX-RSリソースクラスを追加しても起動時に失敗します。
JAX-RSリソースクラスを追加する前に呼んでおくのがポイントみたいです…。VertxJaxrsServer
を使っている時にはこんなことは
起こりませんでしたが…?
また、Vert.xのスタイルに従うので、ここで追加するJAX-RSリソースクラスはブロックするコードを書いてはいけません。
Registry registry = deployment.getRegistry();
registry.addPerRequestResource(/* JAX-RSリソースクラスを追加 */);
When a resource is called, it is done with the Vert.x Event Loop thread, keep in mind to not block this thread and respect the Vert.x programming model, see the related Vert.x manual page.
シングルトンなJAX-RSリソースクラスとして設定していない場合(=今回のようにaddPerRequestResource
している場合)は、
イベントループの呼び出しごとにJAX-RSリソースクラスのインスタンスが作られます。
// Create an instance of resource per Event Loop
JAX-RSリソースクラスを書く
では、まずシンプルにJAX-RSリソースクラスを書きます。
src/main/java/org/littlewings/resteasy/vertx/CalcResource.java
package org.littlewings.resteasy.vertx; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; @Path("calc") public class CalcResource { @GET @Path("query/add") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN) public int add(@QueryParam("a") int a, @QueryParam("b") int b) { return a + b; } @POST @Path("json/add") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Map<String, Object> calc(Map<String, Object> body) { int a = (Integer) body.get("a"); int b = (Integer) body.get("b"); int result = a + b; return Map.of("result", result); } }
起動。
$ mvn compile exec:java -Dexec.mainClass=org.littlewings.resteasy.vertx.App ... 11月 22, 2020 3:49:49 午後 org.littlewings.resteasy.vertx.App main INFO: start server.
確認。
### GET $ curl -i 'localhost:8080/calc/query/add?a=2&b=3' HTTP/1.1 200 OK transfer-encoding: chunked Content-Type: text/plain;charset=UTF-8 5 ### POST $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/calc/json/add -d '{"a": 1, "b": 5}' HTTP/1.1 200 OK transfer-encoding: chunked Content-Type: application/json {"result":6}
OKです。
@ContextでVert.xのクラスを使う
ドキュメントを読むと、Vert.xに関するオブジェクトをインジェクションできるようです。
Vert.x objects can be injected in annotated resources:
以下の4つが使えるようです。
io.vertx.core.Context
io.vertx.core.http.HttpServerRequest
io.vertx.core.http.HttpServerResponse
io.vertx.core.Vertx
で、これらを使ってこんなJAX-RSリソースクラスを作ってみました。
src/main/java/org/littlewings/resteasy/vertx/VertxCalcResource.java
package org.littlewings.resteasy.vertx; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.http.HttpServerResponse; @Path("vertx/calc") public class VertxCalcResource { @GET @Path("query/add") @Produces(MediaType.TEXT_PLAIN) public int queryAdd(@Context HttpServerRequest request) { int a = Integer.parseInt(request.getParam("a")); int b = Integer.parseInt(request.getParam("b")); return a + b; } @GET @Path("query/add2") @Produces(MediaType.TEXT_PLAIN) public void queryAdd2(@Context HttpServerRequest request, @Context HttpServerResponse response) { int a = Integer.parseInt(request.getParam("a")); int b = Integer.parseInt(request.getParam("b")); int result = a + b; response .putHeader("Content-Length", Integer.toString(Integer.toString(result).length())) .write(Integer.toString(result)); } @POST @Path("json/add") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public void jsonAdd(@Context io.vertx.core.Context context, @Context HttpServerResponse response, Map<String, Object> body) { int a = (Integer) body.get("a"); int b = (Integer) body.get("b"); context.executeBlocking(promise -> { int result = a + b; promise.complete(result); }, res -> response .putHeader("Content-Length", Integer.toString(String.valueOf(res.result()).length())) .write(String.format("{\"result\": %d", res.result()))); } }
まずはこちら。Vert.xのHttpServerRequest
を使って、リクエストの内容を受け取ります。
@GET @Path("query/add") @Produces(MediaType.TEXT_PLAIN) public int queryAdd(@Context HttpServerRequest request) { int a = Integer.parseInt(request.getParam("a")); int b = Integer.parseInt(request.getParam("b")); return a + b; }
確認。
$ curl -i 'localhost:8080/vertx/calc/query/add?a=2&b=3' HTTP/1.1 200 OK transfer-encoding: chunked Content-Type: text/plain;charset=UTF-8 5
こちらはOKです。
問題はこちら。
@GET @Path("query/add2") @Produces(MediaType.TEXT_PLAIN) public void queryAdd2(@Context HttpServerRequest request, @Context HttpServerResponse response) { int a = Integer.parseInt(request.getParam("a")); int b = Integer.parseInt(request.getParam("b")); int result = a + b; response .putHeader("Content-Length", Integer.toString(Integer.toString(result).length())) .write(Integer.toString(result)); }
一見、動くように見えるんですが
$ curl -i 'localhost:8080/vertx/calc/query/add2?a=2&b=3' HTTP/1.1 200 OK Content-Length: 1 5
裏で例外を吐いているのと、よくよく見るとContent-Type
などが設定されていません。
11月 22, 2020 4:09:43 午後 io.vertx.core.impl.ContextImpl 重大: Unhandled exception java.lang.IllegalStateException: Response head already sent at io.vertx.core.http.impl.HttpServerResponseImpl.checkHeadWritten(HttpServerResponseImpl.java:638) at io.vertx.core.http.impl.HttpServerResponseImpl.setStatusCode(HttpServerResponseImpl.java:132) at org.jboss.resteasy.plugins.server.vertx.VertxHttpResponse.prepareChunkStream(VertxHttpResponse.java:158) at org.jboss.resteasy.plugins.server.vertx.VertxHttpResponse.finish(VertxHttpResponse.java:183) at org.jboss.resteasy.plugins.server.vertx.VertxRequestHandler.lambda$handle$0(VertxRequestHandler.java:77)
「すでにヘッダーを送ってしまったよ」と言っているのですが、かといってContent-Length
のヘッダーを付与しないようにすると
$ curl -i 'localhost:8080/vertx/calc/query/add2?a=2&b=3' HTTP/1.1 500 Internal Server Error transfer-encoding: chunked
こうなりますし、Content-Length
を付けなさいと怒られます。
11月 22, 2020 4:10:31 午後 org.jboss.resteasy.plugins.server.vertx.VertxRequestHandler lambda$handle$0 ERROR: RESTEASY019525: Unexpected org.jboss.resteasy.spi.UnhandledException: java.lang.IllegalStateException: You must set the Content-Length header to be the total size of the message body BEFORE sending any data if you are not using HTTP chunked encoding. at org.jboss.resteasy.core.ExceptionHandler.handleApplicationException(ExceptionHandler.java:106) at org.jboss.resteasy.core.ExceptionHandler.handleException(ExceptionHandler.java:372) at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:218) at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:519) at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:261) at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:161)
レスポンスの終了時にいろいろと触るようなので、Vert.xのHttpServerResponse
で結果を送り出すのとは相性が悪いようです。
このあたりに関しては、Vert.xのクラスを使うのではなく、素直にJAX-RSのリクエスト、レスポンスの仕組みに乗った方が
よさそうですね。
もうひとつ、仮にブロックするコードを書くならこんなVert.xのContext
を使ってこんな感じかな?と。
@POST @Path("json/add") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public void jsonAdd(@Context io.vertx.core.Context context, @Context HttpServerResponse response, Map<String, Object> body) { int a = (Integer) body.get("a"); int b = (Integer) body.get("b"); context.executeBlocking(promise -> { int result = a + b; promise.complete(result); }, res -> response .putHeader("Content-Length", Integer.toString(String.valueOf(res.result()).length())) .write(String.format("{\"result\": %d", res.result()))); }
先ほどの結果とHttpServerResponse
を使っていることからうまくいかなさそうなのは想像に難くないのですが、実行すると
こうなります。
$ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/vertx/calc/json/add -d '{"a": 1, "b": 5}' HTTP/1.1 204 No Content
HttpServerResponse
が見つからないと…。
11月 22, 2020 4:19:05 午後 io.vertx.core.impl.ContextImpl 重大: Unhandled exception org.jboss.resteasy.spi.LoggableFailure: RESTEASY003880: Unable to find contextual data of type: io.vertx.core.http.HttpServerResponse at org.jboss.resteasy.core.ContextParameterInjector$GenericDelegatingProxy.invoke(ContextParameterInjector.java:124) at com.sun.proxy.$Proxy43.putHeader(Unknown Source) at org.littlewings.resteasy.vertx.VertxCalcResource.lambda$jsonAdd$1(VertxCalcResource.java:53) at io.vertx.core.impl.ContextImpl.lambda$null$0(ContextImpl.java:327) at io.vertx.core.impl.ContextImpl.executeTask(ContextImpl.java:366) at io.vertx.core.impl.EventLoopContext.lambda$executeAsync$0(EventLoopContext.java:38) at io.netty.util.concurrent.AbstractEventExecutor.safeExecute(AbstractEventExecutor.java:164) at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:472)
よくよく見ると、ここでResteasyContext
に登録している先はThreadLocal
なので、そりゃあそうか、と…。
ResteasyContext.pushContext(Context.class, context); ResteasyContext.pushContext(HttpServerRequest.class, req); ResteasyContext.pushContext(HttpServerResponse.class, resp); ResteasyContext.pushContext(Vertx.class, context.owner());
とすると、RESTEasy内でVert.xの仕組みを使って「ブロックする処理を書くな」はわかりますが、「書けない」になってるような。
こうなると、RESTEasyと統合するよりもVert.xだけで処理を書いた方が良さげな気がしますね?
Reactorを使う
それならということで、少し視点を変えてReactive Streamsの実装も入れ込むことにしましょう。
今回はReactorを使います。
<dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-reactor</artifactId> <version>4.5.8.Final</version> </dependency> <dependency> <groupId>io.projectreactor</groupId> <artifactId>reactor-core</artifactId> <version>3.4.0</version> </dependency> <dependency> <groupId>javax.enterprise</groupId> <artifactId>cdi-api</artifactId> <version>2.0.SP1</version> </dependency>
Reactorも、とりあえずRESTEasyが見ているものは置いておいて、最新版を入れました。
https://github.com/resteasy/Resteasy/blob/4.5.8.Final/resteasy-dependencies-bom/pom.xml#L102
どうやらAnnotationLiteral
など一部のクラスに依存があるようで、CDIのAPIを依存関係に入れないと起動しませんでした…。
Reactorを使って作成した、JAX-RSリソースクラス。
src/main/java/org/littlewings/resteasy/vertx/ReactorCalcResource.java
package org.littlewings.resteasy.vertx; import java.util.Map; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.MediaType; import reactor.core.publisher.Mono; @Path("reactor/calc") public class ReactorCalcResource { @GET @Path("query/add") @Consumes(MediaType.TEXT_PLAIN) @Produces(MediaType.TEXT_PLAIN) public Mono<Integer> queryAdd(@QueryParam("a") int a, @QueryParam("b") int b) { return Mono.just(a + b); } @POST @Path("json/add") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Mono<Map<String, Object>> jsonAdd(Map<String, Object> body) { return Mono .just(body) .map(b -> Map.of("result", ((Integer) b.get("a")) + ((Integer) b.get("b")))); } }
こちらは問題なく動作します。
$ curl -i 'localhost:8080/reactor/calc/query/add?a=2&b=3' HTTP/1.1 200 OK transfer-encoding: chunked Content-Type: text/plain;charset=UTF-8 5 $ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/reactor/calc/json/add -d '{"a": 1, "b": 5}' HTTP/1.1 200 OK transfer-encoding: chunked Content-Type: application/json {"result":6}
動作しますが、これをやるならReactorではなくてMutinyかなぁという気もしますし、そこまでやるならQuarkusに行った方が…
という気も(CDIも使えるし)。
まとめ
RESTEasy+Vert.xということで、Vert.xのHandlerを直接使う形でコードを書きつつ、少し脱線まで。
RESTEasy内で、ムリにVert.xの存在を見せない方がいいなーと思うようにはなりました。
だいぶVert.x側の雰囲気もわかってきたので、ほどほどに使い分けをしつつという感じでいきましょう。