你好,Java 22!

工程 | Josh Long | 2024 年 3 月 19 日 | ...

更新 我后来就这个主题发布了一个 Spring Tips 视频!如果你愿意,可以观看那个。

各位 Spring 爱好者们,大家好!庆祝 Java 22 发布日快乐!你已经下载了吗?快去下载吧!Java 22 是一个 重大 改进,我认为值得每个人升级。有一些重要的最终发布特性,比如 Project Panama,还有大量更出色的预览特性。我不可能涵盖所有特性,但我想谈谈我最喜欢的几个。我们将介绍许多特性。如果你想跟着动手,代码在此处 (https://github.com/spring-tips/java22)

我热爱 Java 22,当然也热爱 GraalVM,而且今天它们都发布了新版本!Java 当然是我们最喜欢的运行时和语言,而 GraalVM 是一个高性能的 JDK 发行版,它支持额外的语言并允许预先(AOT)编译(它们被称为 GraalVM native image)。GraalVM 包含了新 Java 22 版本的所有优点,还附带一些额外的工具,所以我总是建议直接下载它。我特别感兴趣的是 GraalVM native image 的能力。生成的二进制文件几乎瞬时启动,并且与 JRE 对应的文件相比,占用的内存显著减少。GraalVM 并不新,但值得记住的是,Spring Boot 有一个很棒的引擎支持将你的 Spring Boot 应用转换为 GraalVM native image。

安装

以下是我所做的。

我正在使用出色的 Java 包管理器 SDKMAN。我还在运行 macOS 的 Apple Silicon 芯片上工作。这一点,以及我喜欢并鼓励使用 GraalVM 的事实,稍后会有点重要,所以请不要忘记。会有个测试!

sdk install java 22-graalce

我也建议将其设为默认值

sdk default java 22-graalce

在继续之前打开一个新的 shell,然后通过运行 javac --versionjava --versionnative-image --version 来验证一切是否正常工作。

如果你在遥远的未来(我们有飞行汽车了吗?)阅读此文,并且看到了 50-graalce,那么务必安装它!版本越高越好!

总得有个开始...

此时,我想开始构建了!所以,我去了网上我第二喜欢的地方,Spring Initializr - start.spring.io - 并按照以下规范生成了一个新项目

  • 我选择了 Spring Boot 的 3.3.0-snapshot 版本。3.3 尚未 GA(普遍可用),但应该在几个月内发布。与此同时,我们继续前进!此版本对 Java 22 有更好的支持。
  • 我选择了 Maven 作为构建工具。
  • 我添加了 GraalVM Native Support 支持、H2 DatabaseJDBC API 支持。

我像这样在 IDE 中打开了项目:idea pom.xml。现在我需要配置一些 Maven 插件来支持 Java 22 以及本文中将要介绍的一些预览特性。这是我完整配置的 pom.xml。它有点复杂,所以我们将在代码之后进行讲解。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.0-SNAPSHOT</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>22</java.version>
    </properties>
    <dependencies>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.graalvm.sdk</groupId>
            <artifactId>graal-sdk</artifactId>
            <version>23.1.2</version>
        </dependency>
        <dependency>
            <groupId>org.graalvm.nativeimage</groupId>
            <artifactId>svm</artifactId>

 <version>23.1.2</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
                <version>0.10.1</version>
                <configuration>
                    <buildArgs>
                        <buildArg> --features=com.example.demo.DemoFeature</buildArg>
                        <buildArg> --enable-native-access=ALL-UNNAMED </buildArg>
                        <buildArg> -H:+ForeignAPISupport</buildArg>
                        <buildArg> -H:+UnlockExperimentalVMOptions</buildArg>
                        <buildArg> --enable-preview</buildArg>
                    </buildArgs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <argLine>--enable-preview</argLine>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <enablePreview>true</enablePreview>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <compilerArguments> --enable-preview </compilerArguments>
                    <jvmArguments> --enable-preview</jvmArguments>
                </configuration>
            </plugin>
            <plugin>
			<groupId>io.spring.javaformat</groupId>
			<artifactId>spring-javaformat-maven-plugin</artifactId>
			<version>0.0.41</version>
			<executions>
				<execution>
					<phase>validate</phase>
					<inherited>true</inherited>
					<goals>
						<goal>validate</goal>
					</goals>
				</execution>
			</executions>
		</plugin>
        </plugins>
    </build>
    <repositories>
    <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
        <repository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </repository>
    </repositories>
    <pluginRepositories>
        <pluginRepository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </pluginRepository>
        <pluginRepository>
            <id>spring-snapshots</id>
            <name>Spring Snapshots</name>
            <url>https://repo.spring.io/snapshot</url>
            <releases>
                <enabled>false</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>
</project>

我知道,我知道!东西很多!但其实不然。这个 pom.xml 几乎与我从 Spring Initializr 获得的一样。主要的更改是

  • 我重新定义了 maven-surefire-pluginmaven-compiler-plugin 以支持预览特性。
  • 我添加了 spring-javaformat-maven-plugin 以支持格式化我的源代码。
  • 我添加了两个新的依赖项:org.graalvm.sdk:graal-sdk:23.1.2org.graalvm.nativeimage:svm:23.1.2,这两个依赖项仅用于创建我们稍后需要的 GraalVM Feature 实现。
  • 我在 <configuration> 部分为 native-maven-pluginspring-boot-maven-plugin 添加了配置节。

很快,Spring Boot 3.3 将会 GA 并支持 Java 22,因此这个构建文件的一半可能会消失。(这可真是 Spring 大扫除!)

快速编程说明

在本文中,我将引用一个名为 LanguageDemonstrationRunner 的函数接口类型。它只是我创建的一个函数接口,声明会抛出 Throwable,这样我就不必担心了。

package com.example.demo;

@FunctionalInterface
interface LanguageDemonstrationRunner {

    void run() throws Throwable;

}

我有一个 ApplicationRunner,它会注入我函数接口的所有实现,然后调用它们的 run 方法,捕获并处理 Throwable


    // ...	
    @Bean
	ApplicationRunner demo(Map<String, LanguageDemonstrationRunner> demos) {
		return _ -> demos.forEach((_, demo) -> {
			try {
				demo.run();
			} //
			catch (Throwable e) {
				throw new RuntimeException(e);
			}
		});
	}
    // ...

好的,说明完了.. 继续!

再见,JNI!

此版本见证了期待已久的 Project Panama 发布。这是我最期待的三个特性之一。另外两个特性,虚拟线程和 GraalVM native image,至少在六个月前就已成为现实。Project Panama 让我们能够利用长期以来我们无法使用的海量 C、C++ 代码。仔细想想,如果它支持 ELF,我想它可能基本上支持任何类型的二进制文件。例如,Rust 程序和 Go 程序可以编译成与 C 兼容的二进制文件,所以我猜想(但还没尝试)这意味着与这些语言的互操作也非常容易。大体而言,在本节中,当我谈论“native code”时,我指的是以可以像调用 C 库那样方式编译的二进制文件。

从历史上看,Java 一直非常封闭。对于 Java 开发者来说,重用 native C 和 C++ 代码并容易。这是有道理的。native 的、操作系统特定的代码只会破坏 Java “一次编写,随处运行” 的承诺。这一直有点禁忌。但我不明白为什么会这样。说实话,尽管缺乏简便的 native code 互操作性,我们也做得不错。有 JNI,我很确定它代表着 Joylessly Navigating the Inferno(乏味地穿越地狱,此处为戏称)。为了使用 JNI,你必须编写更多新的 C/C++ 代码,来将你想要使用的任何语言与 Java 粘合在一起。(这有生产力吗?谁觉得这是个好主意?)大多数人 想要 使用 JNI,就像他们 想要 做根管治疗一样(意为“非常不想”)。

大多数人并不想(用 JNI)。我们只需要以一种地道的 Java 风格的方式重新发明一切。几乎你想做的任何事情,都可能有一个纯 Java 解决方案,可以在 Java 运行的任何地方运行。它运行良好,直到它不再工作为止。Java 在这方面错过了一些关键机会。想象一下,如果 Kubernetes 是用 Java 构建的?想象一下,如果当前的 AI 革命是由 Java 驱动的?Numpy、Scipy 和 Kubernetes 最初创建时,这两个想法之所以不可想象有很多原因,但今天呢?今天,他们发布了 Project Panama。

Project Panama 引入了一种连接 native code 的简便方式。它有两种级别的支持。你可以以一种相当底层的方式,操作内存并将数据来回传递到 native code 中。我说“来回传递”,但我可能应该说“向下和向上”到 native code。Project Panama 支持“downcalls”(从 Java 调用 native code)和“upcalls”(从 native code 调用 Java)。你可以调用函数、分配和释放内存、读写 struct 中的字段等。

让我们来看一个简单的例子。代码使用新的 java.lang.foreign.* API 来查找一个名为 printf 的符号(基本上相当于 System.out.print()),分配内存(有点像 malloc)缓冲区,然后将该缓冲区传递给 printf 函数。


package com.example.demo;

import org.springframework.stereotype.Component;

import java.lang.foreign.Arena;
import java.lang.foreign.FunctionDescriptor;
import java.lang.foreign.Linker;
import java.lang.foreign.SymbolLookup;
import java.util.Objects;

import static java.lang.foreign.ValueLayout.ADDRESS;
import static java.lang.foreign.ValueLayout.JAVA_INT;

@Component
class ManualFfi implements LanguageDemonstrationRunner {

    // this is package private because we'll need it later
	static final FunctionDescriptor PRINTF_FUNCTION_DESCRIPTOR =
            FunctionDescriptor.of(JAVA_INT, ADDRESS);

	private final SymbolLookup symbolLookup;

    // SymbolLookup is a Panama API, but I have an implementation I'm injecting
	ManualFfi(SymbolLookup symbolLookup) {
		this.symbolLookup = symbolLookup;
	}

	@Override
	public void run() throws Throwable {
		var symbolName = "printf";
		var nativeLinker = Linker.nativeLinker();
		var methodHandle = this.symbolLookup.find(symbolName)
			.map(symbolSegment -> nativeLinker.downcallHandle(symbolSegment, PRINTF_FUNCTION_DESCRIPTOR))
			.orElse(null);
		try (var arena = Arena.ofConfined()) {
			var cString = arena.allocateFrom("hello, Panama!");
			Objects.requireNonNull(methodHandle).invoke(cString);
		}
	}

}

这是我整理的 SymbolLookup 的定义。它是一种复合形式,尝试第一个 SymbolLookup,如果失败再尝试另一个。


@Bean
SymbolLookup symbolLookup() {
    var loaderLookup = SymbolLookup.loaderLookup();
    var stdlibLookup = Linker.nativeLinker().defaultLookup();
    return name -> loaderLookup.find(name).or(() -> stdlibLookup.find(name));
}

运行这段代码,你会看到它打印出 hello, Panama!

你可能想知道为什么我没有选择一个更有趣的例子。事实证明,跨所有操作系统都能理所当然地存在,并且 能感知到在你的计算机上确实做了事情的东西非常少。我想到的几乎只有 IO 了,而控制台 IO 更容易理解。

但 GraalVM native image 呢?它并支持你可能想做的所有事情。而且,至少目前,它不在 Apple Silicon 上运行,只在 x86 芯片上运行。我开发了这个例子,并设置了一个 GitHub Action,以在 x86 Linux 环境中查看结果。这对我们这些不使用 Intel 芯片的 Mac 开发者来说有点可惜,但大多数人在生产环境中不会部署到 Apple 设备上,我们部署到 Linux 和 x86 上,所以这不是一个决定性的障碍。

也存在一些其他限制。例如,GraalVM native image 只支持我们复合查找中的第一个 SymbolLookup,即 loaderLookup。如果那个不起作用,那么两个都不会起作用。

GraalVM 需要知道你在运行时将要执行的一些动态操作,包括 foreign function 调用。你需要提前告诉它。对于它需要这类信息的大多数其他事情,比如反射、序列化、资源加载等等,你需要编写一个 .json 配置文件(或者让 Spring 的 AOT 引擎为你编写)。这项特性非常新,你需要降低几个抽象级别,编写一个 GraalVM Feature 类。一个 Feature 包含在 GraalVM native 编译生命周期中会被调用的回调方法。你会告诉 GraalVM native 函数的签名,也就是它的形式,我们最终会在运行时调用它。这是 Feature。只有一行有价值的代码。

package com.example.demo;

import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeForeignAccess;

import static com.example.demo.ManualFfi.PRINTF_FUNCTION_DESCRIPTOR;

public class DemoFeature implements Feature {

	@Override
	public void duringSetup(DuringSetupAccess access) {
        // this is the only line that's important. NB: we're sharing 
        // the PRINTF_FUNCTION_DESCRIPTOR from our ManualFfi bean from earlier. 
		RuntimeForeignAccess.registerForDowncall(PRINTF_FUNCTION_DESCRIPTOR);
	}

}

然后我们需要关联这个特性,通过向 GraalVM native image Maven 插件配置传递 --features 属性来告知 GraalVM。我们还需要解锁 foreign API 支持并解锁实验性内容。(我不知道为什么这在 GraalVM native image 中是实验性的,而在 Java 22 本身中已不再是实验性的)。此外,我们需要告诉 GraalVM 允许对所有未命名类型进行 native 访问。所以,总而言之,这是最终的 Maven 插件配置。


<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <version>0.10.1</version>
    <configuration>
        <buildArgs>
            <buildArg>--features=com.example.demo.DemoFeature</buildArg>
            <buildArg>--enable-native-access=ALL-UNNAMED</buildArg>
            <buildArg>-H:+ForeignAPISupport</buildArg>
            <buildArg>-H:+UnlockExperimentalVMOptions</buildArg>
            <buildArg>--enable-preview</buildArg>
        </buildArgs>
    </configuration>
</plugin>

这是一个很棒的结果。我将这个例子中的代码编译成了在 GitHub Actions runner 上运行的 GraalVM native image,然后执行了它。这个应用程序——我提醒你——包含了 Spring JDBC 支持,一个完整且嵌入式的、符合 SQL 99 规范的 Java 数据库 H2,以及 classpath 上的所有东西——在 0.031 秒(31 毫秒,即千分之三十一秒)内执行完成,占用几十兆的 RAM,并且从 GraalVM native image 中调用了 native C 代码!

各位,我太高兴了。我等这一天等了很久。

但这确实感觉有点底层。归根结底,你是在使用 Java API 以编程方式在 native code 中创建和维护结构。这有点像通过 JDBC 使用 SQL。JDBC 允许你在 Java 中操作 SQL 数据库记录,但你不是在 Java 中编写 SQL、在 Java 中编译,然后在 SQL 中执行。存在一个抽象差距;你将字符串发送到 SQL 引擎,然后将记录作为 ResultSet 对象取回。Panama 的低级 API 也是如此。它能工作,但你并非直接调用 native code,而是在用字符串查找符号并操作内存。

所以,他们发布了一个独立但相关的工具,名为 jextract。你可以将它指向一个 C 头文件,比如 stdio.h,其中定义了 printf 函数,它将生成模拟底层 C 代码调用签名的 Java 代码。我没有在这个例子中使用它,因为生成的 Java 代码最终会绑定到底层平台。我将它指向 stdio.h,得到了很多 macOS 特定的定义。我可以将所有这些隐藏在操作系统的运行时检查之后,然后动态加载特定的实现,但是,嗯,这篇博客已经太长了。如果你想看如何运行 jextract,这是我使用的、适用于 macOS 和 Linux 的 bash 脚本。你的结果可能有所不同。

#!/usr/bin/env bash
LINUX=https://download.java.net/java/early_access/jextract/22/3/openjdk-22-jextract+3-13_linux-x64_bin.tar.gz
MACOS=https://download.java.net/java/early_access/jextract/22/3/openjdk-22-jextract+3-13_macos-x64_bin.tar.gz

OS=$(uname)

DL=""
STDIO=""

if [ "$OS" = "Darwin" ]; then
    DL="$MACOS"
    STDIO=/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/include/stdio.h
elif [ "$OS" = "Linux" ]; then
    DL=$LINUX
    STDIO=/usr/include/stdio.h
else
    echo "Are you running on Windows? This might work inside the Windows Subsystem for Linux, but I haven't tried it yet.."
fi

LOCAL_TGZ=tmp/jextract.tgz
REMOTE_TGZ=$DL
JEXTRACT_HOME=jextract-22

mkdir -p "$(

 dirname  $LOCAL_TGZ )"
wget -O $LOCAL_TGZ $REMOTE_TGZ
tar -zxf "$LOCAL_TGZ" -C .
export PATH=$PATH:$JEXTRACT_HOME/bin

jextract  --output src/main/java  -t com.example.stdio $STDIO

想想看。我们有了简便的 foreign function 互操作性,虚拟线程带来了惊人的可伸缩性,以及静态链接、闪电般快速、节省 RAM、自包含的 GraalVM native image 二进制文件。再告诉我一次,你为什么还要用 Go 启动新项目?:-)

一个崭新的世界

Java 22 是一个令人惊叹的新版本。它带来了一系列巨大的特性和使用体验改进。但请记住,不可能总是这么好!没有人能每六个月持续不断地引入改变范式的新特性。这根本不可能。所以,让我们心怀感激,尽情享受吧,好吗? :) 上一个版本 Java 21,在我看来,也许是我见过自 Java 5 以来,甚至可能更早以来的最大单次发布。它可能是有史以来最大的发布!

其中有许多特性非常值得你关注,包括面向数据编程虚拟线程

我在六个月前写的一篇支持 Java 21 发布的博客文章中,介绍过这些以及更多内容,文章标题是 你好,Java 21

虚拟线程、结构化并发和作用域值

不过,虚拟线程才是真正重要的部分。请阅读我刚才链接给你的那篇博客,往文章底部看。(别像 the Primeagen 那样,他读了文章,却在还没看到最好的部分——虚拟线程——之前就跳过了!我的朋友... 为什么??)

如果你正在运行 IO 密集型服务,虚拟线程是一种从你的云基础设施支出、硬件等方面榨取更多性能的方式。它们让你能够利用针对 java.io 中阻塞 IO API 编写的现有代码,切换到虚拟线程,并处理更好的规模。通常的效果是,你的系统不再持续等待线程可用,从而平均响应时间下降,更棒的是,你会看到系统同时处理更多请求!我怎么强调都不为过。虚拟线程太棒了!如果你使用 Spring Boot 3.2,只需要指定 spring.threads.virtual.enabled=true 就能从中受益!

虚拟线程是新特性集合的一部分,这些特性历时超过五年,旨在让 Java 成为我们都知道它应有的精简、强大的可伸缩机器。而且它正在发挥作用!虚拟线程是三个旨在协同工作的特性之一。虚拟线程是迄今为止唯一一个以发布形式提供的特性。

结构化并发和作用域值都尚未最终确定(此处指未最终发布)。结构化并发为构建并发代码提供了一种更优雅的编程模型,而作用域值则为 ThreadLocal<T> 提供了一种高效且更通用的替代方案,这在虚拟线程的上下文中特别有用,因为现在你实际可以拥有数百万个线程。想象一下,如果每个线程都有一份重复的数据!

这些特性在 Java 22 中处于预览阶段。我觉得目前还不值得展示它们。在我看来,虚拟线程才是神奇的部分,它们之所以如此神奇,恰恰在于你根本不需要了解它们!只需设置那一个属性,就可以开始使用了。

虚拟线程为你带来了类似 Python、Rust、C#、TypeScript、JavaScript 中的 async/await 或 Kotlin 中的 suspend 那样的惊人可伸缩性,但没有使用这些语言特性所需的固有代码冗余和繁忙工作。这是为数不多的几次,除了 Go 的实现可能外,Java 的结果就是直接更优。Go 的实现是理想的,但这仅仅因为他们在 1.0 版本中就内置了它。事实上,Java 的实现更值得称赞,恰恰因为它与旧的平台线程模型共存。

隐式声明的类和实例 main 方法

这个预览特性极大地提升了开发体验,尽管生成的代码量更少,我还是热烈欢迎它。遗憾的是,目前它还不能很好地与 Spring Boot 一起使用。基本思想是有一天你能够直接拥有一个顶级 main 方法,而无需当今 Java 中固有的所有仪式感。将它作为应用程序的入口点岂不是很好?没有 class 定义,没有 public static void,没有不必要的 String[] args。

void main() {
    System.out.println("Hello, world!");
}

调用 super 之前的语句

这是一个不错的质量提升特性。基本上,Java 不允许你在子类中调用 super 构造函数之前访问 this。目的是为了避免一类与无效状态相关的 bug。但这有点过于严厉,迫使开发者在调用 super 方法之前进行任何非简单计算时,不得不求助于 private static 辅助方法。这是一个有时需要的“体操”示例。我直接从这个 JEP 页面偷来了这个例子

class Sub extends Super {

    Sub(Certificate certificate) {
        super(prepareByteArray(certificate));
    }

    // Auxiliary method
    private static byte[] prepareByteArray(Certificate certificate) {
        var publicKey = certificate.getPublicKey();
        if (publicKey == null)
            throw new IllegalArgumentException("null certificate");
        return switch (publicKey) {
            case RSAKey rsaKey -> ///...
            case DSAPublicKey dsaKey -> ...
            //...
            default -> //...
        };
    }

}

你可以看到问题所在。这项新的 JEP(目前是预览特性)将允许你直接在构造函数中内联该方法,从而提高可读性并减少代码分散。

未命名变量和模式

未命名变量和模式是另一个提升开发体验的特性。然而,这个特性已经正式发布了。

当你创建线程,或使用 Java 8 的 streams 和 collectors 时,你会创建很多 lambda。事实上,Spring 中有很多场景你会用到 lambda。想想所有的 *Template 对象,以及它们以回调为中心的方法。JdbcClientRowMapper<T>,嗯... 也 Spring 地浮现在脑海里!(此处玩了 Spring 的双关语)

有趣的事实:Lambda 最初是在 2014 年的 Java 8 版本中引入的。(是的,那是 十年前 了!人们那时在玩冰桶挑战,世界沉迷于自拍杆、《冰雪奇缘》和《Flappy Bird》。)但它们有一个神奇之处,那就是之前近 20 年的 Java 代码,如果方法期望一个单方法接口的实现,就可以立即参与到 lambda 中。

Lambda 太棒了。它们在 Java 语言中引入了新的复用单位。最好的部分是,它们被设计成能够巧妙地融入现有运行时规则,包括自动将所谓的函数式接口或 SAM(Single Abstract Method,单抽象方法)接口转换为 lambda。我对它们唯一的抱怨是,引用了包含作用域中的变量时,必须将其声明为 final,这很烦人。这个问题后来被解决了。而且必须写出 lambda 的每一个参数,即使我无意使用它,这也很烦人,现在,在 Java 22 中,这个问题也解决了!这里有一个冗长的例子,只是为了演示在两个地方使用 _ 字符。因为我可以这样做。

package com.example.demo;

import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

@Component
class AnonymousLambdaParameters implements LanguageDemonstrationRunner {

	private final JdbcClient db;

	AnonymousLambdaParameters(DataSource db) {
		this.db = JdbcClient.create(db);
	}

	record Customer(Integer id, String name) {
	}

	@Override
	public void run() throws Throwable {
		var allCustomers = this.db.sql("select * from customer ")
                // here! 
			.query((rs, _) -> new Customer(rs.getInt("id"), rs.getString("name")))
			.list();
		System.out.println("all: " + allCustomers);
	}

}

该类使用 Spring 的 JdbcClient 查询底层数据库。它逐页遍历结果,然后调用我们的 lambda,该 lambda 符合 RowMapper<Customer> 类型,用于帮助将结果转换为与我的领域模型一致的记录。我们的 lambda 所符合的 RowMapper<T> 接口有一个单方法 T mapRow(ResultSet rs, int rowNum) throws SQLException,它期望两个参数:ResultSet(我需要它)和 rowNum(我几乎从不需要它)。现在,感谢 Java 22,我无需指定它。只需填入 _,就像 Kotlin 或 TypeScript 中那样。太好了!

Gatherers

Gatherers 是另一个不错的特性,目前也在预览阶段。你可能认识我的朋友 Viktor Klang,他因在 Akka 上的出色工作以及他在 Lightbend 期间对 Scala futures 的贡献而闻名。如今,他是 Oracle 的 Java 语言架构师,他一直在研究的项目之一就是新的 Gatherer API。顺便说一下,Stream API 也是在 Java 8 中引入的——它与 lambda 一起,给了 Java 开发者一个机会,极大地简化和现代化他们现有的代码,并朝着更以函数式编程为中心的方向发展。它对值流上的一系列转换进行建模。但是,这种抽象存在一些不足。Stream API 有许多非常方便的操作符,可以处理 99% 的场景,但当你遇到没有现成操作符的场景时,可能会感到沮丧,因为之前没有简单的方法可以插入自定义操作。在过去的十年里,关于向 Stream API 添加新操作符的提议不计其数,甚至在 lambda 的最初提案中,就已经讨论并妥协了编程模型应具备足够灵活性,以支持引入新的操作符。它终于来了,尽管仍是预览特性。Gatherers 提供了一种稍底层一些的抽象,让你能够在 Streams 上插入各种新操作,而无需在任何时候将 Stream 具化为 Collection。这里有一个我直接、毫无愧色地从Viktor 和团队那里偷来的例子。

package com.example.demo;

import org.springframework.stereotype.Component;

import java.util.Locale;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Gatherer;
import java.util.stream.Stream;

@Component
class Gatherers implements LanguageDemonstrationRunner {

    private static <T, R> Gatherer<T, ?, R> scan(
            Supplier<R> initial,
             BiFunction<? super R, ? super T, ? extends R> scanner) {

        class State {
            R current = initial.get();
        }
        return Gatherer.<T, State, R>ofSequential(State::new,
                Gatherer.Integrator.ofGreedy((state, element, downstream) -> {
                    state.current = scanner.apply(state.current, element);
                    return downstream.push(state.current);
                }));
    }

    @Override
    public void run() {
        var listOfNumberStrings = Stream
                .of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .gather(scan(() -> "", (string, number) -> string + number)
                        .andThen(java.util.stream.Gatherers.mapConcurrent(10, s -> s.toUpperCase(Locale.ROOT)))
                )
                .toList();
        System.out.println(listOfNumberStrings);
    }

}

该代码的主要核心是这里有一个名为 scan 的方法,它返回一个 Gatherer<T,?,R> 的实现。每个 Gatherer<T,O,R> 需要一个初始化器(initializer)和一个整合器(integrator)。它会附带一个默认的组合器(combiner)和一个默认的结束器(finisher),不过你都可以覆盖。这个实现读取所有数字条目,并为每个条目构建一个字符串,然后每个后续字符串都会累积到之前的结果中。结果就是你会得到 1,然后是 12,然后是 123,然后是 1234,等等。

上面的例子表明 gatherer 也是可组合的。我们实际上使用了两个 Gatherer:一个进行扫描,另一个将每个项目映射为大写,而且它并发执行。

还是不太明白?我觉得没关系。我想,这对大多数人来说有点深入了。我们大多数人不需要自己编写 Gatherer。但你可以。事实上,我的朋友 Gunnar Morling 前几天就这么做了。Gatherers 方法的巧妙之处在于,现在社区可以解决自己的需求了。我不知道这对像 Eclipse Collections、Apache Commons Collections 或 Guava 这样优秀的项目意味着什么?他们会包含 Gatherers 吗?还有哪些项目会这样做?我很想看到很多常见的 gatherer 被,嗯,很好地 收集 到一个地方。(此处玩了 Gatherers 的双关语)

类解析 API

又一个非常棒的预览特性,这个 JDK 的新增功能非常适合框架和基础设施开发者。它回答了诸如如何构建 .class 文件,以及如何读取 .class 文件等问题。目前市场上有许多不错的选项,尽管它们不兼容,并且按照定义总是稍微过时一点,比如 ASM(这个领域的大猩猩)、ByteBuddy、CGLIB 等。JDK 自身的代码库中就有三种这样的解决方案!这类库随处可见,对于构建像 Spring 这样在运行时生成类以支持你的业务逻辑的框架开发者来说至关重要。可以把它想象成一种反射 API,但作用于 .class 文件——磁盘上的原始字节码。而不是加载到 JVM 中的对象。

这是一个简单的示例,它将一个 .class 文件加载到 byte[] 数组中,然后进行内省。


package com.example.demo;

import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import java.lang.classfile.ClassFile;
import java.lang.classfile.FieldModel;
import java.lang.classfile.MethodModel;

@Component
@ImportRuntimeHints(ClassParsing.Hints.class)
class ClassParsing implements LanguageDemonstrationRunner {

    static class Hints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.resources().registerResource(DEFAULT_CUSTOMER_SERVICE_CLASS);
        }

    }

    private final byte[] classFileBytes;

    private static final Resource DEFAULT_CUSTOMER_SERVICE_CLASS = new ClassPathResource(
            "/simpleclassfile/DefaultCustomerService.class");

    ClassParsing() throws Exception {
        this.classFileBytes = DEFAULT_CUSTOMER_SERVICE_CLASS.getContentAsByteArray();
    }

    @Override
    public void run() {
        // this is the important logic
        var classModel = ClassFile.of().parse(this.classFileBytes);
        for (var classElement : classModel) {
            switch (classElement) {
                case MethodModel mm -> System.out.printf("Method %s%n", mm.methodName().stringValue());
                case FieldModel fm -> System.out.printf("Field %s%n", fm.fieldName().stringValue());
                default -> {
                    // ... 
                }
            }
        }
    }

}

这个示例之所以稍微复杂一些,是因为我在运行时读取了一个资源,所以我实现了一个 Spring AOT RuntimeHintsRegistrar,它会生成一个 .json 文件,其中包含我正在读取的资源信息,即 DefaultCustomerService.class 文件本身。忽略所有这些。它只是为了 GraalVM native image 编译。

有趣的部分在底部,我们枚举了 ClassElement 实例,然后使用一些模式匹配来提取各个元素。太好了!

字符串模板

作为又一个预览功能,字符串模板为 Java 带来了字符串插值!我们已经支持 Java String 的多行值一段时间了。这项新功能允许语言将作用域内可用的变量插入到编译后的 String 值中。最棒的部分是什么?理论上,这个机制本身是可插拔的!不喜欢这种语法?自己写一个。

package com.example.demo;

import org.springframework.stereotype.Component;

@Component
class StringTemplates implements LanguageDemonstrationRunner {

    @Override
    public void run() throws Throwable {
        var name = "josh";
        System.out.println(STR.""" 
            name: \{name.toUpperCase()}
            """);
    }

}

结论

成为 Java 和 Spring 开发者从未有过如此美好的时刻!我总是这么说。我感觉我们正在获得一个全新的语言和运行时,而且奇迹般地以一种不破坏向后兼容性的方式实现。这是我见过的 Java 社区着手进行的最雄心勃勃的软件项目之一,我们很幸运能在这里收获成果。从现在起,我将在所有事情上使用 Java 22 和支持 Java 22 的 GraalVM,也希望您能这样做。感谢您的阅读,如果您喜欢它,希望您能随时查看我们的 Youtube 频道以及我的 *Spring Tips* 播放列表,在那里我一定会 介绍 Java 22 及更多内容

还要感谢我的朋友兼 GraalVM 特级开发者布道师 Alina Yurenko (@http://twitter.com/alina_yurenko/status/1587102593851052032?s=61&t=ahaeq7OhMUteRPzmYqDtKA) 帮助我核对了其中的一些细节

获取 Spring 新闻通讯

订阅 Spring 新闻通讯,保持联系

订阅

领先一步

VMware 提供培训和认证,助您快速提升。

了解更多

获取支持

Tanzu Spring 通过一个简单的订阅提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

近期活动

查看 Spring 社区的所有近期活动。

查看全部