Use virtual threads in REST applications

在本指南中,我们将介绍如何在 REST 应用程序中使用虚拟线程。由于虚拟线程与 I/O 息息相关,因此我们还将使用 REST 客户端。

Prerequisites

如要完成本指南,您需要:

  • Roughly 15 minutes

  • An IDE

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

  • Apache Maven ${proposed-maven-version}

  • 如果你想使用 Quarkus CLI, 则可以选择使用

  • 如果你想构建一个本机可执行文件(或如果你使用本机容器构建,则使用 Docker),则可以选择安装 Mandrel 或 GraalVM 以及 configured appropriately

Architecture

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

Create the Maven project

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

CLI
quarkus create app {create-app-group-id}:{create-app-artifact-id} \
    --no-code
cd {create-app-artifact-id}

要创建一个 Gradle 项目,添加 --gradle--gradle-kotlin-dsl 选项。 有关如何安装和使用 Quarkus CLI 的详细信息,请参见 Quarkus CLI 指南。

Maven
mvn {quarkus-platform-groupid}:quarkus-maven-plugin:{quarkus-version}:create \
    -DprojectGroupId={create-app-group-id} \
    -DprojectArtifactId={create-app-artifact-id} \
    -DnoCode
cd {create-app-artifact-id}

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

适用于 Windows 用户:

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

  • 如果使用 Powershell,将 -D 参数用双引号引起来,例如 "-DprojectArtifactId={create-app-artifact-id}"

此命令生成一个新项目,用于导入 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("quarkus-rest-client-jackson")

您可能想知道我们为什么要使用 reactive 扩展。虚拟线程有局限性,只有在使用响应式扩展时,我们才能正确地集成它们。不必担心:您的代码将 100% 以同步/命令式的方式编写。 有关详细信息,请检查virtual thread reference guide

Prepare the pom.xml file

我们需要定制`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 将在运行测试时检测固定的载体线程(请参阅 the virtual thread reference guide for details)。

Example 1. --enable-preview on Java 19 and 20

如果您使用的是 Java 19 或 20,则在 argLine 和 maven 编译器插件的 parameters 中添加 --enable-preview 标志。请注意,我们强烈推荐 Java 21。

Create the weather client

本节与虚拟线程无关。因为我们需要进行一些 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 交互。在专门的 guide 中了解有关 rest 客户端的更多信息。

Create the HTTP endpoint

现在,创建具有以下内容的 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 在虚拟线程上调用此方法

Run the application in dev mode

确保您使用 OpenJDK 和支持虚拟线程的 JVM 版本,并使用 ./mvnw quarkus:dev 启动 dev mode

> 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 Launch the dev mode

然后,在浏览器中,打开 [role="bare"][role="bare"]http://localhost:8080。您应获得类似以下内容的信息:

Valence! (VirtualThread[#144]/runnable@ForkJoinPool-1-worker-6)

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

了解幕后发生了什么至关重要:

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

  2. 当代码调用 rest 客户端时,虚拟线程被阻塞,但载体线程没有被阻塞(这是虚拟线程 magic touch)。

  3. 出于休息客户端第一次调用的完成,虚拟线程重新安排并继续执行。

  4. 第二次休息客户端调用发生,虚拟线程再次被阻塞(但不是载波线程)。

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

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

  7. 结果被 Quarkus 捕获并写入 HTTP 响应中

Verify pinning using tests

在`pom.xml,`中,我们向 surefire 和 failsafe 插件添加了`argLine`参数:

<argLine>-Djdk.tracePinnedThreads</argLine>

如果虚拟线程不能顺利_unmounted_(意味着它阻塞了载波线程),`-Djdk.tracePinnedThreads`倾倒堆栈轨迹。这就是我们称之为_pinning_的情况(更多信息请参阅the virtual thread reference guide)。

我们建议在测试中启用此标志。因此,您可以检查在使用虚拟线程时应用程序的行为是否正确。只需在运行测试后检查日志即可。如果您看到堆栈轨迹…​…​最好检查一下有什么问题。如果您的代码(或依赖项之一)固定,最好使用常规工作线程。

使用以下内容创建`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 客户端是以对虚拟线程友好的方式实现的。

同样的方法也可以用于集成测试。

Conclusion

本指南演示了如何将虚拟线程与 Quarkus REST 和 REST 客户端配合使用。详细了解涉及虚拟线程支持的内容: