はじめに
Helidon は Java で効率的かつ柔軟にマイクロサービスを開発するための軽量なフレームワークです。クラウドネイティブなアプリケーションに必要な機能とAPIを提供しています。シンプル・軽量な SE と、Eclipse MicroProfile に準拠した MP の二つのエディションがあります。
Helidon 4.0 がリリースされた時、 3.x で実装済みだった gRPC が無くなって心配しましたが、4.1 から Java SE 21 で正式導入された Virtual Threads を使った新しい実装となって再登場しました。Helidon SE と MP 各々に gRPC をサポートしていますが、Helidon MP はアノテーションを使って簡単に gRPC サーバ & クライアントを実装することができるので、今回はこれを試してみます。また最後に、本家 grpc.io の Java Quickstart との相互通信も確認してみます。
ソースコードはこちらにあります。
gRPC と Protocl Buffers と Helidon
gRPC と Protocol Buffers を初めて聞くという方は、こちらに概要が分かりやすく解説されていますのでご覧ください。
gRPCで扱うデータフォーマット(データ構造をシリアライズして送受信するフォーマット)は必ずしもProtocol Buffers である必要はありませんが(JSONや、Javaアプリ間であればシリアライズされたPOJOでも構わない)、Protocol Buffers は Interface Definition Language (IDL = インタフェース記述言語) を使って中立的な Remote Procedure Call (RPC) のデータ構造とメソッドを定義できるので、gRPCではこれがデフォルトとなっています。定義ファイル (.proto) から各種言語用のサーバ&クライアントのコードを生成するコンパイラ (protoc) があり、Helidon も Maven のプラグインとしてこれを使います。ただし、Helidon が使用するのは、Protocol Buffers で扱うメッセージのシリアライズ/デシリアライズを行う部分のみで、サーバ&クライアントの実装は Helidon 自身が提供しています。
デモ内容と作業手順
これから、Helidon MP を使って、gRPC サーバと gRPC クライアントを実装します。クライアントから HelloRequest というデータ型のリクエストを送信して、サーバが HelloReply というデータ型の応答を返します。HelloRequest、HelloReply とも Protocol Buffers です。gRPC には複数の送受信パターンがありますので、そのバリエーションも実装します。そして、クライアントとサーバで折り返しができるテストコードも作成します。
これから先、以下の項目を順番に行なっていきます。
- Helidon CLI を使って Helidon MP 用の Maven Project を作成する
- pom.xml を修正して gRPC に必要な dependency と plugin を追加する
- .protoc を配置して Protocol Buffers に必要な Java コードを生成する
- gRPC サーバを実装する
- gRPC クライアント(クライアント proxy)を実装する
- テストコードを実装する(gRPC クライアントを使ってサーバをコールする)
- grpc.io Quickstart と相互接続してみる
必要なツールとバージョンは以下の通り
- Java SE 21+
- Maven 3.8+
1. Helidon CLI を使って Helidon MP 用の Maven Project を作成する
Helidon には、CLIツール がありますので、これを使って Maven プロジェクトのディレクトリを作り、必要なファイルを配置します。
まずはツールのダウンロード(Linuxの場合)
curl -L -O https://helidon.io/cli/latest/darwin/helidon
chmod +x ./helidon
sudo mv ./helidon /usr/local/bin/
CLI をバッチモードで起動してプロジェクトを作成 (helidon init
) します。
helidon init --batch \
--version 4.1.5 \
--flavor MP \
--archetype custom \
--groupid com.example.helidon \
--artifactid grpc-demo \
--package com.example.grpc
grpc-demo
というディレクトリができているので、そちらに移動して、以下の不要なファイルを削除します(サンプルとして入っているRESTサービスのコード)
src/main/java/com/example/grpc/SimpleGreetResource.java
src/main/java/com/example/grpc/Message.java
src/test/java/com/example/grpc/MainTest.java
cd grpc-demo
rm src/main/java/com/example/grpc/SimpleGreetResource.java
rm src/main/java/com/example/grpc/Message.java
rm src/test/java/com/example/grpc/MainTest.java
2. pom.xml を修正して gRPC に必要な dependency と plugin を追加する
helidon init
の実行によって作成された pom.xml を修正して gRPC 用の設定を追加します。
まず、gRPCの実行に必要なライブラリの dependency を追加します。
<dependencies>
<!-- 省略: helidin init で挿入された dependency -->
<!-- support for gRPC -->
<dependency>
<groupId>io.helidon.grpc</groupId>
<artifactId>helidon-grpc-core</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.grpc</groupId>
<artifactId>helidon-microprofile-grpc-core</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.grpc</groupId>
<artifactId>helidon-microprofile-grpc-server</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.microprofile.grpc</groupId>
<artifactId>helidon-microprofile-grpc-client</artifactId>
</dependency>
</dependencies>
Protocol Buffers をコンパイルするためのプラグインを追加します。
<build>
<!-- extension for GRPC (for protobuf-maven-plugin) -->
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>${version.plugin.os}</version>
</extension>
</extensions>
<plugins>
<!-- for GRPC (source generation from protobuf) -->
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!-- 省略: helidon init で挿入された plugin -->
</plugins>
</build>
3. .protoc を配置して Protocol Buffers に必要な Java コードを生成する
grpc.io が提供する Java example との相互接続を試しますので、RPCのインターフェースを合わせるために 同じ定義ファイル (helloworld.proto)を使います。
syntax = "proto3";
option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
rpc SayHelloStreamReply (HelloRequest) returns (stream HelloReply) {}
rpc SayHelloBidiStream (stream HelloRequest) returns (stream HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
string message = 1;
}
サービスのメソッドは gRPC Core concepts に説明がある通り、リクエストとレスポンス各々を単一のメッセージとして送るのか、一連の複数メッセージ(ストリーム)として送るのかによって、4種類パターンに分類できます。
- Unary RPCs
- Server streaming RPCs
- Client streaming RPCs
- Bidirectional streaming RPCs
helloworld.proto では、Greeter というサービスに3つのメソッドが定義されていますが、これは各々
// Unary
rpc SayHello (HelloRequest) returns (HelloReply) {}
// Server streaming
rpc SayHelloStreamReply (HelloRequest) returns (stream HelloReply) {}
// Bidirectional streaming
rpc SayHelloBidiStream (stream HelloRequest) returns (stream HelloReply) {}
と、コメントで示す通りの種類となります。このメソッドの類型は、この後サーバ/クライアントを実装する際に必要となってきますので覚えておいて下さい。streaming は、あらかじめ何個のメッセージを送るか/送られるかは事前に決められていないことがほとんどなので、それに対応できるような実装を行う必要があります。
では .proto のコンパイルに移りましょう。
helloworld.proto ファイルを src/main/proto に置きます。
wget https://raw.githubusercontent.com/grpc/grpc/refs/heads/master/examples/protos/helloworld.proto
mkdir -p src/main/proto
mv helloworld.proto src/main/proto
先程 pom.xml に plugin の設定を入れたので、Maven の compile もしくは test-compile フェーズで .proto ファイルがコンパイルされ target/generated-sources もしくは target/generated-test-sources に必要なソースファイルが生成されます。
# コンパイルを実行
$ mvn compile
# コードが生成されているのを確認する
$ tree target/generated-sources
target/generated-sources
├── annotations
└── protobuf
└── java
└── io
└── grpc
└── examples
└── helloworld
├── HelloReply.java
├── HelloReplyOrBuilder.java
├── HelloRequest.java
├── HelloRequestOrBuilder.java
└── HelloWorldProto.java
生成されたコードの中に、HelloRequest、HelloReply オブジェクトを作成するためのビルダが含まれています。
4. gRPC サーバを実装する
Helidon MP では、gRPC も REST サービスを実装するのと同様にクラス/メソッドにアノテーションをつけます。
サーバのメソッドの書き方は現在 Helidon 4 よりも Helidon 3 のドキュメンテーション の方に詳しい解説がありますので、そちらを参考にしながら Unary, ServerStreaming, ClientStreaming, Bidirectional 各々に適した引数と返り値を指定して下さい。Unary は単一メッセージなので分かりやすいと思いますが、streaming を扱う場合、引数と返り値は java.util.stream.Stream
、io.grpc.stub.StreamObserver
、java.util.concurrent.CompletableFuture
などの選択肢があります。
サーバのクラスは以下のような格好になります。
// src/main/java/com/example/grpc/HelloWorldService.java
@Grpc.GrpcService("helloworld.Greeter")
@ApplicationScoped
public class HelloWorldService {
@Grpc.Unary("SayHello")
public HelloReply sayHello(HelloRequest request) {
// ここに実装を書く
}
@Grpc.ServerStreaming("SayHelloStreamReply")
public Stream<HelloReply> sayHelloStreamReply(HelloRequest request) {
// ここに実装を書く
// Stream の代わりに StreamObserver や ComletableFuture でも良い
}
@Grpc.Bidirectional("SayHelloBidiStream")
public StreamObserver<HelloRequest> sayHelloBidiStream(StreamObserver<HelloReply> observer) {
// ここに実装を書く
// Bidirectionalの場合は、この引数/返り値のパターンのみ指定可能
}
Unary の実装例では HelloRequest を受け取り、シンプルに HelloReply を作成して返します。
@Grpc.Unary("SayHello")
public HelloReply sayHello(HelloRequest request) {
String reply = "Hello " + request.getName();
return HelloReply.newBuilder().setMessage(reply).build();
}
Server streaming の実装では、サーバから返信するメッセージの個数を予め決めているので、String 配列から Stream<HelloReply>
インスタンスを作成して返信しています。
@Grpc.ServerStreaming("SayHelloStreamReply")
public Stream<HelloReply> sayHelloStreamReply(HelloRequest request) {
String name = request.getName();
String[] parts = {"Hello", name}; // メッセージを2個返す
return Stream.of(parts).map(s -> HelloReply.newBuilder().setMessage(s).build());
}
Bidirectional の実装はちょっと工夫が必要です。StreamObserver<HelloReply>
を受け取って StreamObserver<HelloRequest>
を返すってどういうことでしょう?
Reactive Streams や java.util.concurrent.Flow のようなリアクティブ・プログラミングを扱ったことがある方ならイメージが湧きやすいと思いますが、サーバもクライアントもメッセージの受信はイベント通知(メッセージ受信/エラー/完了)をきっかけに非同期に行われます(自ら受信する動作に入らない)。
io.grpc.stub.StreamObserver<V>
はリアクティブ・プログラミングではお馴染みのシンプルなインターフェースです。
public interface StreamObserver<V> {
void onNext(V value);
void onError(Throwable t);
void onCompleted();
}
Helidon はクライアントが受信するイベントを処理するための橋渡しとなる StreamObserver<HelloReply>
のインスタンスをサーバに渡し、サーバはクライアントから送信されたメッセージを受信処理するための StreamObserver<HelloRequest>
の実装クラスのインスタンスを Helidon に返り値として渡します。
今回のサーバの実装では、受信した HelloRequest を逐次そのまま HelloReply に変換して返信し、完了のイベントを受信したところで自らも完了のイベントを送信します。
@Grpc.Bidirectional("SayHelloBidiStream")
public StreamObserver<HelloRequest> sayHelloBidiStream(StreamObserver<HelloReply> observer) {
return new HelloRequestStreamObserver(observer);
}
// 受信した HelloRequest をそのまま HelloReply に変換して返信する StreamObserver
public class HelloRequestStreamObserver implements StreamObserver<HelloRequest>{
private StreamObserver<HelloReply> replyObserver;
public HelloRequestStreamObserver(StreamObserver<HelloReply> replyObserver){
this.replyObserver = replyObserver;
}
@Override
public void onNext(HelloRequest value) {
// Greeting メッセージにして返信
HelloReply reply = HelloReply.newBuilder().setMessage("Hello " + value.getName()).build();
replyObserver.onNext(reply);
}
@Override
public void onError(Throwable t) {
// warning!!
}
@Override
public void onCompleted() {
// クライアントから完了イベントを受け取ったら、サーバも完了イベントを返す
replyObserver.onCompleted();
}
}
クライアントの処理はこの裏返しになる訳ですが、クライアントは(今回の実装では)最初にメッセージをサーバに送信してストリーミングを開始するところが違いです(必ずしもクライアントから最初にメッセージを送る必要はないです)。
5. gRPC クライアント(client proxy)を実装する
クライアントの実装については、まず以下のようにインターフェースにアノテーションを付けると Helidon が動的に client proxy を作ってくれます。
// src/main/java/com/example/grpc/HelloWorldClient.java
@Grpc.GrpcService("helloworld.Greeter")
@Grpc.GrpcChannel("helloworld-channel")
public interface HelloWorldServiceClient {
@Grpc.Unary("SayHello")
HelloReply sayHello(HelloRequest request);
@Grpc.ServerStreaming("SayHelloStreamReply")
Stream<HelloReply> sayHelloStreamReply(HelloRequest request);
@Grpc.Bidirectional("SayHelloBidiStream")
public StreamObserver<HelloRequest> sayHelloBidiStream(StreamObserver<HelloReply> observer);
}
アノテーションで @Grpc.GrpcChannel("helloworld-channel")
チャネルを指定していますが、これはクライアントが利用する gRPC のチャネルを指定するものです(デフォルトは "default")。
チャネルの定義は、MicroProfile 仕様の Config で行います。Helidon であれば、src/main/resources/application.yaml
に yaml 形式で記述するのが便利です。
grpc:
client:
channels:
- name: "helloworld-channel"
host: localhost
port: 8080
tls:
enabled: false
tls.enabled = false は明示的に指定しておかないとTLS通信を試みますので注意して下さい(証明書や鍵の設定等々を行わないと通信できません)。
REST の入り口
相互通信テストする際に必要なので curl から gRPCクライアントを呼び出すための REST の入り口を作っておきます。JAX-RSの流儀で gRPC client proxy を Inject できるのが Helidon MP の便利なところです。
// src/main/java/com/example/grpc/HelloWorldResource.java
@Path("/grpc")
@ApplicationScoped
public class HelloWorldResource {
@Inject
@Grpc.GrpcProxy
private HelloWorldServiceClient client;
@GET
@Path("/sayHello")
@Produces(MediaType.TEXT_PLAIN)
public String sayHello(@QueryParam("name") String name, @QueryParam("port") Integer port) {
// 動的に channel の port を変えられるようにするためのひと工夫
if(Objects.nonNull(port) && client instanceof GrpcConfigurablePort c) {
c.channelPort(port);
}
String param = Optional.ofNullable(name).orElse("world");
HelloRequest request = HelloRequest.newBuilder().setName(param).build();
try {
HelloReply response = client.sayHello(request);
String msg = response.getMessage();
return msg;
}catch (StatusRuntimeException e) {
throw new RuntimeException("gRPC failed: " + e.getMessage(), e);
}
}
}
これを使って最後に curl →(REST)→ Helidon →(gRPC)→ gRPCサーバ の通信を試します。
6. テストコードを実装する(gRPC クライアント → サーバ 通信のテスト)
client proxy のメソードをコールする gRPC クライアントを実装しなければなりませんが、先に作成した client proxy はインターフェースにアノテーションを追加しただけなので、そのままでは使えません。インターフェースの実装クラスのインスタンスが必要なのですが、ここは流石なことに Helidon MP なので @Inject
アノテーションを使って動的にインスタンスを作成することができます。
@Inject
@Grpc.GrpcProxy
private HelloWorldServiceClient client;
Unary と Server streming のテストパターンは至ってシンプルです。
@Test
void sayHello() {
HelloReply res = client.sayHello(HelloRequest.newBuilder().setName("Felix").build());
assertThat(res.getMessage(), is("Hello Felix"));
}
@Test
void sayHelloStream() {
Stream<HelloReply> stream = client.sayHelloStreamReply(HelloRequest.newBuilder().setName("Simon").build());
List<String> list = stream.map(HelloReply::getMessage).toList();
String[] value = list.toArray(new String[list.size()]);
assertArrayEquals(new String[]{"Hello", "Simon"}, value);
}
Bidirectional テストにおけるクライアントの実装ですが、こちらはサーバと逆に StreamObserver<HelloReply>
の実装クラスが必要なことは想像つくと思います。
今回のシナリオでは、
- "Bob", "Simon", "Felix" のメッセージをサーバに送る
- onComleted イベントをサーバに送る(クライアントの送信完了をサーバに知らせる)
- サーバから onComleted イベントが送られるのを待つ(サーバの送信完了を待つ)
- (完了を待っている間、非同期にサーバからのメッセージを受信して ArrayList に保存)
- サーバから onComleted イベント受信後、保存したテストメッセージを検証する
という一連の流れを実装してテストしています。
@Test
void sayHelloBidiStream() {
HelloReplyStreamObserver observer = new HelloReplyStreamObserver();
StreamObserver<HelloRequest> request = client.sayHelloBidiStream(observer);
request.onNext(HelloRequest.newBuilder().setName("Bob").build());
request.onNext(HelloRequest.newBuilder().setName("Simon").build());
request.onNext(HelloRequest.newBuilder().setName("Felix").build());
request.onCompleted();
String[] messages = observer.getMessages();
assertArrayEquals(new String[]{"Hello Bob", "Hello Simon", "Hello Felix"}, messages);
}
public class HelloReplyStreamObserver implements StreamObserver<HelloReply>{
private ArrayList<String> messages = new ArrayList<String>();
private boolean completed = false;
public String[] getMessages(){
waitForCompletion();
return messages.toArray(new String[messages.size()]);
}
@Override
public void onNext(HelloReply value) {
messages.add(value.getMessage());
}
@Override
public void onError(Throwable t) {
// warning!
}
@Override
public void onCompleted() {
notifyCompletion();
}
public synchronized void waitForCompletion(){
while(!completed){
try{
this.wait();
}catch(InterruptedException e){}
}
}
public synchronized void notifyCompletion(){
completed = true;
this.notifyAll();
}
}
Helidon gRPC クライアントをテストする際の注意点
テストクラスに @HelidonTest
を付け加えると、Helidonのテストハーネスが動的にサーバを起動してくれて便利なのですが、これだとテスト毎にサーバのポート番号が変わるので、それに対応したテストコードを書く必要があります。今回のケースでは application.yaml で静的に定義してある gRPCサーバの port 番号がテスに支障をきたしますので、これをテスト実施時に動的に変更する処理を入れています。
// src/test/java/com/example/grpc/HelloWorldServiceTest.java
@HelidonTest
class HelloWorldServiceTest {
@Inject
private WebTarget webTarget; // webTarget はテストサーバに追随している
@Inject
@Grpc.GrpcProxy
private HelloWorldServiceClient client;
@BeforeEach
void updatePort() {
// Inject された HelloWorldServiceClient のポート番号を変更
if (client instanceof GrpcConfigurablePort c) {
c.channelPort(webTarget.getUri().getPort());
}
}
// 以降実際のテストコード
}
最後に mvn test
してHeldion 自身の折り返しでクライアント〜サーバ間の gRPC が動作していることを確認してください。テストクラスを実行する毎に
Server started on http://localhost:36763
のようなログが出ているはずです。テスト用のサーバ・インスタンスがランダムなポートを使って起動していることが分かります。
では、件の Bidirectional のテストの部分のログを確認してみましょう。
GitHubのソース はログの出力も入っていますので、これを実行します。
出力を見やすくするために src/main/resources/logging.properties
を修正します。
# HelidonConsoleHandler uses a SimpleFormatter subclass that replaces "!thread!" with the current thread
#java.util.logging.SimpleFormatter.format=%1$tY.%1$tm.%1$td %1$tH:%1$tM:%1$tS %4$s %3$s !thread!: %5$s%6$s%n
java.util.logging.SimpleFormatter.format=%4$s %3$s: %5$s%6$s%n
以下のように、送受信が非同期に行われていることが分かります。
INFO HelloWorldServiceTest: Sending message: Bob, Simom and Felix
INFO HelloWorldServiceTest: Sending onCompleted event
INFO HelloWorldService: Bidirectional was called
INFO HelloReplyStreamObserver: waiting for onCompleted()
INFO HelloRequestStreamObserver: Instantiated with StreamObserver<HelloReply>: io.helidon.grpc.core.SafeStreamObserver@6e23c54b
INFO HelloRequestStreamObserver: onNext(): Bob
INFO HelloRequestStreamObserver: onNext(): Simon
INFO HelloReplyStreamObserver: onNext(): Bob
INFO HelloRequestStreamObserver: onNext(): Felix
INFO HelloReplyStreamObserver: onNext(): Simon
INFO HelloRequestStreamObserver: onCompleted()
INFO HelloReplyStreamObserver: onNext(): Felix
INFO HelloReplyStreamObserver: onCompleted()
INFO HelloReplyStreamObserver: waiting was lifted
メソッドに渡される StreamObserver<HelloReply>
の実体が io.helidon.grpc.core.SafeStreamObserver
であることが分かります。
また、この出力ログでは Thread の情報を省いているので確認できませんが、元々の設定でログを出力すると、HelloWorldService, HelloRequestStreamObserver, HelloReplyStreamObserver 各々が Virtual Thread で動作していることが分かります。
gRPC, HTTP2 まわりのログを確認したい場合は、logging.properties に以下の設定を入れると最も詳細なログが出力されます。
io.helidon.http.level=FINEST
io.helidon.webserver.level=FINEST
io.helidon.webclient.level=FINEST
7. grpc.io Quickstart と相互接続してみる
Helidon 折り返しだと、ちょっと味気ないので、他の gRPC 実装と相互接続検証をやってみたいと思います。grpc.io の提供する gRPC Java Quickstart で作成したサーバ/クライアントと Helidon を通信させてみたいと思います。
Quickstart は以下の
rpc SayHello (HelloRequest) returns (HelloReply) {}
一番シンプルな Unary メソッドを実装したサーバとクライアントを実装しています。
Quickstart のビルド
Quickstart に書かれている通りにビルドします。
git clone -b v1.69.0 --depth 1 https://github.com/grpc/grpc-java
cd grpc-java/examples
./gradlew installDist
Heldion クライアント → Quickstart サーバ の疎通テスト
Quickstart サーバを起動します。
$ ./build/install/examples/bin/hello-world-server
INFO: Server started, listening on 50051
Quickstart サーバは 50051 ポートで listen しているので、Helidon も gRPCクライアントの channel のポート番号を 50051に変更して起動します。
$ mvn package
$ java -Dgrpc.client.channels[0].port=50051 -jar target/grpc-demo.jar
別のコンソールを起動して Helidon の RESTの入り口を call してみます。
$ curl localhost:8080/grpc/sayHello
Hello world
REST経由で Quickstart サーバに gRPCリクエストを送りました、OK ですね。
Quickstart クライアント → Helidon サーバ の疎通テスト
今度は Quickstart クライアントのリクエストを受けるために Helidon のサーバ port を 50051 に変更して起動します。
$ java -Dserver.port=50051 -jar target/grpc-demo.jar
では Quickstart クライアントを実行します。
$ ./build/install/examples/bin/hello-world-client
Dec 27, 2024 5:57:10 AM io.grpc.examples.helloworld.HelloWorldClient greet
INFO: Will try to greet world ...
Dec 27, 2024 5:57:11 AM io.grpc.examples.helloworld.HelloWorldClient greet
INFO: Greeting: Hello world
こちらも OK でした。
まとめ
Heldion MP では POJO にアノテーションをつける形でサーバ&クライアントを実装できました。gRPC自体をあまり意識することなく実装できるのはありがたいです(Protocol Buffers とストリームのハンドリングさえ習得すれば OK)。
Helidon 4.1 では gRPC スタックが Virtual Threads ベースで書き換えられているので、高いパフォーマンスが期待できます。もとより gRPC 自体が HTTP2 ベースのプロトコルなので Helidon の実装に関わらず HTTP2 の恩恵を受けられます。あと、目立たないですが、REST サーバと gRPCサーバがシームレスに統合されているのも Heldion MP の優れた点ではないでしょうか。
今回 Docker image や Native image にするところまでやらなかったので、これも後々試して見ようと思います。
途中リアクティブ・プログラミングの話題が出てきましたが、ざっくり理解したい方には拙著のこちらの資料をご紹介しておきます。