在 REST 应用程序中使用虚拟线程
在本指南中,我们将了解如何在 REST 应用程序中使用虚拟线程。由于虚拟线程与 I/O 密切相关,我们还将使用 REST 客户端。
先决条件
要完成本指南,您需要
-
大约 15 分钟
-
一个 IDE
-
已安装 JDK 17+ 并正确配置了
JAVA_HOME
-
Apache Maven 3.9.9
-
如果您想使用它,可以选择 Quarkus CLI
-
如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置
创建 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
将在运行测试时检测固定的 carrier 线程(有关详细信息,请参阅虚拟线程参考指南)。
--enable-preview 在 Java 19 和 20 上
如果您使用的是 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
启动开发模式
> 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 | 启动开发模式 |
然后,在浏览器中,打开 https://:8080。您应该得到类似的东西
Valence! (VirtualThread[#144]/runnable@ForkJoinPool-1-worker-6)
如您所见,该端点在虚拟线程上运行。
了解幕后发生的事情至关重要
-
Quarkus 创建虚拟线程来调用您的端点(因为
@RunOnVirtualThread
注释)。 -
当代码调用 rest 客户端时,虚拟线程被阻塞,但 carrier 线程未被阻塞(这就是虚拟线程的魔力)。
-
一旦 rest 客户端的第一次调用完成,虚拟线程将被重新调度并继续执行。
-
第二次 rest 客户端调用发生,虚拟线程再次被阻塞(但 carrier 线程没有)。
-
最后,当 rest 客户端的第二次调用完成时,虚拟线程将被重新调度并继续执行。
-
该方法返回结果。虚拟线程终止。
-
结果被 Quarkus 捕获并写入 HTTP 响应中
使用测试验证固定
在 pom.xml
中,我们向 surefire 和 failsafe 插件添加了 argLine
参数
<argLine>-Djdk.tracePinnedThreads</argLine>
如果虚拟线程无法平稳地卸载(意味着它会阻塞 carrier 线程),-Djdk.tracePinnedThreads
会转储堆栈跟踪。这就是我们所说的固定(更多信息请参见虚拟线程参考指南)。
我们建议在测试中启用此标志。因此,您可以检查您的应用程序在使用虚拟线程时是否行为正确。只需在运行测试后检查您的日志。如果您看到堆栈跟踪……最好检查一下哪里出了问题。如果您的代码(或您的依赖项之一)固定,则最好使用常规 worker 线程。
创建具有以下内容的 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)
-
虚拟线程参考指南(包括原生编译和容器化)