WildFly Swarm いろいろ 3 選 #javaee

  • 10
    いいね
  • 0
    コメント

これは Java EE Advent Calendar 2016 の 20 日目の記事です。


対象とする環境

  • WildFly Swarm 2016.12.1
  • JDK 8
  • Maven 3.3.9

完全なコード

本文中での説明用コードは断片的なものです。動作する完全なコードは以下にあります。

https://github.com/emag/javaee-advent-calendar/tree/master/2016

WildFly Swarm ってなに?

まず、WildFly Swarm について簡単に説明します。

WildFly Swarm は Java EE によるアプリケーションを実行可能 jar(uber jar)化できるフレームワークです。基盤となる Java EE アプリケーションサーバはその名の通り WildFly です。

まずは雰囲気をつかむために、JAX-RS を用いた簡単な API サーバを作成してみます。

コード全体は以下にあります。

https://github.com/emag/javaee-advent-calendar/tree/master/2016/helloworld

今回対象とするサンプル類はビルドツールとして Maven を利用しますので、まずは pom.xml に必要な依存性と uber jar を作成するためのプラグイン設定を追加していきます。

<dependencyManagement>
  <dependencies>
    <dependency>
      <!-- (1) bom を利用 -->
      <groupId>org.wildfly.swarm</groupId>
      <artifactId>bom-all</artifactId>
      <version>${version.wildfly-swarm}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependencies>
  <dependency>
    <!-- (2) JAX-RS を利用。上記のように bom を利用しているので version 指定は不要 -->
    <groupId>org.wildfly.swarm</groupId>
    <artifactId>jaxrs</artifactId>
  </dependency>
</dependencies>

<build>
  [...]

  <plugins>
    <plugin>
      <!-- (3) uber-jar を作成するためのプラグイン設定 -->
      <groupId>org.wildfly.swarm</groupId>
      <artifactId>wildfly-swarm-plugin</artifactId>
      <version>${version.wildfly-swarm}</version>
      <configuration>
        <mainClass>wildflyswarm.Bootstrap</mainClass>
      </configuration>
      <executions>
        <execution>
          <goals>
            <goal>package</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

次に JAX-RS を利用した簡単な API を作成しておきます。下記は /hello に GET すると return に指定された文字列を JSON として返す、というものになります。

package helloworld;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

@Path("/hello")
public class HelloWorld {

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  public String hello() {
    return "{\"message\" : \"Hello, WildFly Swarm!\"}";
  }

}

次に WildfFly Swarm を利用するこのアプリケーションのエントリポイントを作成します。これは上記の pom.xml 中の wildfly-swarm-plugin の設定で <mainClass> として指定したものです。

package wildflyswarm;

import helloworld.HelloWorld;
import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.jaxrs.JAXRSArchive;

public class Bootstrap {

  public static void main(String[] args) throws Exception {
    // (1) WildFly 自身を表す
    Swarm swarm = new Swarm(args);

    // (2) アプリケーションのアーカイブ
    JAXRSArchive archive = ShrinkWrap.create(JAXRSArchive.class);
    archive.addClass(HelloWorld.class);

    swarm
      // (3) WildFly の起動
      .start()
      // (4) アプリケーションのデプロイ
      .deploy(archive);
  }

}

org.wildfly.swarm.Swarm が WildFly を表すクラスです。このクラスのインスタンスを作成し(1)、WildFly を起動したり(3)、作成したアプリケーションのアーカイブ(2)をデプロイしたりできます(4)。

アプリケーションのアーカイブとは、普段 Java EE によるアプリケーションを開発する際にビルドする war のことです。
WildFly Swarm では ShrinkWrap というフレームワークを利用しオンデマンドに作成します。

ここまでで必要なものは作成完了です。あとはビルドするだけで uber jar ができあがります(uber jar は -swarm とついたものです)。

$ ./mvnw clean package && \
  java -jar target/helloworld-swarm.jar

アクセスしてみます。

$ curl localhost:8080/hello
{"message" : "Hello, WildFly Swarm!"}

うまくいきました!

ただこのアプリケーション、WildFly 自体の設定を一切いじっていないので、なんらかの設定もしてみたいところです。ここでは HTTPS リスナが使えるようにしてみましょう。

まず、依存性として以下を追加します。

<dependency>
  <groupId>org.wildfly.swarm</groupId>
  <artifactId>management</artifactId>
</dependency>

先ほどの wildflyswarm.Bootstrap に以下を追記します。

package wildflyswarm;

[...]
import org.wildfly.swarm.undertow.UndertowFraction;
[...]

Swarm swarm = new Swarm(args);

// 追記
swarm.fraction(
  UndertowFraction.createDefaultFraction("keystore.jks", "password", "selfsigned")
);

UndertowFraction.createDefaultFraction(...) は引数に第1引数から順番に以下を指定します。

  • キーストアのパス(相対パス指定の場合は java 実行ディレクトリ(user.dir)からのパス)
  • キーストアのパスワード
  • サーバ証明書のエイリアス

キーストアはサンプルで用意している keystore.jks を使います。
ご自身で用意したキーストアを利用する場合は、上記の値を適宜修正してください

これだけで HTTPS リスナが利用可能になります。それでは再パッケージしてアクセスしてみます。

$ ./mvnw clean package && java -jar target/helloworld-swarm.jar
$ curl https://localhost:8443/hello -k
{"message" : "Hello, WildFly Swarm!"}

いいですね。なお、HTTPS リスナを有効にした場合、HTTP/2 が利用可能な環境であれば HTTP/2 も有効化されます。

$ curl --http2 https://localhost:8443/hello -kI
HTTP/2 200
content-type: application/json
content-length: 37
date: Tue, 20 Dec 2016 04:07:28 GMT

このように、WildFly Swarm では従来の Java EE アプリケーション開発で必要なアプリケーションサーバのダウンロード・設定・デプロイ・起動が全てプログラムで記述できるようになります。また、上記の UndertowFraction のように、必要な部分だけ Fraction という単位で設定可能です。org.wildfly.swarm:jaxrs なども Fraction の 1 つです。

また、WildFly Swarm は Java EE アプリケーションを uber jar 化できるだけでなく、主に Microservices での利用を目的とした独自機能やサードパーティライブラリ(Netflix OSS や Zipkin/Brave など)とのインテグレーションが追加されています。

次章からは個人的に気になっていた下記機能を紹介します。

  • Monitor
  • Zipkin
  • Enhanced JAX-RS Client(ただし動かないので紹介だけ。。)

war パッケージング

と、その前に war パッケージングについて紹介しておきます。

先の helloworld は jar でパッケージングしていましたが、WildFly Swarm は war パッケージングも可能です。war パッケージングでは main() は不要です(必要に応じて利用しても構いません)。また、uber jar 以外に war も生成されるので、Payara など他の Java EE アプリケーションサーバにデプロイすることも可能です。

先ほどの helloworld を war パッケージングに変更してみましょう。コード全体は以下にあります。

https://github.com/emag/javaee-advent-calendar/tree/master/2016/helloworld-war

pom.xml での変更点は以下です。

  • <packaging>war</packaging> に変更
  • maven-war-plugin を追加(web.xml がなくてもビルドできるように)
  • mainClass 指定を削除

また、wildflyswarm.Bootstrap クラスは消してしまって大丈夫です。

@@ -6,6 +6,7 @@
   <groupId>javaee-ac-2016-wildflyswarm</groupId>
   <artifactId>helloworld</artifactId>
   <version>2016.12.1</version>
+  <packaging>war</packaging>

   <properties>
     <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -15,6 +16,7 @@
     <maven.compiler.target>1.8</maven.compiler.target>

     <version.wildfly-swarm>${project.version}</version.wildfly-swarm>
+    <version.maven-war-plugin>3.0.0</version.maven-war-plugin>
   </properties>

   <dependencyManagement>
@@ -48,9 +50,6 @@
         <groupId>org.wildfly.swarm</groupId>
         <artifactId>wildfly-swarm-plugin</artifactId>
         <version>${version.wildfly-swarm}</version>
-        <configuration>
-          <mainClass>wildflyswarm.Bootstrap</mainClass>
-        </configuration>
         <executions>
           <execution>
             <goals>
@@ -59,6 +58,13 @@
           </execution>
         </executions>
       </plugin>
+      <plugin>
+        <artifactId>maven-war-plugin</artifactId>
+        <version>${version.maven-war-plugin}</version>
+        <configuration>
+          <failOnMissingWebXml>false</failOnMissingWebXml>
+        </configuration>
+      </plugin>
     </plugins>
   </build>

次に wildflyswarm.Bootstrap で行っていた UndertowFraction.createDefaultFraction("keystore.jks", "password", "selfsigned") の部分をどう設定するかです。答えは外からシステムプロパティで指定する、なのですが具体的には何を指定すればよいでしょうか。

今までアプリケーション起動後すぐに出力される、以下のような長いログ出力に気づかれたかと思います。これらは WildFly Swarm で設定可能な組込み済のシステムプロパティと設定値が出力されています。

2016-12-20 00:00:00,000 INFO  [org.wildfly.swarm] (main) WFSWARM0037: Configuration:
  swarm.ajp.enable = false
  swarm.ajp.port   = 8009
  [...]

この途中に以下のようなログが出ていました。

swarm.http.certificate.alias = selfsigned
swarm.http.keystore.password = <redacted>
swarm.http.keystore.path     = keystore.jks

キーストア関連はこれらを設定すればよい、ということですね。

残念ながら現時点(2016/12/20)では設定可能な値の一覧が記載されているドキュメントはありません

では上記システムプロパティをずらずらと並べて起動してもよいのですが、もう 1 つやり方があります。以下のような project-stages.yml というファイルをプロジェクト直下に用意し、そこに書き下すことができます。

swarm:
  http:
    certificate.alias: selfsigned
    keystore:
      password: password
      path: keystore.jks

システムプロパティのドット1つ分を区切りにネストさせていきます。

ここまでで準備はできましたので、再パッケージ/再起動をします...としてもいいですが、せっかくなので別の起動方法もご紹介しましょう。wildfly-swarm-plugin には wildfly-swarm:run というゴールが用意されているので、こちらが利用できます。

$ ./mvnw clean wildfly-swarm:run

jar パッケージングの時と挙動になっていれば問題なしです。

もう 1 つ project-stages.yml についてですが、その名の通りステージを指定することができます。以下のように追記してみてください。

swarm:
  http:
    certificate.alias: selfsigned
    keystore:
      password: password
      path: keystore.jks
--- # ここから追記
project:
  stage: production
swarm:
  undertow:
    servers:
      default-server:
        http-listeners:
          default:
            enabled: false

--- でステージを区切ります。最初に書いたステージ指定がないものは default ステージとみなされます。ここでは production ステージでは HTTP リスナ(8080 ポート)は有効にならないようにしてみました。

default で設定した値は他のステージでも引き継がれます。また、上書きすることも可能です。

上記を追記したら、 production ステージを利用するように指定した上で再起動します(再パッケージングは不要です)。

$ java -jar target/helloworld-swarm.jar -Dswarm.project.stage=production

8080 ポートでリスンしなくなっていることが確認できるかと思います。

$ ss -alnp | grep <pid> | grep 8080
# 何も表示されない

その他 war と jar の各パッケージングでの違いについてや、project-stages.yml については以下ドキュメントも参考ください。

Monitor

ちょっと前置きが長くなってしまいましたが、ここから WildFly Swarm が用意している機能について 3 種類ほどご紹介します。

1 つ目はモニタリングです。機能は限定されていますが、Spring Boot Actuator のようなものをイメージしていただければと思います。

コード全体は以下にあります。

https://github.com/emag/javaee-advent-calendar/tree/master/2016/health

以下の依存性を追加するとモニタリング用のエンドポイント(/node, /heap, /threads)が追加されます。

<dependency>
  <groupId>org.wildfly.swarm</groupId>
  <artifactId>monitor</artifactId>
</dependency>

追加されたエンドポイントをそれぞれ見てみます。

/node

ノードの状態が確認できます。

WildFly に馴染みのある方向きの説明をするならば、server-state などは CLI で取得できる属性ですね

$ curl localhost:8080/node
{
    "name" : "halo5",
    "server-state" : "running",
    "suspend-state" : "RUNNING",
    "running-mode" : "NORMAL",
    "uuid" : "99cf030e-f638-4aa8-9a78-398e8cf782d4",
    "wfs-version" : "fixme"
}

/heap

実行している JVM のヒープ情報です。

$ curl localhost:8080/heap
{
    "heap-memory-usage" : {
        "init" : 257949696,
        "used" : 62501624,
        "committed" : 385351680,
        "max" : 3666870272
    },
    "non-heap-memory-usage" : {
        "init" : 2555904,
        "used" : 71957736,
        "committed" : 78274560,
        "max" : -1
    }
}

/threads

スレッドの情報です。

$ curl localhost:8080/threads
{
    "thread-count" : 130,
    "peak-thread-count" : 145,
    "total-started-thread-count" : 162,
    "current-thread-cpu-time" : 921661,
    "current-thread-user-time" : 0
}

ユーザ独自のヘルスチェック

モニタリングの一環としてユーザ独自のヘルスチェック API も作成可能です。

エンドポイントは /health に集約されます。

何も作成していない場合はその旨が返されます。

$ curl localhost:8080/health -I
HTTP/1.1 204 No health endpoints configured!

では 1 つ作ってみましょう。JAX-RS のエンドポイントを作る感覚で作成可能です。

package health;

import org.wildfly.swarm.monitor.Health;
import org.wildfly.swarm.monitor.HealthStatus;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import java.time.LocalDateTime;

@Path("/health-check")
public class HealthCheckResource {

  @GET
  @Path("/something")
  @Health // @Health を付与
  public HealthStatus checkSomething() { // HealthStatus を返す
    return HealthStatus
      .named("something") // 名前をつける(必須)
      .up() // 状態の設定(必須)
      .withAttribute("date", LocalDateTime.now().toString())
      .withAttribute("time", System.currentTimeMillis());
  }

}

@Path に指定するパスはなんでもよいですが、メソッドレベルで @org.wildfly.swarm.monitor.Health を付与することと、org.wildfly.swarm.monitor.HealthStatus を返り値とする必要があります。

HealthStatus の設定にはいくつかでき、最低限必要なのは named(...) で名前をつけることと、状態を表すために up() もしくは down() を呼んでステータスを設定することです。また withAttribute() ではキーバリュー形式で任意の値を設定できます。

ではこのヘルスチェック API にアクセスしてみます。

$ curl localhost:8080/health-check/something -s | jq .
{
  "id": "something",
  "result": "UP",
  "data": {
    "date": "2016-12-20T00:00:00.000",
    "time": 1482159600000
  }
}

次に集約用のエンドポイントである /health にアクセスしてみます。

$ curl localhost:8080/health -s | jq .
{
  "checks": [
    {
      "id": "something",
      "result": "UP",
      "data": {
        "date": "2016-12-20T00:00:00.000",
        "time": 1482159600000
      }
    }
  ],
  "outcome": "UP"
}

よさそうですが、1 つだとあまり意味ないので、もう 1 つヘルスチェック API を追加してみます。something ヘルスチェックは常に UP なので、DOWN になるパターンにしてみましょう。

@GET
@Path("/unstable")
@Health
public HealthStatus unstable() {

  return (Math.random() < 0.5) ?
    HealthStatus
      .named("unstable")
      .up()
      .withAttribute("message", "Woo Hoo!") :
    HealthStatus
      .named("unstable")
      .down()
      .withAttribute("message", "Too Bad...");
}

1/2 の確率で UP/DOWN いずれかになるようにしてみました。

今度はこちらにアクセスしてみます。

$ curl localhost:8080/health-check/unstable -s | jq .
{
  "id": "unstable",
  "result": "UP",
  "data": {
    "message": "Woo Hoo!"
  }
}

UP なときは元気ですが。。

$ curl localhost:8080/health-check/unstable -s | jq .
{
  "id": "unstable",
  "result": "DOWN",
  "data": {
    "message": "Too Bad..."
  }
}

場合によっては DOWN してしょんぼりしています。

/health も見てみます。

両ヘルスチェックが UP であれば全体の結果(outcome プロパティ)も UP ですが、

$ curl localhost:8080/health -s | jq .
{
  "checks": [
    {
      "id": "something",
      "result": "UP",
      "data": {
        "date": "2016-12-20T00:00:00.000",
        "time": 1482159600000
      }
    },
    {
      "id": "unstable",
      "result": "UP",
      "data": {
        "message": "Woo Hoo!"
      }
    }
  ],
  "outcome": "UP"
}

ひとつでも DOWN があると outcome も DOWN になります。

$ curl localhost:8080/health -s | jq .
{
  "checks": [
    {
      "id": "something",
      "result": "UP",
      "data": {
        "date": "2016-12-20T00:00:00.000",
        "time": 1482159600000
      }
    },
    {
      "id": "unstable",
      "result": "DOWN",
      "data": {
        "message": "Too Bad..."
      }
    }
  ],
  "outcome": "DOWN"
}

ユーザが任意のヘルスチェックを作成できるのはなかなか便利そうですね。

Monitor については以下にドキュメントがあります。

https://wildfly-swarm.gitbooks.io/wildfly-swarm-users-guide/content/v/2016.12.1/advanced/monitoring.html

ヘルスチェックについては内容が古いので注意です。詳細は以下リリースノートを参照ください。

http://wildfly-swarm.io/posts/announcing-wildfly-swarm-2016-12-0/#_health_checks

Zipkin

2 つ目は Zipkin インテグレーションです。

コード全体は以下にあります。

https://github.com/emag/javaee-advent-calendar/tree/master/2016/zipkin

Zipkin は Microservices などで複数のサービス呼び出しが行われるような分散環境において、各サービスをトレーシングできるものです。

今年のこの Java EE Advent Calendar でも @kazuhira_r さんがすでに紹介されていますので、Zipkin やその Java ライブラリである Brave の JAX-RS ライブラリについての詳しい説明はこちらを参照ください。

Brave JAXRS2で、JAX-RSでもDistributed Tracing #javaee

ここでは WildFly Swarm で Zipkin/Brave を利用する方法を簡単にご紹介します。

まず、全体の構成としては以下のようなものとします。First は Second を、Second は Third を呼び出します。

[Client] <-> [First Service] <-> [Second Service] <-> [Third Service]

サービス自体は単純なもので、送られてきたメッセージに特定文字列を追加するのみです(Second であれば TWO など)。

それでは、まずはサービス自体が Zipkin でトレースできるようにするための設定をします。依存性として以下を追加します。

<dependency>
  <groupId>org.wildfly.swarm</groupId>
  <artifactId>zipkin-jaxrs</artifactId>
</dependency>

上記依存性を追加すると Zipkin サーバの URL やサービス名を指定できる ZipkinFraction が使えるようになるので、main() メソッドで指定します。

この設定は現状システムプロパティで設定できないので、main() が必須です。

package wildflyswarm;

import org.jboss.shrinkwrap.api.ShrinkWrap;
import org.wildfly.swarm.Swarm;
import org.wildfly.swarm.jaxrs.JAXRSArchive;
import org.wildfly.swarm.jaxrs.btm.ZipkinFraction;

public class Bootstrap {

  public static void main(String[] args) throws Exception {
    Swarm swarm = new Swarm(args);

    swarm.fraction(new ZipkinFraction("first")
      .reportAsync("http://localhost:9411/api/v1/spans"));

    [...]
  }
}

First Service は first 、Second Service は second といった要領で各サービスに対してサービス名を設定します。reportAsync にはレポートする Zipkin サーバの URL を指定します。

また、サービス側だけ登録するとトレーシングがそこで途切れてしまうため、サービスの呼び出し側も登録しておきます。以下は Second Service を呼び出す First Service 側の例です。

package zipkin;

import com.github.kristofa.brave.Brave;
import zipkin.reporter.AsyncReporter;
import zipkin.reporter.urlconnection.URLConnectionSender;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

@Path("/")
public class FirstController {

  @GET
  @Produces(MediaType.TEXT_PLAIN)
  public String first() {
    AsyncReporter<Span> asyncReporter = AsyncReporter.builder(
        URLConnectionSender.create("http://localhost:9411/api/v1/spans"))
        .build();

    Brave brave = new Brave.Builder("second-client")
      .reporter(asyncReporter)
      .build();

    Client client = ClientBuilder.newBuilder()
      .register(BraveTracingFeature.create(brave))
      .build();

    try {
      WebTarget target = client.target("http://localhost:8280");
      Response response = target.queryParam("message", "ONE").request().get();
      return response.readEntity(String.class);
    } finally {
      client.close();
    }
  }

}

First Service で Second Service を呼び出している部分に対して second-client として登録しておきます。

AsyncReporter<Span> asyncReporter = AsyncReporter.builder(
    URLConnectionSender.create("http://localhost:9411/api/v1/spans"))
    .build();

Brave brave = new Brave.Builder("second-client")
  .reporter(asyncReporter)
  .build();

Client client = ClientBuilder.newBuilder()
  .register(BraveTracingFeature.create(brave))
  .build();

WildFly Swarm の Zipkin Fraction が依存している brave-jaxrs2 のバージョンは 3.14.1 であり、最新の 3.16.0 で追加されている BraveTracingFeature が存在しません。
よって、3.16.0 のコードを参考に必要なところだけ抜き出したものを作成しています。

なお、Second Service も Third Service を呼び出しているので同様の対処が必要ですが、Second Service はトレーシングの効果をわかりやすくするために 5 秒間スリープさせる処理を追加しています。

ここまでできたら Zipkin サーバを起動します。ここでは簡単のため Docker を利用します。

$ docker run -d -p 9411:9411 openzipkin/zipkin

次に各サービスをビルド後、起動します。同一マシンで起動する場合は swarm.port.offset でポートをずらしておきます。

$ ./mvnw clean package

First Service の起動(8080 ポート)。

$ java -jar first/target/first-swarm.jar

Second Service の起動(8280 ポート)。

$ java -jar second/target/second-swarm.jar -Dswarm.port.offset=200

Third Service の起動(8380 ポート)。

$ java -jar third/target/third-swarm.jar -Dswarm.port.offset=300

各サービスが起動したら First Service へリクエストします。

$ curl localhost:8080
ONE TWO THREE

Second Service で 5 秒スリープしているため、ちょっと時間かかります。

では http://localhost:9411 にアクセスし、トレース状況を確認しましょう。サービス名として first を選択し、Start timeEnd time を適当な値にした上で Find Traces をクリックします。

zipkin-1.png

ちゃんと出てますね。5 秒かかってます。ここをさらにクリックすると呼び出し階層が表示されます。

zipkin-2.png

second が 5 秒かかってることが一目瞭然です。各サービスの呼び出し期間内の部分をクリックするとクライアント・サービス間の内容が確認できます。

zipkin-3.png

ZipkinFraction については以下にドキュメントがあります。

https://wildfly-swarm.gitbooks.io/wildfly-swarm-users-guide/content/v/2016.12.1/advanced/tracing-zipkin.html

Enhanced JAX-RS Client

最後に Enahnced JAX-RS Client についてご紹介します。

とはいえ、この機能は手元で確認したところ以下の問題のため 2016.12.1 時点で動作しませんでしたので参考程度に読んでいただければと思います。

SWARM-940: NoClassDefFoundError with Enhanced JAX-RS Client

例えば以下のようなクエリパラメータ i1, i2 を渡すと足し算してくれるような JAX-RS のサービスがあったとします。

@GET
@Produces(MediaType.APPLICATION_JSON)
public String add(@QueryParam("i1") int i1,
                  @QueryParam("i2") int i2) {

  return String.format("{\"result\" : %d}", i1 + i2);

}

単純に JAXRS Client を利用して上記のサービスを呼び出す場合は以下のようになります。

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;

Client client = ClientBuilder.newClient();

try {
  WebTarget target = client.target("<service base url>");

  Response response = target
    .queryParam("i1", i1)
    .queryParam("i2", i2)
    .request(MediaType.APPLICATION_JSON).get();

  // response を読んでどうこうする処理

} finally {
  client.close();
}

個人的にはわかりやすくて好きですが、WildFly Swarm では JAX-RS Client をラップしたクライアント API を提供しています。

利用する際は以下の依存性が必要です。

<dependency>
  <groupId>org.wildfly.swarm</groupId>
  <artifactId>cdi</artifactId>
</dependency>
<dependency>
  <groupId>org.wildfly.swarm</groupId>
  <artifactId>cdi-jaxrsapi</artifactId>
</dependency>

次に対象サービスの API に即した形でインターフェースを切ります。このインターフェースは org.wildfly.swarm.cdi.jaxrsapi.ServiceClient を継承した上で、@org.wildfly.swarm.cdi.jaxrsapi.Service を付与し、baseUrl に対象サービスのベース URL を指定します。

各メソッドはリクエストの内容に応じた API を記載します。

import org.wildfly.swarm.cdi.jaxrsapi.Service;
import org.wildfly.swarm.cdi.jaxrsapi.ServiceClient;

import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

@Path("/")
@Service(baseUrl = "<service base url>")
public interface AdditionService extends ServiceClient<AdditionService> {

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  String add(@QueryParam("i1") int i1,
             @QueryParam("i2") int i2);

}

こういったアプローチは RESTEasy の Proxy FrameworkRetrofit でもありますね

クライアントは上記のインタフェースをインジェクションして利用します。

import javax.enterprise.context.ApplicationScoped;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

@Path("/")
@ApplicationScoped
public class CalcController {

  @Inject
  AdditionService additionService;

  @GET
  @Produces(MediaType.APPLICATION_JSON)
  public String calc(@QueryParam("i1") int i1,
                     @QueryParam("i2") int i2) {

    return additionService.add(i1, i2);

  }

}

まるでローカル呼び出しのように書けるのがよいですね。

ただ、現在の実装だと baseUrl で指定するサービスの URL はハードコードするしかないため、あまり実用的ではありません。この問題に対しては以下のようなチケットが切られています。サービスディスカバリや、せめてシステムプロパティで名前解決できるのは必須でしょう。

また、先ほどご紹介した Zipkin インテグレーションではクライアントにもトレース用の設定を行う必要がありました。このクライアント API を使うとここらへんの設定も勝手にやってくれるとうれしいところです。

Spring Cloud Sleuth はほとんど Zipkin を意識せずに済みます。さすが。
すぐ試すには以下のサンプルがシンプルでわかりやすかったです。
https://github.com/openzipkin/sleuth-webmvc-example

おまけ

個人的に以下のような WildFly Swarm のガイドを作っています。

WildFly Swarm Tour

JAX-RS/CDI/JPA を用いた簡単な Java EE による CRUD アプリケーションから認証サーバを用いたセキュリティ設定、ちょっとだけ Docker など、それなりにエッセンスを盛り込んでいるつもりです。
本エントリはちょっとした機能紹介でしたが、基本的な Web アプリケーションを組んでみたいという方はぜひ一通り遊んでみていただければ幸いです。

参考

この投稿は Java EE Advent Calendar 201620日目の記事です。