编辑此页面

在 REST 应用程序中使用虚拟线程

在本指南中,我们将了解如何在 REST 应用程序中使用虚拟线程。由于虚拟线程与 I/O 密切相关,我们还将使用 REST 客户端。

先决条件

要完成本指南,您需要

  • 大约 15 分钟

  • 一个 IDE

  • 已安装 JDK 17+ 并正确配置了 JAVA_HOME

  • Apache Maven 3.9.9

  • 如果您想使用它,可以选择 Quarkus CLI

  • 如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置

架构

本指南中构建的应用程序非常简单。它调用两个城市(法国瓦朗斯和希腊雅典)的天气服务,并根据当前温度决定最佳去处。

创建 Maven 项目

首先,我们需要一个新项目。使用以下命令创建一个新项目

CLI
quarkus create app org.acme:rest-virtual-threads \
    --extension='rest-jackson,quarkus-rest-client-jackson' \
    --no-code
cd rest-virtual-threads

要创建 Gradle 项目,请添加 --gradle--gradle-kotlin-dsl 选项。

有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.24.4:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=rest-virtual-threads \
    -Dextensions='rest-jackson,quarkus-rest-client-jackson' \
    -DnoCode
cd rest-virtual-threads

要创建 Gradle 项目,请添加 -DbuildTool=gradle-DbuildTool=gradle-kotlin-dsl 选项。

对于 Windows 用户

  • 如果使用 cmd,(不要使用反斜杠 \ 并将所有内容放在同一行上)

  • 如果使用 Powershell,请将 -D 参数用双引号括起来,例如 "-DprojectArtifactId=rest-virtual-threads"

此命令生成一个新项目,导入 Quarkus REST(以前称为 RESTEasy Reactive)、REST 客户端和 Jackson 扩展,特别是添加以下依赖项

pom.xml
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-rest-jackson</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
build.gradle
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,请在 argLine 中添加 --enable-preview 标志,并将其作为 maven compiler plugin 的 parameters。请注意,我们强烈建议使用 Java 21。

创建天气客户端

本节与虚拟线程无关。因为我们需要进行一些 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)

如您所见,该端点在虚拟线程上运行。

了解幕后发生的事情至关重要

  1. Quarkus 创建虚拟线程来调用您的端点(因为 @RunOnVirtualThread 注释)。

  2. 当代码调用 rest 客户端时,虚拟线程被阻塞,但 carrier 线程未被阻塞(这就是虚拟线程的魔力)。

  3. 一旦 rest 客户端的第一次调用完成,虚拟线程将被重新调度并继续执行。

  4. 第二次 rest 客户端调用发生,虚拟线程再次被阻塞(但 carrier 线程没有)。

  5. 最后,当 rest 客户端的第二次调用完成时,虚拟线程将被重新调度并继续执行。

  6. 该方法返回结果。虚拟线程终止。

  7. 结果被 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 客户端中使用虚拟线程。了解更多关于虚拟线程支持的信息,请访问

相关内容