你好,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 原生镜像)。GraalVM 包含了 Java 22 所有新特性的优点,并附带了一些额外的实用工具,所以我总是建议下载它。我特别对 GraalVM 原生镜像功能感兴趣。生成的二进制文件几乎可以即时启动,并且与它们的 JRE 相比,占用的内存要少得多。GraalVM 并非新事物,但值得记住的是,Spring Boot 拥有一个强大的引擎来支持将你的 Spring Boot 应用程序转换为 GraalVM 原生镜像。

安装

这是我所做的。

我正在使用出色的 SDKMAN 包管理器来管理 Java。我还在运行 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 实现。
  • 我在 native-maven-pluginspring-boot-maven-plugin<configuration> 部分添加了配置节。

很快,Spring Boot 3.3 将正式发布并支持 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 原生镜像,已经实现至少六个月了。Project Panama 是让我们能够利用长期以来被拒绝的 C、C++ 代码的广阔世界的工具。说起来,如果它支持 ELF,我想它基本上支持任何种类的二进制文件。例如,Rust 程序和 Go 程序可以编译成 C 兼容的二进制文件,所以我认为(但还没有尝试过)这同样意味着与这些语言的轻松互操作。总的来说,在这一部分,当我谈到“原生代码”时,我指的是可以像调用 C 库一样调用的二进制文件。

历史上,Java 一直非常封闭。Java 开发人员要重新利用原生的 C 和 C++ 代码*不*容易。这很有道理。原生、操作系统特定的代码只会破坏 Java 的 *一次编写,到处运行* 的承诺。这一直有点禁忌。但我认为不应该这样。公平地说,尽管缺乏便捷的原生代码互操作性,我们也做得不错。有 JNI,我确定它代表着 *痛苦地导航炼狱*。为了使用 JNI,你必须编写更多、*新的* C/C++ 代码来连接你想要使用的任何语言与 Java。(这有什么生产力?谁认为这是个好主意?)大多数人*想要*使用 JNI 的程度就像他们*想要*根管治疗一样!

大多数人不喜欢。我们只能用惯用的、Java 风格的方式重新发明一切。对于你可能想做的几乎任何事情,可能都有一个纯 Java 解决方案,它可以运行在任何 Java 运行的地方。它运行得很好,直到它不行。Java 在这里错失了关键机会。想象一下,如果 Kubernetes 是用 Java 构建的?想象一下,如果当前的 AI 革命是由 Java 驱动的?有很多原因说明这些想法在 Numpy、Scipy 和 Kubernetes 最初创建时是不可想象的,但现在呢?今天,他们发布了 Project Panama。

Project Panama 引入了一种轻松链接到原生代码的方法。它有两种级别的支持。你可以通过一种相当底层的方式来操作内存,并在 Java 代码和原生代码之间传递数据。我说“传递”,但我可能应该说“向下”和“向上”调用原生代码。Project Panama 支持“下行调用”(从 Java 调用到原生代码)和“上行调用”(从原生代码调用到 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 原生镜像呢?它并不支持*你可能想做的所有事情*。而且,至少目前为止,它在 Apple Silicon 上无法运行,只能在 x86 芯片上运行。我开发了这个例子,并设置了 GitHub Action 来在 x86 Linux 环境中查看结果。对于我们这些使用非 Intel 芯片的 Mac 开发人员来说,这有点可惜,但我们大多数人实际上并不在生产环境中部署到 Apple 设备,我们部署到 Linux 和 x86,所以这也不是什么交易破坏者。

还有一些其他限制。例如,GraalVM 原生镜像只支持复合中的第一个 SymbolLookup,即 loaderLookup。如果那个不起作用,那么它们都将不起作用。

GraalVM 需要了解你在运行时会做的一些动态事情,包括外部函数调用。你需要提前告诉它。对于它需要这些信息的其他大多数事情,例如反射、序列化、资源加载等,你需要编写一个 .json 配置文件(或者让 Spring 的 AOT 引擎为你编写)。这个功能非常新,你必须深入几个抽象级别,并编写一个 GraalVM Feature 类。Feature 有回调方法,会在 GraalVM 的原生编译生命周期中被调用。你将告诉 GraalVM 我们最终将在运行时调用的原生函数的签名,即*形状*。这是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);
	}

}

然后我们需要将这个 feature 连接起来,通过将 --features 属性传递给 GraalVM 原生镜像 Maven 插件配置来告知 GraalVM。我们还需要解锁外部 API 支持并解锁实验性内容。(我不知道为什么这在 GraalVM 原生镜像中是实验性的,而它在 Java 22 本身中不再是实验性的。)此外,我们需要告诉 GraalVM 允许所有未命名类型的原生访问。所以,总而言之,这是最终的 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 运行器上运行的 GraalVM 原生镜像,然后执行它。该应用程序,我提醒你,它有 Spring JDBC 支持,一个完整且符合 SQL 99 标准的嵌入式 Java 数据库 H2,以及类路径上的所有东西——执行时间为 0.031 秒(31 毫秒,或千分之一秒的 31),占用几 MB 的 RAM,并且从 GraalVM 原生镜像调用了原生 C 代码!

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

但这感觉有点低级。说到底,你正在使用 Java API 来以编程方式创建和维护原生代码中的结构。这有点像使用 JDBC 中的 SQL。JDBC 允许你在 Java 中操作 SQL 数据库记录,但你不是在 Java 中编写 SQL,也不是在 Java 中编译并执行 SQL。这里有一个抽象上的差异;你将字符串发送到 SQL 引擎,然后作为 ResultSet 对象获取记录。低级 Panama API 也是如此。它有效,但你不是在调用原生代码,你是在用字符串查找符号并操作内存。

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

#!/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

想想看。我们拥有便捷的外部函数互操作性、提供惊人扩展性的虚拟线程,以及静态链接、闪电般快速、内存高效、自包含的 GraalVM 原生镜像二进制文件。告诉我,你为什么要再次在新项目中使用 Go?:-)

一个崭新世界

Java 22 是一个令人惊叹的新版本。它带来了大量重要的功能和生活质量的改进。请记住,事情不可能总是这么好!没有人能够每六个月持续推出改变范式的全新功能。这根本不可能。所以,让我们感恩并享受我们所拥有的,好吗?:) 上一个版本,Java 21,在我看来,可能是我自 Java 5 以来,甚至更早,见过的最重大的版本。它可能是迄今为止最大的版本!

其中有大量值得您关注的功能,包括*面向数据编程*和*虚拟线程*。

我覆盖了这一点,以及更多内容,在我六个月前为了支持该版本而写的博客*你好,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 的流和收集器时,你会创建很多 lambda。事实上,在 Spring 中有很多情况下你会用到 lambda。想想所有的 *Template 对象及其以回调为中心的方法。JdbcClientRowMapper<T>,嗯……也浮现在脑海中!

有趣的事实:Lambda 最初是在 2014 年的 Java 8 版本中引入的。(是的,那已经是*十年前*了!人们当时在做冰桶挑战,全世界都痴迷于自拍杆、*冰雪奇缘*和*Flappy Bird*),但它们有一个很棒的特性,就是可以自动适应之前的近 20 年的 Java 代码,只要方法需要一个单一方法接口的实现,就可以参与 lambda。

Lambda 很棒。它们为 Java 语言引入了新的复用单位。最棒的是,它们的设计方式能够自动适应运行时现有的规则,包括自动适应所谓的*函数式接口*或 SAM(单抽象方法)接口到 lambda。我唯一抱怨的是,在 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> 类型,以帮助将我们的结果适配到与我的领域模型匹配的记录。RowMapper<T> 接口,我们的 lambda 符合该接口,有一个单一方法 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 中引入的)让 Java 开发人员有机会与 lambda 一起,极大地简化和现代化他们现有的代码,并朝着更偏向函数式编程的方向发展。它模拟了一系列对值流的转换。但是,抽象中存在一些裂痕。Streams API 有许多非常方便的操作符,适用于 99% 的场景,但当你遇到没有方便操作符的情况时,会感到沮丧,因为没有简单的办法可以插入一个。在过去的十年里,关于向 Streams API 添加新操作符的提案不计其数,甚至在 lambda 的原始提案中也有讨论和让步,认为编程模型应该足够灵活,以支持引入新操作符。它终于到来了,尽管是以预览功能的形式。Gatherers 提供了一个稍微低级别的抽象,让你能够插入各种新的流操作,而无需在任何时候将 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> 都需要一个初始化器和一个积分器。它会附带一个默认的组合器和一个默认的完成器,尽管你可以覆盖两者。这个实现会读取所有这些数字条目,并为每个条目构建一个字符串,然后该字符串会在每次连续的字符串后累加。结果是你得到 1,然后是 12,然后是 123,然后是 1234,依此类推。

上面的例子表明 gatherers 也是可以组合的。我们实际上有两个 Gatherer 在起作用:一个进行扫描,另一个将每个项映射为大写,并且它会并行进行。

还是不太明白?我觉得这没关系。我想这对大多数人来说有点太深入了。我们大多数人不需要编写自己的 Gatherers。但你*可以*。我的朋友 Gunnar Morling 实际上前几天就这么做了。Gatherers 方法的天才之处在于,社区现在可以解决自己的痛点。我想知道这对像 Eclipse Collections、Apache Commons Collections 或 Guava 这样的出色项目意味着什么?它们会发布 Gatherers 吗?还有哪些项目可能会?我希望看到很多常识性的 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 原生镜像编译。

最有趣的部分在底部,我们枚举 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 社区 embarked 上的最大胆的软件项目之一,我们很幸运能够在此收获回报。从现在开始,我将使用 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 社区所有即将举行的活动。

查看所有