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("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 | Must be 19+, we recommend 21+ |
2 | 開発モードを起動する |
次に、ブラウザで http://localhost:8080 を開きます。 次のようなメッセージが表示されるはずです:
Valence! (VirtualThread[#144]/runnable@ForkJoinPool-1-worker-6)
ご覧のように、エンドポイントは仮想スレッド上で実行されます。
舞台裏で何が起こったのかを理解することが不可欠です:
-
Quarkusは、(
@RunOnVirtualThread
アノテーションのため)エンドポイントを呼び出すための仮想スレッドを作成します。 -
コードがRESTクライアントを呼び出すと、仮想スレッドはブロックされますが、キャリア・スレッドはブロックされません(これが仮想スレッドの マジック・タッチ です)。
-
RESTクライアントの最初の呼び出しが完了すると、仮想スレッドは再スケジュールされ、実行を継続します。
-
2回目のRESTクライアント呼び出しが発生し、仮想スレッドは再びブロックされます(キャリアスレッドはブロックされません)。
-
最後に、RESTクライアントの2回目の呼び出しが完了すると、仮想スレッドは再スケジュールされ、実行を継続します。
-
メソッドは結果を返します。仮想スレッドは終了します。
-
結果はQuarkusによってキャプチャされ、HTTPレスポンスに書き込まれます。
テストによるピン止めの検証
In the pom.xml,
we added an argLine
argument to the surefire and failsafe plugins:
<argLine>-Djdk.tracePinnedThreads</argLine>
The -Djdk.tracePinnedThreads
dumps the stack trace if a virtual thread cannot be unmounted smoothly (meaning that it blocks the carrier thread).
That’s what we call pinning (more info in the virtual thread reference guide).
We recommend enabling this flag in tests. Thus, you can check that your application behaves correctly when using virtual threads. Just check your log after having run the test. If you see a stack trace… better check what’s wrong. If your code (or one of your dependencies) pins, it might be better to use regular worker thread instead.
Create the src/test/java/org/acme/TheBestPlaceToBeResourceTest.java
class with the following content:
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);
}
}
It is a straightforward test, but at least it will detect if our application is pinning. Run the test with either:
-
r
in dev mode (using continuous testing) -
./mvnw test
As you will see, it does not pin - no stack trace. It is because the REST client is implemented in a virtual-thread-friendly way.
結合テストでも同じアプローチが使えます。
まとめ
このガイドでは、Quarkus RESTとRESTクライアントで仮想スレッドを使用する方法を説明しました。 仮想スレッドのサポートについては、こちらを参照してください:
-
メッセージングアプリケーションにおける @RunOnVirtualThread (このガイドでは Apache Kafka を扱います)
-
仮想スレッドリファレンスガイド (ネイティブコンパイルとコンテナ化を含む)