Dapr Advent Calendar 10日目 - Dapr Java SDKを使う
こんにちは Dapr Advent Calendar 10日目です。今回は「基本編」の最終回となります。ひとつの節目を迎えました!
Dapr Java SDKを使ってみる
ここまでのAdvent Calendarで、Daprの主要な機能は(Actorを除き)一通り説明しました。そこで今回は、Daprの機能ではなくDaprのAPIをJavaから利用しやすくするためのDapr Java SDKについて説明します。なお、Actorについては僕が使った経験がないのと、機能が多くて大変だったので、いったんスキップすることにしました。ごめんね!
Dapr Java SDKはJava 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-sdk
は okhttp
のバージョン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 WorldとInvoke 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.xmlの dapr-sdk
を dapr-sdk-springboot
に変更します。dapr-sdk-springboot
は dapr-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クラスしかありません。
いちおうソースコードは全部読んだのですが、分かったことは、@Topic
アノテーションがついたメソッドの情報を集約して、外部から取得できるようにするためのAPIのエンドポイントを設けている、ということくらいでした。
そのエンドポイントはcurlコマンドで確認できます。
curl localhost:8089/dapr/subscribe
次のようなレスポンスが返ってきます。
[ { "pubsubName": "pubsub", "topic": "my-message", "route": "/subscribe", "metadata": {} } ]
これは確かに @Topic
アノテーションがついたメソッドにあった情報です。しかしなぜこれで、Daprのsubscribeが有効になるのでしょうか。
Daprがアプリケーションのエンドポイントを使って設定する
実は /dapr/subscribe
を用意するだけでSubscribeできることが、公式のリファレンスに記載されていました。
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の弱点のようにも思っていたところだったので、どんなエントリーなのか楽しみです!
それでは、また次回!