你好,Java 22!

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

更新 我已经发布了一个关于此主题的Spring Tips视频!如果您愿意,可以观看它。

嗨,Spring 粉丝们!祝那些庆祝的人 Java 22 发布日快乐!您已经获得了版本了吗?快去快去!Java 22 是一个重大改进,我认为这对每个人来说都是一次值得的升级。它有一些重大的最终发布功能,例如 Panama 项目,以及一系列更好的预览功能。我无法涵盖所有内容,但我确实想谈谈我的一些最喜欢的功能。我们将触及许多功能。如果您想在家里跟着一起学习,代码在这里 (https://github.com/spring-tips/java22)

我喜欢 Java 22,当然,我也喜欢 GraalVM,而且今天两者都发布了!Java 当然是我们最喜欢的运行时和语言,而 GraalVM 是一种高性能的 JDK 发行版,支持其他语言并允许提前 (AOT) 编译(它们被称为 GraalVM 本机镜像)。GraalVM 包含新 Java 22 版本的所有优点,以及一些额外的实用程序,所以我总是建议直接下载它。我特别感兴趣的是 GraalVM 本机镜像功能。与它们的 JRE 同类相比,生成的二进制文件启动速度几乎是瞬间的,并且占用更少的 RAM。GraalVM 并不新鲜,但值得记住的是,Spring Boot 有一个很棒的引擎来支持将您的 Spring Boot 应用程序转换为 GraalVM 本机镜像。

安装

以下是我所做的。

我正在使用很棒的 SDKMAN Java 包管理器。我还在运行 macOS 的 Apple 硅芯片上。这一点,以及我喜欢并鼓励使用 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 将会 GA 并支持 Java 22,因此也许此构建文件的一半将消失。(真是春季大扫除!)

快速编程说明

在本文中,我将引用一个名为 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!

此版本见证了 Panama 项目 的期待已久的发布。这是我期待已久的三个功能之一。另外两个功能,虚拟线程和 GraalVM 本机镜像,至少在六个月前就已经成为现实了。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 首次创建时,这两个想法之所以无法想象有很多原因,但今天?今天,他们发布了 Panama 项目。

Panama 项目引入了一种链接到本地代码的简单方法。有两种级别的支持。您可以以一种相当底层的方式操作内存并将数据来回传递到本地代码中。我说的是“来回”,但我可能应该说“向下和向上”到本地代码。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 本机镜像呢?它不支持您可能想要做的所有事情。而且,至少目前,它仅在 x86 芯片上运行,而不是在 Apple 硅芯片上运行。我开发了这个示例并设置了一个 GitHub Action 以在 x86 Linux 环境中查看结果。对于我们不使用英特尔芯片的 Mac 开发人员来说,这有点可惜,但我们大多数人并没有在生产环境中部署到 Apple 设备,而是部署到 Linux 和 x86,因此这不是一个障碍。

还有一些其他的 限制。例如,GraalVM 本机镜像仅支持我们复合中的第一个 SymbolLookuploaderLookup。如果第一个不起作用,那么这两个都不会起作用。

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);
	}

}

然后我们需要连接该功能,将其告知GraalVM,方法是将--features属性传递给GraalVM原生镜像Maven插件配置。我们还需要解锁外部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千分之一秒)内执行,占用数十兆字节的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,以下是我在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

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

一个崭新的世界

Java 22是一个令人惊叹的新版本。它带来了一系列巨大的功能和生活质量改进。请记住,它不可能一直都这么好!没有人能够每六个月都持续引入改变范式的全新功能。这根本不可能。所以,让我们心怀感激,趁现在还能享受就享受吧,好吗?:) 上一个版本,Java 21,在我看来,可能是自Java 5以来,甚至更早,我见过的最大的一次版本发布。它可能是迄今为止最大的!

那里有很多功能值得你关注,包括面向数据的编程虚拟线程

我在六个月前发布的一篇博客中介绍了这一点以及更多内容,Hello, Java 21

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

不过,虚拟线程才是真正重要的部分。阅读我刚刚链接给你的博客,在底部。(不要像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。其目标是避免一类与无效状态相关的错误。但它有点过于严格,并迫使开发人员在希望在调用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对象及其以回调为中心的

有趣的事实: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 表达式,该表达式符合RowMapper<Customer>类型,以帮助将我们的结果适配到与我的领域模型一致的记录中。RowMapper<T>接口(我们的 Lambda 表达式符合该接口)具有一个单一方法T mapRow(ResultSet rs, int rowNum) throws SQLException,该方法期望两个参数:ResultSet(我需要它)和rowNum(我几乎永远不需要它)。现在,感谢 Java 22,我不需要指定它。只需插入_,就像在 Kotlin 或 TypeScript 中一样。不错!

收集器

收集器是另一个不错的功能,它也处于预览阶段。你可能认识我的朋友Viktor Klang,他因在Akka上的出色工作以及在 Lightbend 工作期间对 Scala Futures 的贡献而闻名。如今,他是 Oracle 的 Java 语言架构师,他一直在从事的一项工作就是新的收集器 API。顺便说一句,Stream API 也在 Java 8 中引入 - 与 Lambda 表达式一起,让 Java 开发人员有机会极大地简化和现代化其现有代码,并朝着更以函数式编程为中心的的方向发展。它模拟了一组对值流的转换。但是,抽象中存在缺陷。Streams API 有许多非常方便的操作符,适用于 99% 的场景,但是当你发现某些操作符不存在的场景时,可能会令人沮丧,因为没有简单的方法可以插入一个操作符。在过去的十年中,针对 Streams API 的新操作符添加提出了无数的建议,并且在 Lambda 表达式的原始提案中甚至进行了讨论和让步,即编程模型应足够灵活以支持引入新的操作符。它终于来了,尽管是一个预览功能。收集器提供了一个稍微更底层的抽象,使你能够在 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>都期望一个初始化器和一个集成器。它将带有一个默认的组合器和一个默认的完成器,尽管你可以覆盖两者。此实现读取所有这些数字条目,并为每个条目构建一个字符串,然后在每个后续字符串之后累积。结果是,你得到1,然后是12,然后是123,然后是1234,依此类推。

上面的示例演示了收集器也是可组合的。我们实际上使用了两个Gatherer:一个执行扫描,另一个将每个项目映射到大写,并且它并行执行。

仍然不太理解?我感觉没关系。这对大多数人来说有点深奥,我想。我们大多数人不需要编写自己的收集器。但是你可以。事实上,我的朋友Gunnar Morling前几天就做了这件事。收集器方法的巧妙之处在于,现在社区可以自己解决问题。我想知道这对 Eclipse Collections、Apache Commons Collections 或 Guava 等很棒的项目意味着什么?它们会提供收集器吗?还有哪些其他项目可能会提供?我希望看到很多通用的收集器,呃,好吧,收集到一个地方。

类解析 API

作为另一个非常棒的预览功能,JDK 中的这个新增功能确实针对框架和基础设施人员进行了调整。它回答了诸如如何构建.class文件以及如何读取.class文件之类的问题。目前,市场上充斥着良好但又不兼容且总是(根据定义)略微过时的选项,例如 ASM(该领域中 800 磅的大猩猩)、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,它会生成一个包含有关我正在读取的资源(DefaultCustomerService.class文件本身)的信息的.json文件。忽略所有这些。这只是为了 GraalVM 原生镜像编译。

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

字符串模板

另一个预览功能,字符串模板将字符串插值引入 Java!我们已经有一段时间可以使用多行 JavaString值了。这个新功能允许语言在编译后的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 和 GraalVM 支持 Java 22 来完成所有事情,我希望你也会这样做。感谢你的阅读,我希望如果你喜欢它,你会随时查看我们的 Youtube 频道和我的Spring 提示播放列表,我肯定会在那里涵盖 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 社区中所有即将举行的活动。

查看全部