これは、なにをしたくて書いたもの?
Eclipse MicroProfileに含まれている、Rest Clientをちょっと見ておこうかなと。
今まで何回か別のテーマを扱っている時に使っているのですが、完全に雰囲気で使っていたのでこの機会に、と。
Eclipse MicroProfile Rest Client
Eclipse MicroProfile Rest Clientは、Eclipse MicroProfileのプロジェクトのひとつです。
eclipse/microprofile-rest-client / MicroProfile Rest Client
Eclipse MicroProfile 6.1にはRest Client 3.0が含まれています。
GitHub - eclipse/microprofile-rest-client: MicroProfile Rest Client
Releaseから参照可能な
Release MicroProfile Rest Client 3.0 · eclipse/microprofile-rest-client · GitHub
仕様書とAPIリファレンスが主なリソースになります。
Rest Clientがどういうものかというと、GitHubリポジトリーのREADME.md
を見るのがわかりやすいと思うのですが、
Jakarta RESTful Web Services(JAX-RS)のアノテーションを使ったインターフェースを定義して
@Path("/movies") public interface MovieReviewService { @GET Set<Movie> getAllMovies(); @GET @Path("/{movieId}/reviews") Set<Review> getAllReviews( @PathParam("movieId") String movieId ); @GET @Path("/{movieId}/reviews/{reviewId}") Review getReview( @PathParam("movieId") String movieId, @PathParam("reviewId") String reviewId ); @POST @Path("/{movieId}/reviews") String submitReview( @PathParam("movieId") String movieId, Review review ); @PUT @Path("/{movieId}/reviews/{reviewId}") Review updateReview( @PathParam("movieId") String movieId, @PathParam("reviewId") String reviewId, Review review ); }
このインターフェースに対するクライアントを生成することができます。
URI apiUri = new URI("http://localhost:9080/movieReviewService"); MovieReviewService reviewSvc = RestClientBuilder.newBuilder() .baseUri(apiUri) .build(MovieReviewService.class); Review review = new Review(3 /* stars */, "This was a delightful comedy, but not terribly realistic."); reviewSvc.submitReview( movieId, review );
インターフェースは基本的にサーバー側と同じものを使い、ヘッダーは@HeaderParam
を使うようになりますが、クライアント側の
宣言としてだけ入れたい場合は@ClientHeaderParam
ヘッダーを使うことになるようです。
MicroProfile Rest Client Definition Examples / Specifying Additional Client Headers
これには動的な値も指定可能ですが、インターフェース内のデフォルトメソッドか他のクラスのstaticメソッドを指定することに
なるようです。
また、CDIが使える場合は@RegisterRestClient
アノテーションを付与したインターフェースに対してRest Clientを生成して
package com.mycompany.remoteServices; @RegisterRestClient(baseUri="http://someHost/someContextRoot") public interface MyServiceClient { @GET @Path("/greet") Response greet(); }
CDI管理Beanとしてインジェクションすることも可能なようです。
@ApplicationScoped public class MyService { @Inject @RestClient private MyServiceClient client; }
MicroProfile Rest Client CDI Support
さらにCDIが使える場合は、Eclipse MicroProfile Configを使ってRest Clientの設定もできるようです。
MicroProfile Rest Client CDI Support / Support for MicroProfile Config
リクエストするURL、タイムアウト、リダイレクトへの追従、プロキシ設定などが可能なようです。
その他、FilterやBodyWriter、Converter、InterceptorといったRest Client用のProviderなどの記載もあったりします。
MicroProfile Rest Client Provider Registration
Rest Clientの実装としては、以下の4つです。
- Apache CXF
- Open Liberty
- RESTEasy
- Jersey
今回はRESTEasy単体、それからWildFlyを使って試してみたいと思います。
RESTEasyでのドキュメントはこちらですね。
Chapter 51. MicroProfile Rest Client
ソースコードは、RESTEasy本体とは別のリポジトリーにあるようです。
GitHub - resteasy/resteasy-microprofile: RESTEasy MicroProfile
お題
お題は足し算にします。
以下のようなJAX-RSリソース定義に対して
@Path("calc") public interface CalcResource { @GET @Path("plus") @Produces(MediaType.APPLICATION_JSON) CalcResult plus(@QueryParam("a") int a, @QueryParam("b") int b); @POST @Path("plus") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) CalcResult plus(CalcRequest calcRequest); } public class CalcRequest { private int a; private int b; // getter/setterは省略 } public class CalcResult { private int result; // getter/setterは省略 }
以下のパターンでEclipse MicroProfile Rest Clientを使うことを考えてみます。
環境
今回の環境は、こちら。
$ java --version openjdk 21.0.1 2023-10-17 OpenJDK Runtime Environment (build 21.0.1+12-Ubuntu-222.04) OpenJDK 64-Bit Server VM (build 21.0.1+12-Ubuntu-222.04, mixed mode, sharing) $ mvn --version Apache Maven 3.9.6 (bc0240f3c744dd6b6ec2920b3cd08dcc295161ae) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 21.0.1, vendor: Private Build, runtime: /usr/lib/jvm/java-21-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.15.0-91-generic", arch: "amd64", family: "unix"
WildFlyも使いますが、バージョンは30.0.1.Finalとします。
Java SE環境(CDIなし)でEclipse MicroProfile Rest Clientを試す
まずは、Java SE環境でEclipse MicroProfile Rest Clientを試します。ここでCDIを使うパターンを試さないのは、Java SE環境でCDIを
使うことはそんなにないだろうと思うからですね…。
Maven依存関係など。
<properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>org.jboss.resteasy.microprofile</groupId> <artifactId>microprofile-rest-client</artifactId> <version>2.1.5.Final</version> </dependency> <dependency> <groupId>io.smallrye.config</groupId> <artifactId>smallrye-config</artifactId> <version>3.4.4</version> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-jackson2-provider</artifactId> <version>6.2.7.Final</version> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>5.10.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <version>3.25.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.jboss.resteasy</groupId> <artifactId>resteasy-undertow</artifactId> <version>6.2.7.Final</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>3.1.2</version> </plugin> </plugins> </build>
使用するのは、microprofile-rest-clientです。
<dependency> <groupId>org.jboss.resteasy.microprofile</groupId> <artifactId>microprofile-rest-client</artifactId> <version>2.1.5.Final</version> </dependency>
また、実行にはEclipse MicroProfile Configの実装も必要です。今回は、SmallRye Configを使いました。
<dependency> <groupId>io.smallrye.config</groupId> <artifactId>smallrye-config</artifactId> <version>3.4.4</version> </dependency>
resteasy-jackson2-providerはJSONを扱うためで、それ以外はテスト用ですね。テストではUndertowを使ってテスト用のJAX-RSサーバーの
ソースコードを書きます。
Rest Clientで使うインターフェースを定義。
src/main/java/org/littlewings/resteasy/client/CalcResource.java
package org.littlewings.resteasy.client; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; @Path("calc") public interface CalcResource { @GET @Path("plus") @Produces(MediaType.APPLICATION_JSON) CalcResult plus(@QueryParam("a") int a, @QueryParam("b") int b); @POST @Path("plus") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) CalcResult plus(CalcRequest calcRequest); }
src/main/java/org/littlewings/resteasy/client/CalcRequest.java
package org.littlewings.resteasy.client; public class CalcRequest { private int a; private int b; public static CalcRequest create(int a, int b) { CalcRequest calcRequest = new CalcRequest(); calcRequest.a = a; calcRequest.b = b; return calcRequest; } // getter/setterは省略 }
計算結果。
src/main/java/org/littlewings/resteasy/client/CalcResult.java
package org.littlewings.resteasy.client; public class CalcResult { private int result; public static CalcResult create(int result) { CalcResult calcResult = new CalcResult(); calcResult.result = result; return calcResult; } // getter/setterは省略 }
では、こちらを使うテストコードで確認していくのですが、アクセスするHTTPサーバーが必要です。
これはRESTEasyとUndertowの組み合わせで作成。インターフェースは、Rest Client用に定義したものをそのまま実装しました。
src/test/java/org/littlewings/resteasy/client/TestServer.java
package org.littlewings.resteasy.client; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer; import java.util.Set; public class TestServer { private UndertowJaxrsServer server; protected TestServer(UndertowJaxrsServer server) { this.server = server; } public static TestServer start(int port) { UndertowJaxrsServer server = new UndertowJaxrsServer(); server.setPort(port); server.deploy(JaxrsActivator.class); server.start(); return new TestServer(server); } public void stop() { server.stop(); } @ApplicationPath("") public static class JaxrsActivator extends Application { @Override public Set<Class<?>> getClasses() { return Set.of(CalcResourceServer.class); } } public static class CalcResourceServer implements CalcResource { @Override public CalcResult plus(int a, int b) { return CalcResult.create(a + b); } @Override public CalcResult plus(CalcRequest calcRequest) { return CalcResult.create(calcRequest.getA() + calcRequest.getB()); } } }
そして、Rest Clientを使ったテストコード。
src/test/java/org/littlewings/resteasy/client/MicroProfileRestClientTest.java
package org.littlewings.resteasy.client; import org.eclipse.microprofile.rest.client.RestClientBuilder; import org.junit.jupiter.api.Test; import java.net.URI; import static org.assertj.core.api.Assertions.assertThat; class MicroProfileRestClientTest { @Test void get() { TestServer server = TestServer.start(8090); CalcResource calcResource = RestClientBuilder .newBuilder() .baseUri(URI.create("http://localhost:8090")) .build(CalcResource.class); CalcResult result = calcResource.plus(5, 3); assertThat(result.getResult()).isEqualTo(8); server.stop(); } @Test void post() { TestServer server = TestServer.start(8090); CalcResource calcResource = RestClientBuilder .newBuilder() .baseUri(URI.create("http://localhost:8090")) .build(CalcResource.class); CalcResult result = calcResource.plus(CalcRequest.create(2, 7)); assertThat(result.getResult()).isEqualTo(9); server.stop(); } }
およそ使い方は見ればわかりますが、RestClientBuilder
を使い、作成したインターフェースを指定してbuild
してインターフェースに
対するインタンスを取得します。
あとは、エンドポイントに対応するメソッドを呼び出すだけですね。
// GET CalcResource calcResource = RestClientBuilder .newBuilder() .baseUri(URI.create("http://localhost:8090")) .build(CalcResource.class); CalcResult result = calcResource.plus(5, 3); // POST CalcResource calcResource = RestClientBuilder .newBuilder() .baseUri(URI.create("http://localhost:8090")) .build(CalcResource.class); CalcResult result = calcResource.plus(CalcRequest.create(2, 7));
MicroProfile Rest Client Programmatic Lookup
とても簡単ですね。
Jakarta EE 10環境(WildFly上)で実行
次は、Jakarta EE 10環境(WildFly上)で実行してみます。実際に使う時は、Jakarta EEサーバー上で使うことの方が多いでしょう。
WildFlyのWeb Profile(standalone.xml
)にはMicroProfile Rest Client、MicroProfile Configのどちらも含まれているようなので、
そのままWeb Profileを使います。
2つのWebアプリケーションを作成して、以下のような構成にしてみたいと思います。
flowchart LR クライアント --> |curl/HTTP| A subgraph WildFly A[JAX-RS Server/API-A] A --> |MicroProfile REST Client/HTTP| B[JAX-RS Server/API-B] end
お題が足し算なのは変わりません。
使用するWildFlyのバージョンはこちら。
$ bin/standalone.sh --version ========================================================================= JBoss Bootstrap Environment JBOSS_HOME: /opt/wildfly JAVA: /opt/java/openjdk/bin/java JAVA_OPTS: -Djdk.serialFilter="maxbytes=10485760;maxdepth=128;maxarray=100000;maxrefs=300000" -Xms64m -Xmx512m -XX:MetaspaceSize=96M -XX:MaxMetaspaceSize=256m -Djava.net.preferIPv4Stack=true -Djboss.modules.system.pkgs=org.jboss.byteman -Djava.awt.headless=true --add-exports=java.desktop/sun.awt=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.ldap=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.url.ldap=ALL-UNNAMED --add-exports=java.naming/com.sun.jndi.url.ldaps=ALL-UNNAMED --add-exports=jdk.naming.dns/com.sun.jndi.dns=ALL-UNNAMED --add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.lang.invoke=ALL-UNNAMED --add-opens=java.base/java.lang.reflect=ALL-UNNAMED --add-opens=java.base/java.io=ALL-UNNAMED --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.security=ALL-UNNAMED --add-opens=java.base/java.util=ALL-UNNAMED --add-opens=java.base/java.util.concurrent=ALL-UNNAMED --add-opens=java.management/javax.management=ALL-UNNAMED --add-opens=java.naming/javax.naming=ALL-UNNAMED -Djava.security.manager=allow ========================================================================= 14:06:39,001 INFO [org.jboss.modules] (main) JBoss Modules version 2.1.2.Final WildFly Full 30.0.1.Final (WildFly Core 22.0.2.Final)
起動コマンドは以下です。
$ bin/standalone.sh \ -Djboss.bind.address=0.0.0.0 \ -Djboss.bind.address.management=0.0.0.0 \
まずは、奥にあるapi-bに方から。
Maven依存関係。
<properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>ROOT</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.4.0</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build>
JAX-RS有効化。
src/main/java/org/littlewings/wildfly/client/b/JaxrsActivator.java
package org.littlewings.wildfly.client.b; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("") public class JaxrsActivator extends Application { }
JAX-RSリソースクラス。
src/main/java/org/littlewings/wildfly/client/b/CalcResource.java
package org.littlewings.wildfly.client.b; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; @Path("calc") public class CalcResource { @GET @Path("plus") @Produces(MediaType.APPLICATION_JSON) public CalcResult plus(@QueryParam("a") int a, @QueryParam("b") int b) { return CalcResult.create(a + b); } @POST @Path("plus") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public CalcResult plus(CalcRequest calcRequest) { return CalcResult.create(calcRequest.getA() + calcRequest.getB()); } }
CalcRequest
とCalcResult
の定義はJava SE環境の時と同じなので省略します。
次に、こちらにアクセスするapi-a側を作成します。
Maven依存関係。
<properties> <maven.compiler.release>21</maven.compiler.release> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.eclipse.microprofile</groupId> <artifactId>microprofile</artifactId> <version>6.0</version> <scope>import</scope> <type>pom</type> </dependency> </dependencies> </dependencyManagement> <dependencies> <dependency> <groupId>jakarta.platform</groupId> <artifactId>jakarta.jakartaee-web-api</artifactId> <version>10.0.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.eclipse.microprofile.rest.client</groupId> <artifactId>microprofile-rest-client-api</artifactId> <scope>provided</scope> </dependency> </dependencies> <build> <finalName>ROOT</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>3.4.0</version> <configuration> <failOnMissingWebXml>false</failOnMissingWebXml> </configuration> </plugin> </plugins> </build>
Eclipse MicroProfile Rest Clientの依存関係は、APIのみですね。
<dependency> <groupId>org.eclipse.microprofile.rest.client</groupId> <artifactId>microprofile-rest-client-api</artifactId> <scope>provided</scope> </dependency>
JAX-RS有効化。
src/main/java/org/littlewings/wildfly/client/a/JaxrsActivator.java
package org.littlewings.wildfly.client.a; import jakarta.ws.rs.ApplicationPath; import jakarta.ws.rs.core.Application; @ApplicationPath("") public class JaxrsActivator extends Application { }
Rest Client用のインターフェース。
src/main/java/org/littlewings/wildfly/client/a/CalcClient.java
package org.littlewings.wildfly.client.a; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; @Path("calc") @RegisterRestClient public interface CalcClient { @GET @Path("plus") @Produces(MediaType.APPLICATION_JSON) CalcResult plus(@QueryParam("a") int a, @QueryParam("b") int b); @POST @Path("plus") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) CalcResult plus(CalcRequest calcRequest); }
先ほどと違うのは、@RegisterRestClient
アノテーションを付与していることですね。
CalcRequest
とCalcResult
はやっぱり先ほどと同じなので省略します。
そして、Rest Clientを使うJAX-RSリソースクラス。
src/main/java/org/littlewings/wildfly/client/a/ProxyResource.java
package org.littlewings.wildfly.client.a; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import org.eclipse.microprofile.rest.client.inject.RestClient; import java.util.Map; @ApplicationScoped @Path("proxy") public class ProxyResource { @Inject @RestClient private CalcClient calcClient; @GET @Path("plus") @Produces(MediaType.TEXT_PLAIN) public int plus(@QueryParam("a") int a, @QueryParam("b") int b) { return calcClient.plus(a, b).getResult(); } @POST @Path("plus") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Map<String, Integer> plus(Map<String, Integer> request) { CalcResult result = calcClient.plus(CalcRequest.create(request.get("a"), request.get("b"))); return Map.of("result", result.getResult()); } }
@RegisterRestClient
アノテーションを付与していることで、CDI管理BeanとしてRest Clientのインスタンスをインジェクションすることが
できます。
@Inject @RestClient private CalcClient calcClient;
MicroProfile Rest Client CDI Support
ただ、これだとアクセス先が決まりません。
今回は、Eclipse MicroProfile Configを使って設定します。
src/main/resources/META-INF/microprofile-config.properties
org.littlewings.wildfly.client.a.CalcClient/mp-rest/url=http://172.17.0.3:8080
[インターフェース名]//mp-rest/[プロパティ名]
で指定できます。設定可能なプロパティは、以下に記載されています。
MicroProfile Rest Client CDI Support / Support for MicroProfile Config
その他の方法としては、baseUri
については@RegisterRestClient
アノテーションで設定することもできます。
@RegisterRestClient(baseUri="http://someHost/someContextRoot") public interface MyServiceClient { @GET @Path("/greet") Response greet(); }
これで準備ができたので、それぞれパッケージングしてデプロイしておきます。
$ mvn package
確認。
## GET $ curl '172.17.0.2:8080/proxy/plus?a=5&b=3' 8 ## POST $ curl -XPOST -H 'Content-Type: application/json' 172.17.0.2:8080/proxy/plus -d '{"a": 2, "b": 3}' {"result":5}
OKですね。
これで、ざっくり確認できたと思います。
レスポンスヘッダーを確認したい
ところで、ドキュメントを見ているとリクエストヘッダーに関する話はあるのですが、レスポンスヘッダーについて触れられていません。
Rest Clientで使うインターフェースの定義で、以下のようにメソッドの戻り値をjakarta.ws.rs.core.Response
にする案もあるのかなと
思ったのですが。
@Path("calc") @RegisterRestClient public interface CalcClient2 { @GET @Path("plus") @Produces(MediaType.APPLICATION_JSON) Response plus(@QueryParam("a") int a, @QueryParam("b") int b); @POST @Path("plus") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) Response plus(CalcRequest calcRequest); }
こうすると、レスポンスボディはResponse#getEntity
でObject
型として取得するか、Response#readEntity
で特定の型として取得する
ことになります。
Rest Clientを使う側のイメージはこんな感じです。
@ApplicationScoped @Path("proxy2") public class ProxyResource2 { @Inject @RestClient private CalcClient2 calcClient2; @GET @Path("plus") @Produces(MediaType.TEXT_PLAIN) public int plus(@QueryParam("a") int a, @QueryParam("b") int b) { try (Response response = calcClient2.plus(a, b)) { return response.readEntity(CalcResult.class).getResult(); } } @POST @Path("plus") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Map<String, Integer> plus(Map<String, Integer> request) { try (Response response = calcClient2.plus(CalcRequest.create(request.get("a"), request.get("b")))) { CalcResult result = response.readEntity(CalcResult.class); return Map.of("result", result.getResult()); } } }
これはこれでレスポンスボディもレスポンスヘッダーも扱えますが、レスポンスボディの型はインターフェース定義からは
わからなくなってしまいます。
まあ、仕方ないかなと…。
おわりに
Eclipse MicroProfile Rest Clientを試してみました。
使い方はだいたいわかりましたが、HTTPヘッダーまわりの扱いがちょっと小回りが効かなさそうなので、ハマることがあるのかなと
思ったり。
とはいえ、JAX-RS Clientそのままよりは簡単に使えそうなので、使える環境の場合は基本的にはこちらでよいかなと思います。