谷本 心 in せろ部屋

はてなダイアリーから引っ越してきました

Dapr Advent Calendar 10日目 - Dapr Java SDKを使う

こんにちは Dapr Advent Calendar 10日目です。今回は「基本編」の最終回となります。ひとつの節目を迎えました!

Dapr Java SDKを使ってみる

ここまでのAdvent Calendarで、Daprの主要な機能は(Actorを除き)一通り説明しました。そこで今回は、Daprの機能ではなくDaprのAPIJavaから利用しやすくするためのDapr Java SDKについて説明します。なお、Actorについては僕が使った経験がないのと、機能が多くて大変だったので、いったんスキップすることにしました。ごめんね!

Dapr Java SDKのドキュメントはここにあります。

docs.dapr.io

Dapr Java SDKJava 11以降を求めており、なかなか現代的です。いちおうJava 8向けのコンパイルをしているようなので、Java 8でも動作しそうですけどね。

今回作成するアプリケーションのソースコードgithubに置いてあります。

https://github.com/cero-t/dapr-advent-2021/tree/main/java-sdk

今回は、これまで書いてきたソースコードがDapr Java SDKを使うとどう変わるかを中心に説明します。

pom.xmlの修正

Dapr Java SDKを利用するために、まずは io.dapr:dapr-sdkをdependenciesに追加します。

pom.xml

        <dependency>
            <groupId>io.dapr</groupId>
            <artifactId>dapr-sdk</artifactId>
            <version>1.3.1</version>
        </dependency>

ここでちょっと注意が必要で、dapr-sdkokhttp のバージョン4.xに依存しているのですが、親のモジュールとして spring-boot-starter-parent を使っていると、okhttp のバージョン3.xが使われてしまうため、起動時にエラーが起きてしまいます。

そのため spring-boot-starter-parent を使っている場合は、okhttp もあわせてdependenciesに追加します。

pom.xml

        <dependency>
            <groupId>io.dapr</groupId>
            <artifactId>dapr-sdk</artifactId>
            <version>1.3.1</version>
        </dependency>
        <dependency>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
            <version>4.9.0</version>
        </dependency>

もちろん spring-boot-starter-parent を外して自分でビルド設定を書く方向でも構いません。

これで準備は完了です。

Hello WorldInvoke API

それでは、まずHello Worldと、それを呼び出すAPIを作成します。これはそれぞれDapr Advent Calendarの2日目3日目の内容となります。

Hello World

Hello Worldは特に何も変わりありません。

JavaSdkController.java

@GetMapping("/hello")
public Map<String, String> hello() {
    return Map.of("message", "Hello, world!");
}

DaprのAPIを利用しているわけではなく、呼び出されるだけなので、何も変わらないですね。

Hello Worldを呼び出すAPI

続いて、このHello Worldを外部から呼び出すAPIです。Dapr Advent Calendar3日目では、このように実装していました。

InvokeController.java(分かりやすいよう一部改変しています)

@RestController
public class InvokeController {
    private RestTemplate restTemplate;

    @Value("http://localhost:${DAPR_HTTP_PORT}/v1.0/invoke/hello-app/method")
    private String baseUrl;

    public InvokeController(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @GetMapping("/invokeHello")
    public Map<String, ?> invokeHello() {
        Map<?, ?> result = restTemplate.getForObject(baseUrl + "/hello", Map.class);
        return Map.of("baseUrl", baseUrl, "remoteMessage", result);
    }
}

DaprのInvoke APIのURLに対して、RestTemplate を使ってアクセスしていました。

これに対してDapr Java SDKを使うと、次のように変わります。

JavaSdkController.java

@RestController
public class JavaSdkController {
    private DaprClient daprClient;

    public JavaSdkController(DaprClient daprClient) {
        this.daprClient = daprClient;
    }

    @GetMapping("/invokeHello")
    public Map<String, ?> invokeHello() {
        Map<?, ?> result = daprClient.invokeMethod("java-sdk-app", "hello", null, HttpExtension.GET, Map.class)
                .block();
        return Map.of("remoteMessage", result);
    }
}

利用するクライアントが RestTemplate から DaprClient に変わりました。

このDaprClientには invokeMethod というInvoke APIを呼び出すメソッドが定義されています。第1引数が app-id で、第2引数が method であるAPIのパス、第3引数がリクエストボディ、第4引数がHTTPメソッド、第5引数が戻り値の型です。他にも引数のパターンが異なる同名のメソッドがいくつか定義されています。

Dapr Java SDKを使えば、Dapr APIのURLの書き間違いや引数の間違いなどを防ぎやすくなるのが、一つのメリットですね。

またDaprClientはProject Reactorを利用しているため、単独の戻り値はMono、複数の戻り値はFluxとなっています。Project Reactorはノンブロッキングな処理を書くためのライブラリですが、詳しく説明するととんでもなく長くなるので、ここでは説明を割愛します。ごめんね!

DaprClientがProject Reactorを使っているのであれば、アプリケーション側もSpring WebFluxを使って良い感じにノンブロッキングな処理にすることもできるのでしょうけど、ここでは簡単にするために block() メソッドでブロックしています。ごめんね!

Daprを使ったアプリケーションの起動

それではDaprを使ってアプリケーションを起動します。設定ファイルの説明は省略しましたが、アプリケーションのポートは8089番にしています。

dapr run --app-id java-sdk-app --app-port 8089 ../mvnw spring-boot:run

次のコマンドでアクセスします。

curl localhost:8089/hello

次のメッセージが表示されます。

{
  "message": "Hello, world!"
}

もう一つのAPIも確認しましょう。

curl localhost:8089/invokeHello

次のメッセージが表示されます。

{
  "remoteMessage": {
    "message": "Hello, world!"
  }
}

それぞれAPIが正常に動作していることを確認できました。

今回は簡単にするために同じアプリケーション内に2つのAPIを作って呼び出しを行いましたが、もちろん別々のアプリケーションとして作っても構いません。問題なくDaprを経由して呼び出すことができます。

Dapr Java SDKのSpring Boot向け機能を使ってみる

これまでのDapr Advent CalendarでやってきたすべてのコードをDapr Java SDKに置き換えても良いのですが、そんなことを説明してもあまり面白くないでしょうから、アプリケーションの作成は次のメッセージング(Pub/sub API)までにします。

ちなみにGitHubにあるサンプルコードには、データストアの読み書きをするメソッドも書いているので、興味がある人は読んでみてください。

pom.xmlの修正

この先で、Subsribe側の処理を作るところでDapr Java SDKのSpring Boot向け機能を使うため、pom.xmlを少し修正します。

pom.xml

        <dependency>
            <groupId>io.dapr</groupId>
            <artifactId>dapr-sdk-springboot</artifactId>
            <version>1.3.1</version>
        </dependency>

pom.xmldapr-sdkdapr-sdk-springboot に変更します。dapr-sdk-springbootdapr-sdk に依存しているため、変更で構いません。

これで何が変わるかは、後ほど説明します。

Pub/sub API

メッセージのPublish

まずはメッセージのPublish処理です。Dapr Advent Calendar5日目では、このように実装していました。

PublishController.java

@PostMapping("/publish")
public void publish(@RequestBody Object message) {
    restTemplate.postForObject(pubsubUrl, message, Void.class);
}

RestTemplateを使ってDaprのAPIにオブジェクトをPOSTするだけですね。

これがDaprClientを使うと次のように変わります。

JavaSdkController.java

@PostMapping("/publish")
public void publish(@RequestBody Object message) {
    daprClient.publishEvent("pubsub", "my-message", message)
            .block();
}

処理の長さに違いはありませんね。

DaprClientの publishEvent メソッドはPublishを行うもので、引数にpubsub名、メッセージトピック、メッセージ本体を受け取ります。非常に分かりやすいですね。

メッセージのSubscribe

続いてメッセージのSubscribe処理です。Dapr Advent Calendar5日目では、このように実装していました。

SubscribeController.java

@PostMapping("/subscribe")
public void subscribe(@RequestBody String message) {
    System.out.println(message);
}

取得した文字列(JSON)を標準出力に出力しているだけです。

そしてSubscriptionのための設定ファイルも作成していました。

~/.dapr/components/subscription.yaml

apiVersion: dapr.io/v1alpha1
kind: Subscription
metadata:
  name: subscription
spec:
  pubsubname: pubsub
  topic: my-message
  route: /subscribe
scopes:
  - subscribe-app

pubsub名やメッセージトピック、また呼び出すアプリケーションやエンドポイントURLなどを記載するものです。

これがDapr Java SDK (extension for Spring Boot) を使うと次のように変わります。

JavaSdkController.java

@Topic(pubsubName = "pubsub", name = "my-message")
@PostMapping("/subscribe")
public void subscribe(@RequestBody String message) {
    System.out.println(message);
}

処理は全く同じなのですが @Topic というアノテーションがついています。このアノテーションdapr-sdk-springboot が提供しています。

このアノテーションさえつければ、Subscriptionの設定ファイルは必要ありません。アイエエエエ、ナンデ!? という感じなのですが、その辺りの仕組みは後ほど説明します。

Daprを使って確認

いったんアプリケーションを停止させた後、Daprを使ってアプリケーションを再起動します。

dapr run --app-id java-sdk-app --app-port 8089 ../mvnw spring-boot:run

特にコンポーネント設定ファイルなどは指定しないため、メッセージブローカーとして標準のRedisが使われることになります。

次のコマンドでpublishします。

curl -XPOST "localhost:8089/publish" -H "Content-type:application/json" -d '{
  "name": "Shin Tanimoto",
  "twitter": "@cero_t"
}'

コマンドを実行すると、コンソールに次のようなメッセージが表示されるはずです。

== APP == {"pubsubname":"pubsub","datacontenttype":"application/json","source":"java-sdk-app","type":"com.dapr.event.sent","topic":"my-message","id":"011754a8-c83d-4551-ac77-2fab25ec40e4","specversion":"1.0","traceid":"00-b493697e0f972f44572871d7010d680f-14ad50d87dc50de5-01","data":{"name":"Shin Tanimoto","twitter":"@cero_t"}}

問題なくSubscribeできていました。

繰り返しになりますが、Subscribeのための設定ファイルを作ることなく、Subscribeができたのです。

アノテーションだけでSubscriberを実装する仕組み

これまでは、Daprの挙動を変えるにはDaprの設定ファイルを書く必要がありました。しかし今回は、Dapr側ではなくアプリケーション側にアノテーションを書くだけで、DaprのSubscribeに相当する設定ができました。

Spring Cloudのようにアプリケーションの中で動くようなライブラリならまだしも、Daprのようなサイドカーがアプリケーション側のアノテーションをどういう風に扱っているのか、不思議ですよね。いえ、不思議じゃないよという人は、この項は読み飛ばしてください。

dapr-sdk-springbootがSubscribe情報を集約して外部公開している

dapr-sdk-springboot はとても小さいライブラリで、バージョン1.3.1の時点で5クラスしかありません。

github.com

いちおうソースコードは全部読んだのですが、分かったことは、@Topic アノテーションがついたメソッドの情報を集約して、外部から取得できるようにするためのAPIのエンドポイントを設けている、ということくらいでした。

そのエンドポイントはcurlコマンドで確認できます。

curl localhost:8089/dapr/subscribe 

次のようなレスポンスが返ってきます。

[
  {
    "pubsubName": "pubsub",
    "topic": "my-message",
    "route": "/subscribe",
    "metadata": {}
  }
]

これは確かに @Topic アノテーションがついたメソッドにあった情報です。しかしなぜこれで、Daprのsubscribeが有効になるのでしょうか。

Daprがアプリケーションのエンドポイントを使って設定する

実は /dapr/subscribe を用意するだけでSubscribeできることが、公式のリファレンスに記載されていました。

docs.dapr.io

Daprはアプリケーションの /dapr/subscribe というエンドポイントを参照し、その内容に従ってSubscribeの設定をするようです。最初からドキュメントをきちんと読んでおけって話ですよね・・・。

それでは実際にアプリケーション側のアクセスログを出して確認してみましょう。

application.properties

server.port=8089
server.tomcat.accesslog.enabled=true
server.tomcat.basedir=(任意のディレクトリ)

そしてDaprアプリケーション側を再起動します。

dapr run --app-id java-sdk-app --app-port 8089 ../mvnw spring-boot:run

すると、アプリケーション起動後に次のようなログが出ました。

127.0.0.1 - - [10/Dec/2021:00:00:00 +0900] "GET /dapr/config HTTP/1.1" 200 15
127.0.0.1 - - [10/Dec/2021:00:00:00 +0900] "GET /dapr/subscribe HTTP/1.1" 200 81

このログにある2つのエンドポイントが、アプリケーション起動時にDaprから呼ばれるようです。

こういう仕組みであれば、確かにアプリケーション側で定義したものをDapr側に反映することもできますね。なるほどなぁという感じです。

それで、Dapr Java SDKはどうなの?

さて、ここまでDapr Java SDKを使ってきましたが、どうでしょうか。

  • 型が明確になるから間違いが減る
  • IDEの自動補完が効くから、ドキュメントを読まなくても、どのようなAPIがあるか分かる
  • 環境変数からポート番号を取ってきたり、DaprのURLを設定ファイルに書いたりしなくて済む
  • 特にSubscribeの設定をアノテーションで設定できるのが便利

というメリットがあったと思います。

しかしその一方で

  • 開発時から常にDaprを使う必要がある
  • 設定だけで接続先を変えることができなくなる(モックに接続する、メッセージブローカーを迂回する、など)
  • APIの一部がちょっと分かりづらかったり、使いにくかったりする

というデメリットもあります。

僕自身は、特に開発時から常にDaprを使う必要になる点がデメリットとしては大きいと感じているため、今のところDapr Java SDKは使わない方向で進めています。もちろん最初からDaprを前提にしたほうが、Dapr化した時にハマらないから良いとか、IDEで自動補完できるところが初期の学習コストを下げられるから良い、という判断もあるでしょう。

なお、今回は長くなるので触れませんでしたが、分散トレーシングのための traceparent を伝播するような機能もDapr Java SDKにはありません。伝播させるためには他の仕組みも使って、うまく作り込む必要があります。もしDapr Java SDKを使うだけで、トレースIDが良い感じに伝播されるようなっていれば、また少し判断が変わっていたのかも知れませんね。

どうあれ、使う使わないの辺りは、組織や状況に応じてそれぞれで判断してもらえると良いかなと思います。

まとめ

  • Daprの提供するAPIを、DaprClientのメソッド経由で利用できる
  • DaprClientを使うと型や引数が明確になって間違いを減らせる
  • Dapr JavaSDKはProject Reactorを利用しており、ノンブロッキングな処理を書くことができる
  • Subscribeの設定ファイルの代わりに @Topic アノテーションをメソッドにつけるだけで済むようになる(要dapr-sdk-springboot)
  • 分散トレーシングのトレースIDを伝播するような機能はない

主要な機能とDapr Java SDKを説明し終わったので、これで基本編は終わりです。次回からはk8sを使う分散環境編となります。

なお、明日のエントリーは @tmak_tw さんが書いてくださるようです。Daprの回復性のあたりは、Daprの弱点のようにも思っていたところだったので、どんなエントリーなのか楽しみです!

それでは、また次回!