これは、なにをしたくて書いたもの?
RESTEasy 4.1.0.Finalで追加されたReactorに関する2つのモジュールのうち、Reactorと統合するモジュールについてエントリを
書きました。
RESTEasy × Reactorを試す - CLOVER🍀
もうひとつ、JAX-RS ClientとしてのRESTEasyの、HTTPを取り扱うエンジンをReactor Nettyにするモジュールがあるので、今回は
こちらを使ってみたいと思います。
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と連携し始めたんだなぁという感じですね。