CLOVER🍀

That was when it all began.

RESTEasy+Vert.x(Handler)で遊ぶ

これは、なにをしたくて書いたもの?

以前、RESTEasyとVert.xでこんなエントリを書きました。

RESTEasy+Vert.x(Embedded Container)で遊ぶ - CLOVER🍀

こちらはVertxJaxrsServerResteasyDeploymentでデプロイするやり方でしたが、今回はVert.xにHandlerを設定する
やり方でやってみようと思います。

ドキュメントでいくと、ここですね。

Vert.x can also embed a RESTEasy deployment, making easy to use Jax-RS annotated controller in Vert.x applications:

Vert.x

環境

今回の環境は、こちら。

$ 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の内部でも使われているものになります。

https://github.com/resteasy/Resteasy/blob/4.5.8.Final/server-adapters/resteasy-vertx/src/main/java/org/jboss/resteasy/plugins/server/vertx/VertxJaxrsServer.java#L262

ドキュメント通り、HttpServer#requestHandlerVertxRequestHandlerResteasyDeploymentとともに設定すればOK…
と思いきや、少し落とし穴があって。

Vert.x

ここです。

        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:

Vert.x

以下の4つが使えるようです。

  • io.vertx.core.Context
  • io.vertx.core.http.HttpServerRequest
  • io.vertx.core.http.HttpServerResponse
  • io.vertx.core.Vertx

https://github.com/resteasy/Resteasy/blob/4.5.8.Final/server-adapters/resteasy-vertx/src/main/java/org/jboss/resteasy/plugins/server/vertx/RequestDispatcher.java#L90-L93

で、これらを使ってこんな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で結果を送り出すのとは相性が悪いようです。

https://github.com/resteasy/Resteasy/blob/4.5.8.Final/server-adapters/resteasy-vertx/src/main/java/org/jboss/resteasy/plugins/server/vertx/VertxHttpResponse.java

このあたりに関しては、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());

https://github.com/resteasy/Resteasy/blob/4.5.8.Final/server-adapters/resteasy-vertx/src/main/java/org/jboss/resteasy/plugins/server/vertx/RequestDispatcher.java#L90-L93

とすると、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など一部のクラスに依存があるようで、CDIAPIを依存関係に入れないと起動しませんでした…。

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側の雰囲気もわかってきたので、ほどほどに使い分けをしつつという感じでいきましょう。