协同发力:Spring Boot 3.2、GraalVM 原生镜像、Java 21 和 Project Loom 虚拟线程,

工程技术 | Josh Long | 2023年9月9日 | ...

这已经酝酿了很长一段时间,但最终,我们可以创建使用 Spring Boot(通过 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 或更早的版本中,I/O 是阻塞的。调用 int InputStream#read() 时,你可能需要等待下一个字节最终到达。在 java.io.File I/O 中,延迟很少。然而,在网络上,你永远无法确定。客户端可能断开连接。客户端可能正在通过隧道。再说一遍,这很难说。在此期间,程序流程据说被阻塞,无法在执行线程上继续进行。在下面的代码片段中,我们无法知道何时会看到 after 这个词被打印出来。可能是一纳秒之后,也可能是一周之后。它是阻塞的。

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

这已经够糟糕的了,但在 Java 21 之前,Java 中当前的线程架构使其变得更糟。目前,每个线程或多或少地映射到一个原生操作系统线程。创建更多线程也很昂贵,大约需要两兆字节的内存。

当然,也有绕过这个问题的方法。你可以使用 Java NIO (java.nio.\*) 启用的非阻塞 I/O。在这种模式下,你请求字节并注册一个回调,该回调仅在实际有字节可用时执行。没有等待,没有阻塞。这种方法有一个显著的优势,即在无事可做时,它不会占用线程,允许其他线程在此期间使用它们。然而,这有点乏味且底层。Spring 对响应式编程提供了出色的支持,它在非阻塞 I/O 的基础上提供了一种函数式风格的编程模型。它运行良好。但是,它需要改变你编写代码的方式。如果你能像上面演示的那样,直接使用现有的代码,让它做正确的事情,在没有发生任何事情时透明地将执行流移出线程,然后在有事情发生时恢复执行流,那难道不好吗?绝对如此。这就是 Project Loom 的承诺。获取上面的代码,确保你在虚拟线程中执行它(很简单,你可以使用 ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()),然后它就能正常工作

Spring Boot 3.2

你的典型 Spring Boot 应用到处都使用线程池(ExecutorExecutorService 实例)!在你的 Web 服务、消息逻辑等方面。而现在,在新的 Spring Boot 3.2 里程碑版本中(最终版本预计于 2023 年 11 月发布),你可以让 Spring Boot 使用虚拟执行器,只需一个简单的属性设置:spring.threads.virtual.enabled=true

协同发力

请记住:Spring Boot 3.2 尚未正式发布 (GA)。Java 21 尚未正式发布。支持 Java 21 的 GraalVM 尚未正式发布。情况还有些困难,但我一直渴望将所有东西一起尝试:在 Spring Boot 应用中使用 GraalVM 原生镜像和虚拟线程。当一切看起来都准备就绪可以尝试时,我在 GraalVM 编译器中发现了一个需要克服的小错误!当然,这对于出色的 GraalVM 团队来说不是问题,但正如我所说:情况还有点困难。不过还是值得的!让我们把所有必要的准备工作做好,以便你可以尝试一下。

安装适用于 Java 21 的 GraalVM

首先,我们需要安装 GraalVM 和 Java 21。我使用的是 Apple Silicon / ARM 架构的 Mac,所以我选择了 最新版本(截至本文撰写时)中的 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.spring.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。现在我们进展顺利了!

基本上,这次小小的探索已经接近尾声了,但我还是要再强调一次——这还不是正式发布 (GA) 的软件!如果一切按计划进行,它将在 2023 年 11 月底发布,但目前还没有。这就是为什么发布这篇博客对我来说非常有价值:我想让你去尝试一下。即使是 Project Loom,它将在不到两周后(2023 年 9 月 19 日)部分纳入 Java 21,但严格来说它还没有全部完成。在此版本中我们会获得一部分功能,但还有另外两方面的支持你可以作为预览特性今天就尝试。如果你发现了问题,将这些信息反馈到流程中是非常有价值的,这样就可以现在就解决这些问题,而不是以后。毕竟,我两周前才刚刚在 GraalVM 编译器中发现了一个 bug!所以,去尝试一下吧。现在是成为 Java 开发者的绝佳时机。而 Project Loom 和 GraalVM 这两项技术就像是免费的赠礼。如果成功实现,你的 Spring Boot 工作负载将获得更好的运行时可伸缩性、能效、启动时间、内存消耗等。升级,尝试一下。我敢打赌你会爱上它的。

获取 Spring 新闻邮件

订阅 Spring 新闻邮件,保持联系

订阅

领先一步

VMware 提供培训和认证,助你加速进步。

了解更多

获取支持

Tanzu Spring 提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件,一站式订阅即可获得。

了解更多

即将到来的活动

查看 Spring 社区所有即将到来的活动。

查看全部