CLOVER🍀

That was when it all began.

RESTEasy JAX-RS Client × Reactor Netty

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

RESTEasy 4.1.0.Finalで追加されたReactorに関する2つのモジュールのうち、Reactorと統合するモジュールについてエントリを
書きました。

RESTEasy × Reactorを試す - CLOVER🍀

もうひとつ、JAX-RS ClientとしてのRESTEasyの、HTTPを取り扱うエンジンをReactor Nettyにするモジュールがあるので、今回は
こちらを使ってみたいと思います。

Reactor Netty Client Engine

ClientHttpEngine

RESTEasyのJAX-RS Clientは、HTTP通信に関する部分がClientHttpEngineインターフェースとして抽象化されています。

Apache HTTP Client 4.x and other backends

デフォルトではApache HttpComponentsおよびApache HttpClientですが、他にもClientHttpEngineの実装があり、これを差し替えることが
できます。

ClientHttpEngineの種類としては、これまで以下がありました。

  • Apache HttpComponents + Apache HttpClient
  • Apache HttpComponents AsyncClient
  • Jetty
  • java.net.HttpURLConnection

これに、Reactor Nettyを使うものが加わりました。

といっても、ClientHttpEngineの切り替え方は単純で、RESTEasyのJAX-RS Clientを作る時に作成したClientHttpEngineのインスタンス
設定するだけです。

        ClientHttpEngine engine =
                new ReactorNettyClientHttpEngine(
                        HttpClient.create(),
                        new DefaultChannelGroup(new DefaultEventExecutor()),
                        HttpResources.get());
        Client client =
                ((ResteasyClientBuilder) ClientBuilder.newBuilder())
                        .httpEngine(engine).build();

あとは、通常通りのJAX-RS Clientとして利用することができます。

では、使っていってみましょう。

環境

今回の環境は、こちらです。

$ java -version
openjdk version "11.0.3" 2019-04-16
OpenJDK Runtime Environment (build 11.0.3+7-Ubuntu-1ubuntu218.04.1)
OpenJDK 64-Bit Server VM (build 11.0.3+7-Ubuntu-1ubuntu218.04.1, mixed mode, sharing)


$ mvn -version
Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-05T04:00:29+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.3, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-54-generic", arch: "amd64", family: "unix"

準備

Maven依存関係は、こちら。

    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-undertow</artifactId>
            <version>4.1.0.Final</version>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jackson2-provider</artifactId>
            <version>4.1.0.Final</version>
        </dependency>


        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-client-reactor-netty</artifactId>
            <version>4.1.0.Final</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>1.7.26</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.4.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.4.2</version>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.12.2</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.22.0</version>
            </plugin>
        </plugins>
    </build>

Reactor NettyによるClientHttpEngineの実装を利用するには、「resteasy-client-reactor-netty」モジュールが必要です。

また、「resteasy-client-reactor-netty」モジュールにはSLF4Jが必要なようなのですが、「resteasy-client-reactor-netty」側での
スコープが「provided」になっているので、自分で追加する必要があります。

「resteasy-client-reactor-netty」は今回testスコープとしているのですが、動作確認の方法として簡単なJAX-RS Serverアプリケーションを
RESTEasy+Undertowで作成し、これをテスト側で「resteasy-client-reactor-netty」を使って確認する、というシナリオで
いこうかなと思います。

なお、今回はサーバー側にはReactorは使用しません。

サーバー側のコード

最初に、RESTEasy JAX-RS Clientからアクセスする、サーバープログラムを書いていきます。サーバー側は、書籍をお題にした
単純なものにします。

書籍に関するクラス。
src/main/java/org/littlewings/resteasy/reactor/Book.java

package org.littlewings.resteasy.reactor;

public class Book {
    private String isbn;
    private String title;
    private 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は省略
}

書籍を扱う、JAX-RSリソースクラス。書籍の1件取得、全件取得、登録ができます。データの置き場所には、データベースなどは使わず
簡単にMapで済ませました。
src/main/java/org/littlewings/resteasy/reactor/BookResource.java

package org.littlewings.resteasy.reactor;

import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
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;

    public BookResource() {
        Map<String, Book> tmpBooks = new LinkedHashMap<>();
        tmpBooks.put("978-4774182179", Book.create("978-4774182179", "[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ", 4104));
        tmpBooks.put("978-4798142470", Book.create("978-4798142470", "Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発", 4320));

        books = Collections.synchronizedMap(tmpBooks);
    }

    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public Book get(@PathParam("isbn") String isbn) {
        return books.get(isbn);
    }

    @GET
    @Produces(MediaType.APPLICATION_JSON)
    public List<Book> all() {
        return new ArrayList<>(books.values());
    }

    @PUT
    @Path("{isbn}")
    @Consumes(MediaType.APPLICATION_JSON)
    public void register(@PathParam("isbn") String isbn, Book book) {
        books.put(isbn, book);
    }
}

Applicationのサブクラス。
src/main/java/org/littlewings/resteasy/reactor/JaxrsActivator.java

package org.littlewings.resteasy.reactor;

import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("")
public class JaxrsActivator extends Application {
    Set<Object> singletons;

    public JaxrsActivator() {
        singletons = new HashSet<>();
        singletons.add(new BookResource());
    }

    @Override
    public Set<Object> getSingletons() {
        return singletons;
    }
}

起動クラス。
src/main/java/org/littlewings/resteasy/reactor/Server.java

package org.littlewings.resteasy.reactor;

import io.undertow.Undertow;
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;

public class Server {
    UndertowJaxrsServer server;
    int port;

    public static void main(String... args) {
        Server server = new Server();
        server.start();
    }

    public Server() {
        this(8080);
    }

    public Server(int port) {
        this.port = port;
    }

    public void start() {
        server = new UndertowJaxrsServer();
        server.deploy(JaxrsActivator.class);

        server.start(Undertow.builder().addHttpListener(port, "localhost"));
    }

    public void shutdown() {
        server.stop();
    }
}

これで、サーバー側の準備は完了です。

RESTEasy JAX-RS Client + Reactor Netty

それでは、RESTEasyのJAX-RS Clientと、Reactor Nettyを組み合わせて動作確認していってみましょう。

テストコードの雛形は、こちら。
src/test/java/org/littlewings/resteasy/reactor/ReactorNettyClientTest.java

package org.littlewings.resteasy.reactor;

import java.util.List;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;

import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.DefaultEventExecutor;
import org.jboss.resteasy.client.jaxrs.ClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.engines.ReactorNettyClientHttpEngine;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.netty.http.HttpResources;
import reactor.netty.http.client.HttpClient;

import static org.assertj.core.api.Assertions.assertThat;

public class ReactorNettyClientTest {
    Server server;

    @BeforeEach
    public void setUp() {
        server = new Server();
        server.start();
    }

    @AfterEach
    public void tearDown() {
        server.shutdown();
    }

    // ここに、テストを書く!
}

では、ISBNを指定して、書籍を1件取得してみます。

    @Test
    public void getSingleBook() {
        ClientHttpEngine engine = new ReactorNettyClientHttpEngine(
                HttpClient.create(),
                new DefaultChannelGroup(new DefaultEventExecutor()),
                HttpResources.get());
        Client client =
                ((ResteasyClientBuilder) ClientBuilder.newBuilder())
                        .httpEngine(engine).build();

        Response response =
                client
                        .target(UriBuilder.fromUri("http://localhost:8080/book/{isbn}").build("978-4774182179"))
                        .request()
                        .get();

        Book book = response.readEntity(Book.class);
        assertThat(book.getIsbn()).isEqualTo("978-4774182179");
        assertThat(book.getTitle()).isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ");
        assertThat(book.getPrice()).isEqualTo(4104);

        response.close();
        client.close();
    }

こちらの部分以外は、いたってふつうなJAX-RS Clientを使ったプログラムです。

        ClientHttpEngine engine = new ReactorNettyClientHttpEngine(
                HttpClient.create(),
                new DefaultChannelGroup(new DefaultEventExecutor()),
                HttpResources.get());
        Client client =
                ((ResteasyClientBuilder) ClientBuilder.newBuilder())
                        .httpEngine(engine).build();

ReactorNettyClientHttpEngineのコンストラクタ引数がいろいろ気になるところですが、

  • Reactor NettyのHttpClient
  • NettyのChannelGroup
  • Reactor NettyのConnectionProvider

を渡すことになります。それぞれの詳細は、Reactor NettyおよびNettyの方を調べましょう、と。

ReactorNettyClientHttpEngine / Constructor

書籍全件取得。

    @Test
    public void getAllBooks() {
        ClientHttpEngine engine = new ReactorNettyClientHttpEngine(
                HttpClient.create(),
                new DefaultChannelGroup(new DefaultEventExecutor()),
                HttpResources.get());
        Client client =
                ((ResteasyClientBuilder) ClientBuilder.newBuilder())
                        .httpEngine(engine).build();

        Response response =
                client
                        .target("http://localhost:8080/book")
                        .request()
                        .get();

        List<Book> books = response.readEntity(new GenericType<List<Book>>() {
        });

        assertThat(books).hasSize(2);

        assertThat(books.get(0).getIsbn()).isEqualTo("978-4774182179");
        assertThat(books.get(0).getTitle()).isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ");
        assertThat(books.get(0).getPrice()).isEqualTo(4104);
        assertThat(books.get(1).getIsbn()).isEqualTo("978-4798142470");
        assertThat(books.get(1).getTitle()).isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発");
        assertThat(books.get(1).getPrice()).isEqualTo(4320);

        response.close();
        client.close();
    }

書籍の登録と、登録した書籍の取得。

    @Test
    public void putBookAndGet() {
        ClientHttpEngine engine = new ReactorNettyClientHttpEngine(
                HttpClient.create(),
                new DefaultChannelGroup(new DefaultEventExecutor()),
                HttpResources.get());
        Client client =
                ((ResteasyClientBuilder) ClientBuilder.newBuilder())
                        .httpEngine(engine).build();

        Response putResponse =
                client
                        .target(UriBuilder.fromUri("http://localhost:8080/book/{isbn}").build("978-4777519699"))
                        .request()
                        .put(
                                Entity.entity(
                                        Book.create("978-4777519699", "はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発", 2700),
                                        MediaType.APPLICATION_JSON_TYPE
                                )
                        );

        putResponse.close();

        Response getResponse =
                client
                        .target(UriBuilder.fromUri("http://localhost:8080/book/{isbn}").build("978-4777519699"))
                        .request()
                        .get();

        Book book = getResponse.readEntity(Book.class);
        assertThat(book.getIsbn()).isEqualTo("978-4777519699");
        assertThat(book.getTitle()).isEqualTo("はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発");
        assertThat(book.getPrice()).isEqualTo(2700);


        client.close();
    }

OKです。

オマケ:RxInvokerを使う

オマケとして、Reactor向けのRxInvokerを使うサンプルも作成してみましょう。

Maven依存関係に、以下のモジュールを加えます。

        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-reactor</artifactId>
            <version>4.1.0.Final</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>io.projectreactor</groupId>
            <artifactId>reactor-test</artifactId>
            <version>3.2.9.RELEASE</version>
            <scope>test</scope>
        </dependency>

RESTEasyのReactor向けモジュールと、Reactorのテスト用モジュールです。

先ほどのプログラムを、RxInvokerとMonoを使うように修正したのが、こちら。
src/test/java/org/littlewings/resteasy/reactor/ReactiveReactorNettyClientTest.java

package org.littlewings.resteasy.reactor;

import java.util.List;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.GenericType;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;

import io.netty.channel.group.DefaultChannelGroup;
import io.netty.util.concurrent.DefaultEventExecutor;
import org.jboss.resteasy.client.jaxrs.ClientHttpEngine;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.engines.ReactorNettyClientHttpEngine;
import org.jboss.resteasy.reactor.MonoRxInvoker;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.netty.http.HttpResources;
import reactor.netty.http.client.HttpClient;
import reactor.test.StepVerifier;

import static org.assertj.core.api.Assertions.assertThat;

public class ReactiveReactorNettyClientTest {
    Server server;

    @BeforeEach
    public void setUp() {
        server = new Server();
        server.start();
    }

    @AfterEach
    public void tearDown() {
        server.shutdown();
    }

    @Test
    public void getSingleBook() {
        ClientHttpEngine engine =
                new ReactorNettyClientHttpEngine(
                        HttpClient.create(),
                        new DefaultChannelGroup(new DefaultEventExecutor()),
                        HttpResources.get());
        Client client =
                ((ResteasyClientBuilder) ClientBuilder.newBuilder())
                        .httpEngine(engine).build();

        Mono<Book> bookMono =
                client
                        .target(UriBuilder.fromUri("http://localhost:8080/book/{isbn}").build("978-4774182179"))
                        .request()
                        .rx(MonoRxInvoker.class)
                        .get(Book.class);

        StepVerifier
                .create(bookMono)
                .consumeNextWith(book -> {
                    assertThat(book.getIsbn()).isEqualTo("978-4774182179");
                    assertThat(book.getTitle()).isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ");
                    assertThat(book.getPrice()).isEqualTo(4104);
                })
                .verifyComplete();

        client.close();
    }

    @Test
    public void getAllBooks() {
        ClientHttpEngine engine =
                new ReactorNettyClientHttpEngine(
                        HttpClient.create(),
                        new DefaultChannelGroup(new DefaultEventExecutor()),
                        HttpResources.get());
        Client client =
                ((ResteasyClientBuilder) ClientBuilder.newBuilder())
                        .httpEngine(engine).build();

        Mono<List<Book>> booksMono =
                client
                        .target("http://localhost:8080/book")
                        .request()
                        .rx(MonoRxInvoker.class)
                        .get(new GenericType<List<Book>>() {
                        });

        StepVerifier
                .create(booksMono)
                .consumeNextWith(books -> {
                    assertThat(books).hasSize(2);

                    assertThat(books.get(0).getIsbn()).isEqualTo("978-4774182179");
                    assertThat(books.get(0).getTitle()).isEqualTo("[改訂新版]Spring入門 ――Javaフレームワーク ・より良い設計とアーキテクチャ");
                    assertThat(books.get(0).getPrice()).isEqualTo(4104);
                    assertThat(books.get(1).getIsbn()).isEqualTo("978-4798142470");
                    assertThat(books.get(1).getTitle()).isEqualTo("Spring徹底入門 Spring FrameworkによるJavaアプリケーション開発");
                    assertThat(books.get(1).getPrice()).isEqualTo(4320);
                })
                .verifyComplete();

        client.close();
    }

    @Test
    public void putBookAndGet() {
        ClientHttpEngine engine =
                new ReactorNettyClientHttpEngine(
                        HttpClient.create(),
                        new DefaultChannelGroup(new DefaultEventExecutor()),
                        HttpResources.get());
        Client client =
                ((ResteasyClientBuilder) ClientBuilder.newBuilder())
                        .httpEngine(engine).build();

        Mono<Response> putMono =
                client
                        .target(UriBuilder.fromUri("http://localhost:8080/book/{isbn}").build("978-4777519699"))
                        .request()
                        .rx(MonoRxInvoker.class)
                        .put(
                                Entity.entity(
                                        Book.create("978-4777519699", "はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発", 2700),
                                        MediaType.APPLICATION_JSON_TYPE
                                )
                        );

        Mono<Book> bookMono =
                putMono
                        .flatMap(response ->
                                client
                                        .target(UriBuilder.fromUri("http://localhost:8080/book/{isbn}").build("978-4777519699"))
                                        .request()
                                        .rx(MonoRxInvoker.class)
                                        .get(Book.class)
                        );

        StepVerifier
                .create(bookMono)
                .consumeNextWith(book -> {
                    assertThat(book.getIsbn()).isEqualTo("978-4777519699");
                    assertThat(book.getTitle()).isEqualTo("はじめてのSpring Boot―スプリング・フレームワークで簡単Javaアプリ開発");
                    assertThat(book.getPrice()).isEqualTo(2700);
                })
                .verifyComplete();

        client.close();
    }
}

アサーションは、Reactor TestのStep Verifierで行っています。

こちらは、さらっと。

まとめ

RESTEasy JAX-RS Clientで、ClientHttpEngineをReactor Nettyを使うようにしてみました。

ふつうに使う分にはさらっと済むのですが、実はサーバーサイドもFluxとかを使うとハマりにハマって諦めたので、今回はサーバー側は
通常のJAX-RSリソースクラスのまま突き通しました…。

できれば、全体的にReactiveな感じの構成で試してみたいので、またどこかでチャレンジするかもしれません…。

それにしても、ついにReactorと連携し始めたんだなぁという感じですね。