Use virtual threads in REST applications
在本指南中,我们将介绍如何在 REST 应用程序中使用虚拟线程。由于虚拟线程与 I/O 息息相关,因此我们还将使用 REST 客户端。
In this guide, we see how you can use virtual threads in a REST application. Because virtual threads are all about I/O, we will also use the REST client.
Prerequisites
Unresolved directive in rest-virtual-threads.adoc - include::{includes}/prerequisites.adoc[]
Architecture
此指南中构建的应用程序非常简单。它为两个城市(法国瓦朗斯和希腊雅典)调用天气服务,并根据当前温度决定最佳地点。
The application built into this guide is quite simple. It calls a weather service for two cities (Valence, France, and Athens, Greece) and decides the best place based on the current temperature.
Create the Maven project
首先,我们需要一个新项目。使用以下命令创建一个新项目:
First, we need a new project. Create a new project with the following command:
Unresolved directive in rest-virtual-threads.adoc - include::{includes}/devtools/create-app.adoc[]
此命令生成一个新项目,用于导入 Quarkus REST(以前称为 RESTEasy Reactive)、REST 客户端和 Jackson 扩展,此外特别添加了以下依赖项:
This command generates a new project importing the Quarkus REST (formerly RESTEasy Reactive), REST client, and Jackson extensions, and in particular, adds the following dependencies:
<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")
您可能想知道我们为什么要使用 reactive 扩展。虚拟线程有局限性,只有在使用响应式扩展时,我们才能正确地集成它们。不必担心:您的代码将 100% 以同步/命令式的方式编写。 You might wonder why we use reactive extensions. Virtual threads have limitations, and we can only integrate them properly when using the reactive extensions. No worries: your code will be written 100% in a synchronous / imperative style. 有关详细信息,请检查virtual thread reference guide。 Check the virtual thread reference guide for details. |
Prepare the pom.xml
file
我们需要定制`pom.xml` 文件以使用虚拟线程。
We need to customize the pom.xml
file to use virtual threads.
1) 找到 <maven.compiler.release>17</maven.compiler.release>
所在的行,并用以下内容替换它:
1) Locate the line with <maven.compiler.release>17</maven.compiler.release>
, and replace it with:
<maven.compiler.release>21</maven.compiler.release>
2) 在 maven-surefire-plugin 和 maven-failsafe-plugin 配置中,添加以下 argLine
参数:
2) In the maven-surefire-plugin and maven-failsafe-plugin configurations, add the following argLine
parameter:
<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
将在运行测试时检测固定的载体线程(请参阅 the virtual thread reference guide for details)。
The -Djdk.tracePinnedThreads
will detect pinned carrier threads while running tests (See the virtual thread reference guide for details).
如果您使用的是 Java 19 或 20,则在 argLine
和 maven 编译器插件的 parameters
中添加 --enable-preview
标志。请注意,我们强烈推荐 Java 21。
If you are using a Java 19 or 20, add the --enable-preview
flag in the argLine
and as parameters
of the maven compiler plugin.
Note that we strongly recommend Java 21.
Create the weather client
本节与虚拟线程无关。因为我们需要进行一些 I/O 以演示虚拟线程的使用,我们需要一个执行 I/O 操作的客户端。此外,REST 客户端有利于虚拟线程:它不固定且能正确处理传播。
This section is not about virtual threads. Because we need to do some I/O to demonstrate virtual threads usage, we need a client doing I/O operations. In addition, the REST client is virtual thread friendly: it does not pin and handle propagation correctly.
创建具有以下内容的 src/main/java/org/acme/WeatherService.java
类:
Create the src/main/java/org/acme/WeatherService.java
class with the following content:
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 交互。在专门的 guide 中了解有关 rest 客户端的更多信息。
This class models the HTTP interaction with the weather service. Read more about the rest client in the dedicated guide.
Create the HTTP endpoint
现在,创建具有以下内容的 src/main/java/org/acme/TheBestPlaceToBeResource.java
类:
Now, create the src/main/java/org/acme/TheBestPlaceToBeResource.java
class with the following content:
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 | Instructs Quarkus to invoke this method on a virtual thread |
Run the application in dev mode
确保您使用 OpenJDK 和支持虚拟线程的 JVM 版本,并使用 ./mvnw quarkus:dev
启动 dev mode:
Make sure that you use OpenJDK and JVM versions supporting virtual thread and launch the dev mode with ./mvnw quarkus: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 | Launch the dev mode |
然后,在浏览器中,打开 [role="bare"][role="bare"]http://localhost:8080。您应获得类似以下内容的信息:
Then, in a browser, open [role="bare"]http://localhost:8080. You should get something like:
Valence! (VirtualThread[#144]/runnable@ForkJoinPool-1-worker-6)
如您所见,端点在虚拟线程上运行。
As you can see, the endpoint runs on a virtual thread.
了解幕后发生了什么至关重要:
It’s essential to understand what happened behind the scene:
-
Quarkus creates the virtual thread to invoke your endpoint (because of the
@RunOnVirtualThread
annotation). -
When the code invokes the rest client, the virtual thread is blocked, BUT the carrier thread is not blocked (that’s the virtual thread magic touch).
-
Once the first invocation of the rest client completes, the virtual thread is rescheduled and continues its execution.
-
The second rest client invocation happens, and the virtual thread is blocked again (but not the carrier thread).
-
Finally, when the second invocation of the rest client completes, the virtual thread is rescheduled and continues its execution.
-
The method returns the result. The virtual thread terminates.
-
The result is captured by Quarkus and written in the HTTP response
Verify pinning using tests
在`pom.xml,`中,我们向 surefire 和 failsafe 插件添加了`argLine`参数:
In the pom.xml,
we added an argLine
argument to the surefire and failsafe plugins:
<argLine>-Djdk.tracePinnedThreads</argLine>
如果虚拟线程不能顺利_unmounted_(意味着它阻塞了载波线程),`-Djdk.tracePinnedThreads`倾倒堆栈轨迹。这就是我们称之为_pinning_的情况(更多信息请参阅the virtual thread reference guide)。
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.
使用以下内容创建`src/test/java/org/acme/TheBestPlaceToBeResourceTest.java`类:
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
您将看到,它并未固定 - 没有堆栈轨迹。这是因为 REST 客户端是以对虚拟线程友好的方式实现的。
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.
同样的方法也可以用于集成测试。
The same approach can be used with integration tests.
Conclusion
本指南演示了如何将虚拟线程与 Quarkus REST 和 REST 客户端配合使用。详细了解涉及虚拟线程支持的内容:
This guide shows how you can use virtual threads with Quarkus REST and the REST client. Learn more about virtual threads support on:
-
@RunOnVirtualThread in messaging applications (this guide covers Apache Kafka)
-
the virtual thread reference guide (include native compilation and containerization)