RESTアプリケーションで仮想スレッドを使用
このガイドでは、REST アプリケーションで仮想スレッドを使用する方法を説明します。 仮想スレッドは I/O に関するものなので、REST クライアントも使用します。
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 17+がインストールされ、
JAVA_HOME
が適切に設定されていること -
Apache Maven 3.9.9
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
アーキテクチャ
このガイドに組み込まれているアプリケーションは非常にシンプルです。 2つの都市(フランスのヴァランスとギリシャのアテネ)の気象サービスを呼び出し、現在の気温に基づいて最適な場所を決定します。
Maven プロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。
Windowsユーザーの場合:
-
cmdを使用する場合、(バックスラッシュ
\
を使用せず、すべてを同じ行に書かないでください)。 -
Powershellを使用する場合は、
-D
パラメータを二重引用符で囲んでください。例:"-DprojectArtifactId=rest-virtual-threads"
このコマンドは、Quarkus REST(旧RESTEasy Reactive)、RESTクライアント、 Jackson エクステンションをインポートする新しいプロジェクトを生成し、特に以下の依存関係を追加します:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
implementation("io.quarkus:quarkus-rest-jackson")
implementation("io.quarkus:quarkus-rest-client-jackson")
なぜ リアクティブ エクステンションを使うのか不思議に思うかもしれません。 仮想スレッドには限界があり、リアクティブエクステンションを使用する場合にのみ適切に統合することができます。 ご心配なく。あなたのコードは100%同期/命令型スタイルで書かれます。 詳しくは 仮想スレッドリファレンスガイド をご確認ください。 |
pom.xml
ファイルの準備
仮想スレッドを使うためには、 pom.xml
ファイルをカスタマイズする必要があります。
1) <maven.compiler.release>17</maven.compiler.release>
の行を探し、次のように置換します:
<maven.compiler.release>21</maven.compiler.release>
2) maven-surefire-pluginおよびmaven-failsafe-plugin設定で、次の argLine
パラメータを追加します:
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<configuration>
<systemPropertyVariables>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
<argLine>-Djdk.tracePinnedThreads</argLine> <!-- Added line -->
</configuration>
</plugin>
<plugin>
<artifactId>maven-failsafe-plugin</artifactId>
<version>${surefire-plugin.version}</version>
<executions>
<execution>
<goals>
<goal>integration-test</goal>
<goal>verify</goal>
</goals>
<configuration>
<systemPropertyVariables>
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
<maven.home>${maven.home}</maven.home>
</systemPropertyVariables>
<argLine>-Djdk.tracePinnedThreads</argLine> <!-- Added line -->
</configuration>
</execution>
</executions>
</plugin>
-Djdk.tracePinnedThreads
は、テスト実行中にピン留めされたキャリアスレッドを検出します(詳細については、 仮想スレッドリファレンスガイド を参照してください)。
Java 19 と 20 における --enable-preview
Java 19 または 20 を使用している場合は、 |
天気クライアントの作成
このセクションは仮想スレッドに関するものではありません。 仮想スレッドの使い方を実証するためにI/Oを行う必要があるため、I/O操作を行うクライアントが必要だからです。 さらに、RESTクライアントは仮想スレッドと親和的です。ピンを行わず、伝搬を正しく行います。
以下の内容で src/main/java/org/acme/WeatherService.java
クラスを作成します:
package org.acme;
import com.fasterxml.jackson.annotation.JsonProperty;
import io.quarkus.rest.client.reactive.ClientQueryParam;
import jakarta.ws.rs.GET;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import org.jboss.resteasy.reactive.RestQuery;
@RegisterRestClient(baseUri = "https://api.open-meteo.com/v1/forecast")
public interface WeatherService {
@GET
@ClientQueryParam(name = "current_weather", value = "true")
WeatherResponse getWeather(@RestQuery double latitude, @RestQuery double longitude);
record WeatherResponse(@JsonProperty("current_weather") Weather weather) {
// represents the response
}
record Weather(double temperature, double windspeed) {
// represents the inner object
}
}
このクラスは気象サービスとの HTTP インタラクションをモデル化します。 RESTクライアントについて詳しくは、専用の ガイド を参照してください。
HTTPエンドポイントの作成
次に、以下の内容で src/main/java/org/acme/TheBestPlaceToBeResource.java
クラスを作成します:
package org.acme;
import io.smallrye.common.annotation.RunOnVirtualThread;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.rest.client.inject.RestClient;
@Path("/")
public class TheBestPlaceToBeResource {
static final double VALENCE_LATITUDE = 44.9;
static final double VALENCE_LONGITUDE = 4.9;
static final double ATHENS_LATITUDE = 37.9;
static final double ATHENS_LONGITUDE = 23.7;
@RestClient WeatherService service;
@GET
@RunOnVirtualThread (1)
public String getTheBestPlaceToBe() {
var valence = service.getWeather(VALENCE_LATITUDE, VALENCE_LONGITUDE).weather().temperature();
var athens = service.getWeather(ATHENS_LATITUDE, ATHENS_LONGITUDE).weather().temperature();
// Advanced decision tree
if (valence > athens && valence <= 35) {
return "Valence! (" + Thread.currentThread() + ")";
} else if (athens > 35) {
return "Valence! (" + Thread.currentThread() + ")";
} else {
return "Athens (" + Thread.currentThread() + ")";
}
}
}
1 | 仮想スレッド上でこのメソッドを呼び出すようQuarkusに指示します。 |
アプリケーションを開発モードで実行する
仮想スレッドをサポートするOpenJDKとJVMのバージョンを使用し、 ./mvnw quarkus:dev
で devモード を起動しているようにして下さい:
> java --version
openjdk 21 2023-09-19 LTS (1)
OpenJDK Runtime Environment Temurin-21+35 (build 21+35-LTS)
OpenJDK 64-Bit Server VM Temurin-21+35 (build 21+35-LTS, mixed mode)
> ./mvnw quarkus:dev (2)
1 | 19以上必須、21以上を推奨 |
2 | 開発モードを起動する |
次に、ブラウザで http://localhost:8080 を開きます。 次のようなメッセージが表示されるはずです:
Valence! (VirtualThread[#144]/runnable@ForkJoinPool-1-worker-6)
ご覧のように、エンドポイントは仮想スレッド上で実行されます。
舞台裏で何が起こったのかを理解することが不可欠です:
-
Quarkusは、(
@RunOnVirtualThread
アノテーションのため)エンドポイントを呼び出すための仮想スレッドを作成します。 -
コードがRESTクライアントを呼び出すと、仮想スレッドはブロックされますが、キャリア・スレッドはブロックされません(これが仮想スレッドの マジック・タッチ です)。
-
RESTクライアントの最初の呼び出しが完了すると、仮想スレッドは再スケジュールされ、実行を継続します。
-
2回目のRESTクライアント呼び出しが発生し、仮想スレッドは再びブロックされます(キャリアスレッドはブロックされません)。
-
最後に、RESTクライアントの2回目の呼び出しが完了すると、仮想スレッドは再スケジュールされ、実行を継続します。
-
メソッドは結果を返します。仮想スレッドは終了します。
-
結果はQuarkusによってキャプチャされ、HTTPレスポンスに書き込まれます。
テストによるピン止めの検証
pom.xml
では、surefire と failafe プラグインに argLine
引数を追加しました:
<argLine>-Djdk.tracePinnedThreads</argLine>
-Djdk.tracePinnedThreads
は、仮想スレッドをスムーズに アンマウント できない場合(キャリアスレッドをブロックすることを意味する)、スタックトレースをダンプします。
これが pinning と呼ばれるものです(詳細は 仮想スレッドリファレンスガイド を参照してください)。
テストでこのフラグを有効にすることをお勧めします。 こうすることで、仮想スレッドを使用したときにアプリケーションが正しく動作することを確認できます。テストを実行した後のログをチェックしてください。 スタックトレースが表示されたら、何が問題なのかを調べましょう。もしあなたのコード (あるいは依存関係) がピンなら、代わりに通常のワーカースレッドを使ったほうがよいかもしれません。
以下の内容で src/test/java/org/acme/TheBestPlaceToBeResourceTest.java
クラスを作成します:
package org.acme;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;
@QuarkusTest
class TheBestPlaceToBeResourceTest {
@Test
void verify() {
RestAssured.get("/")
.then()
.statusCode(200);
}
}
これは単純なテストですが、少なくともアプリケーションがピン止めされているかどうかを検出します。どちらかを使ってテストを実行してください:
-
開発モードで
r
(継続的テストを使用) -
./mvnw test
ご覧のとおり、ピン留めは行われず、スタック トレースも行われません。 これは、REST クライアントが仮想スレッドに適した方法で実装されているためです。
結合テストでも同じアプローチが使えます。
まとめ
このガイドでは、Quarkus RESTとRESTクライアントで仮想スレッドを使用する方法を説明しました。 仮想スレッドのサポートについては、こちらを参照してください:
-
メッセージングアプリケーションにおける @RunOnVirtualThread (このガイドでは Apache Kafka を扱います)
-
仮想スレッドリファレンスガイド (ネイティブコンパイルとコンテナ化を含む)