万剑归宗:Spring Boot 3.2、GraalVM 原生镜像、Java 21 和 Project Loom 虚拟线程

酝酿已久,我们终于可以创建使用 Spring Boot(3.2)和 Java 21 虚拟线程(Project Loom)的 GraalVM 原生镜像了!

这一切有什么意义呢?Project Loom 和 GraalVM 原生镜像各自都具有引人注目的运行时特性。我已经等了很久,终于等到了它们的融合!让我们依次唠唠。

GraalVM 原生镜像

GraalVM 是一个 OpenJDK 发行版,提供了一些额外的实用工具,其中包括一个名为 native-image 的工具,它可以对你的代码进行提前编译(AOT)。我们在这里不会详细介绍所有的实用功能,基本上它会对你的代码进行优化,去除你不需要的部分,然后将剩余的代码编译成针对特定操作系统和架构的原生代码,运行速度非常快。结果令人惊叹,类似于编译 C 或 Go 程序所得到的结果。生成的可执行文件在启动时几乎没有延迟,并且在运行时占用的内存要少得多。想象一下,能够部署现有的 Spring Boot 应用程序,并且它只占用几十兆字节的内存,并在几百毫秒内启动。现在,这是可能的。只需运行 ./gradlew nativeCompile./mvnw -Pnative native:compile,即可。自 2022 年 11 月 Spring Boot 3.0 发布以来,Spring Boot 已经支持在生产环境中使用 GraalVM 原生镜像。

Project Loom

Project Loom 为 JVM 引入了透明的 Fiber(纤程,也成为协程、虚拟线程)。就目前而言,在 Java 20 或更早的版本中,IO 是阻塞的。调用 InputStream#read() 可能需要等待下一个字节的到达。在 java.io.File IO中,很少会有太多延迟。然而,在网络中,你真的无法确定。客户端可能会断开连接。客户端可能会经过一个隧道。同样,很难说。在此期间,程序流程被阻塞在执行线程上。在下面的代码片段中,我们无法知道何时会看到打印出来的单词 after。可能是从现在开始的纳秒级时间,也可能是从现在开始的一周后。它是阻塞的。

InputStream in = ... 
System.out.println("before");
int next = in.read(); 
System.out.println("after");

这已经够难受了,但 Java 21 之前的 Java 线程架构使情况变得更糟。目前,每个线程或多或少都会映射到一个本地操作系统线程。创建更多线程的成本也很高,大约需要两兆内存。

当然有办法解决这个问题。你可以使用非阻塞 IO,通过 Java NIO(java.nio.*)来实现。在这种模型中,你请求字节并注册一个回调函数,只有当有实际可用的字节时才执行该回调函数。没有等待,没有阻塞。这种方法的重要好处是,当没有任务需要执行时,它可以让我们不占用线程,同时允许其他任务使用这些线程。然而,这种方法有点繁琐且低级。Spring 对响应式编程提供了出色的支持,它在非阻塞 IO 之上提供了一种函数式编程模型。它工作得很好。但是,它需要改变你编写代码的方式。如果你可以直接使用上面演示的现有代码,并在没有任何操作时自动将“执行流程”从线程中转移出来,然后在有操作时恢复“执行流程”,那该多好啊!这就是 Project Loom 的承诺。使用虚拟线程执行上面的代码非常简单(可以使用 ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()),它就能正常工作!

Spring Boot 3.2

典型的 Spring Boot 应用程序在各个地方都使用线程池(ExecutorExecutorService 实例), Web 服务、消息处理逻辑等等。而现在,在新的 Spring Boot 3.2 里程碑版本(最终版本预计在 2023 年 11 月发布),你可以通过一个简单的属性 spring.threads.virtual.enabled=true 让 Spring Boot 使用 virtual executor。

整合到一起

注意:Spring Boot 3.2 目前还未正式发布。Java 21 也还未正式发布。支持 Java 21 的 GraalVM 也还未正式发布。目前情况还有些不稳定,但我一直渴望尝试将所有东西都结合在一起:在一个 Spring Boot 应用程序中使用 GraalVM 原生镜像、虚拟线程等。当一切看起来准备就绪时,我发现了 GraalVM 编译器中的一个小 bug 需要解决!当然,这对于了不起的 GraalVM 团队来说不是问题,但正如我所说:情况有些不稳定。不过,这是值得的!让我们把所有的组件都准备好,这样你就可以尝试一下。

安装适用于 Java 21 的 GraalVM

首先,我们需要安装 GraalVM 和 Java 21。我的 Mac 采用的是 Apple Silicon / ARM 架构,因此我选择了最新发布的 graalvm-community-java21-darwin-aarch64-dev.tar.gz(截至本文撰写时)。你可以直接下载并解压缩,但要确保正确配置 JAVA_HOMEPATH 等关键环境变量。我是 SDKMan 项目 的粉丝,所以用它来管理这个新下载的版本。将 .tar.gz 文件解压到一个名为 ~/bin/graalvm-community-openjdk-21/ 的文件夹,然后运行以下命令。

sdk install java graalvm-ce-21 $HOME/bin/graalvm-community-openjdk-21/Contents/Home

然后,为了确保它在所有地方都可用:

sdk default java graalvm-ce-21

打开一个新的 shell,并确认已成功:

> native-image --version 
native-image 21 2023-09-19
GraalVM Runtime Environment GraalVM CE 21-dev+35.1 (build 21+35-jvmci-23.1-b14)
Substrate VM GraalVM CE 21-dev+35.1 (build 21+35, serial gc)

配置 Spring Boot 项目以使用 Java 21

在浏览器打开 Spring Initializr(start.springboot.io),指定版本 3.2.0 (M2)(显然是更高版本),添加 GraalVMWeb,然后下载压缩包,打开并将其加载到你的 IDE 中。我们仍然需要配置构建以适用于Java 21。目前情况并不理想,因为 Gradle 实际上并不了解 Java 21,但它会工作。基本上。我对 Gradle 的熟练程度几乎为零,但是以下配置似乎可以工作:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.0-M2'
    id 'io.spring.dependency-management' version '1.1.3'
    id 'org.graalvm.buildtools.native' version '0.9.24'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '21'
}

graalvmNative {

    binaries {
        main {
            buildArgs.add('--enable-preview')
        }
    }
}

java {
    toolchain { languageVersion = JavaLanguageVersion.of(21) }
}

repositories {
    mavenCentral()
    maven { url 'https://repo.spring.io/snapshot' }
    maven { url 'https://repo.spring.io/milestone' }
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

将其重新导入 IDE 的构建配置中。

application.properties 中添加以下属性:

spring.threads.virtual.enabled=true

然后将你的 main(String[] args) 类改成如下:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Collection;
import java.util.Set;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

}

@RestController
class CustomersHttpController { 

    @GetMapping("/customers")
    Collection<Customer> customers() {
        return Set.of(new Customer(1, "A"), new Customer(2, "B"), new Customer(3, "C"));
    }

    record Customer(Integer id, String name) {
    }

}

你可以像往常一样运行程序:./gradlew bootRun,它使用的是 Project Loom!真正牛逼的是:让我们构建一个 GraalVM 原生镜像!./gradlew nativeCompile。这可能需要一两分钟

编译完成后,就可以运行 build 目录下的原生二进制文件:./build/native/nativeCompile/demo

我们基本上已经接近完成线了,但是让我再次提醒你一下:这仍然不是正式发布的软件!如果一切按计划进行,到 2023 年 11 月底就会完成,但现在还没有。这就是为什么对我来说发布这篇博客非常有价值的原因:我希望你们去尝试一下。即使 Project Loom,它的一部分也将在不到两周后的 2023 年 9 月 19 日发布的 Java 21 中出现,但严格来说还没有全部完成。我们将在这个版本中获得其中的一部分功能,但还有另外两个支持列可以作为预览功能进行尝试。如果你发现了什么问题,将这些信息反馈给开发团队非常重要,这样可以及时解决这些问题,而不是拖到以后。毕竟,就在不到两周前,我发现了 GraalVM 编译器中的一个错误!所以,去尝试一下吧。现在是 Java 开发人员最好的时机。Loom 和 GraalVM 带来的收益是免费的。如果成功实施,你的 Spring Boot 应用将会获得更好的运行时可扩展性、效率、启动时间、内存消耗等等。升级,尝试一下,你会喜欢上它们的。


参考:https://spring.io/blog/2023/09/09/all-together-now-spring-boot-3-2-graalvm-native-images-java-21-and-virtual