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 原生镜像。
简介 {#简介}
我使用的是的 Java SDKMAN 包管理器。在运行 macOS 的苹果 Silicon 芯片上运行。
从 官网 下载安装 GraalVM Community Edition 的预发布版,GraalVM Community 是开源版本。(GraalVM 也有免费的商业版本,但不是开源的)。它允许你通过配置文件引导优化(PGO)等技术构建更快的本地镜像。
解压它,并使用 SDKMAN 命令行工具手动安装,如下所示:
sdk install java 22.07-graalce $HOME/bin/graalvm-jdk-22+36.1/Contents/Home`
然后输入:sdk default java 22.07-graalce
,打开了一个新的 shell。输入 javac --version
和 java --version
以及 native-image --version
来验证一切正常。
不过,当你读到这篇文章时,你可能已经不需要做这些了。SDKMAN 本身就已经发布了。只需执行 sdk list java
,如果看到 22-graalce
(或类似的东西),就安装它:sdk install java 22-graalce
。
创建项目 {#创建项目}
从 Spring Initializr - start.springboot.io 生成一个新的 Spring Boot 项目:
- 选择 Spring Boot 的
3.3.0-snapshot
版本。3.3 还不是正式版,但再过几个月就会正式发布,该版本更好地支持 Java 22。 - 选择
Maven
作为构建工具。 - 添加
GraalVM Native Support
、H2 Database
和JDBC API
依赖。
在 IDE 中打开这个项目,配置一些 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>
看起来有点繁杂,但其实并不多:
- 重新定义了
maven-surefire-plugin
和maven-compiler-plugin
以支持预览功能。 - 添加
spring-javaformat-maven-plugin
来支持源码格式化。 - 添加了两个新的依赖:
org.graalvm.sdk:graal-sdk:23.1.2
和org.graalvm.nativeimage:svm:23.1.2
,这两个依赖专门用于创建我们稍后需要的 GraalVMFeature
实现。 - 在
native-maven-plugin
和spring-boot-maven-plugin
的<configuration>
部分添加了配置选项。
再过不久,Spring Boot 3.3 将支持 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 {#再见-jni}
这次发布的是期待已久的 Project Panama。这是我最期待的三大功能之一。其他两个功能,即虚拟线程和 GraalVM 本地镜像,至少在六个月前就已经实现了。Project Panama 可以让我们能够利用长期以来一直被拒之门外的 C、C++ 代码的丰富资源。仔细想想,如果它支持 ELF,我想它基本上可以支持任何二进制文件。例如,Rust 和 Go 都可以编译成与 C 兼容的二进制文件,所以我认为(但还没试过),这意味着与这些语言的互操作也足够简单。广义地说,在本节中,当我谈到 "本地代码" 时,指的是编译后的二进制文件,它们可以像 C 库一样被调用。
从历史上看,Java 一直相对封闭。Java 开发人员很难重新利用原生的 C 和 C++ 代码。这是有道理的。原生的、特定于操作系统的代码只会削弱Java "一次编写,到处运行" 的承诺。这一直有些禁忌。但我不明白为什么会这样。公平地说,尽管缺乏简单的原生代码互操作性,我们仍然做得不错。有 JNI(Java Native Interface),~~它的缩写应该是"痛苦地穿越地狱(Joylessly Navigating the Inferno)"~~。为了使用 JNI,你得编写更多的、新的 C/C++ 代码,将你想要与 Java 结合使用的任何语言粘合在一起。(这怎么会有成效呢?谁认为这是个好主意?)大多数人想要使用 JNI 就像他们想要做根管手术一样!
大多数人并不这样做。我们只是不得不以符合 Java 风格的方式重新发明一切。对于你可能想做的几乎任何事情,可能已经存在一个纯 Java 的解决方案,在任何 Java 可运行的地方都能运行。这种方法在大多数情况下运行良好,直到遇到问题。Java 在这方面错过了关键的机会。想象一下如果 Kubernetes 是用 Java 构建的?想象一下如果当前的人工智能革命由 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 {
// 这个包是私有的,以后会用到它
static final FunctionDescriptor PRINTF_FUNCTION_DESCRIPTOR =
FunctionDescriptor.of(JAVA_INT, ADDRESS);
private final SymbolLookup symbolLookup;
// SymbolLookup 是一个 Panama API,通过构造函数注入实现。
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) {
// 这是唯一重要的一行
// 注意:共享的是前面 ManualFfi Bean 中的 PRINTF_FUNCTION_DESCRIPTOR。
RuntimeForeignAccess.registerForDowncall(PRINTF_FUNCTION_DESCRIPTOR);
}
}
然后,需要通过在 GraalVM 原生镜像 Maven 插件配置中传递 --features
属性来装配 Feature,并将其告知 GraalVM。 还需要解锁 foreign 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 runners 上的 GraalVM 原生镜像,然后执行了它。这个应用具有 Spring JDBC 支持、一个完整的嵌入式 SQL 99 兼容 Java 数据库 H2 以及 classpath 上的所有内容,执行时间为 0.031 秒(31 毫秒,即千分之 31 秒),占用数十兆内存,并调用了 GraalVM 本地镜像中的本地 C 代码!
但是,这确实感觉有点底层。说到底,你是在使用 Java API 编程式地创建和维护本地代码中的结构。这有点像从 JDBC 中使用 SQL。JDBC 可以让你在 Java 中操作 SQL 数据库记录,但你并不是在 Java 中编写 SQL,在 Java 中编译,在 SQL 中执行。这是一个抽象的脱节;你是将字符串发送到 SQL 引擎,然后以 ResultSet
对象的形式返回记录。Panama 的底层 API 也是如此。它可以工作,但你不是在调用本地代码,而是在用字符串查找符号并操作内存。
因此,他们发布了一款名为 jextract
的独立但相关的工具。 你可以把它指向一个 C 头文件,比如定义了 printf
函数的 stdio.h
,然后它就会生成模仿底层 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
试想一下。
我们有简单的 Foreign 函数互操作,虚拟线程给我们带来了惊人的可扩展性,还有静态链接的、快如闪电的、内存高效的、自包含的 GraalVM 原生镜像二进制文件。再告诉我一次,你为什么要用 Go 来启动一个新项目?
新的世界 {#新的世界}
Java 22 是一个了不起的新版本,带来了大量强大的功能。但是,它不可能总是这么好!没有人能坚持每六个月推出一次改变模式的新功能,这是不可能的。 据我估计,上一次发布的 Java 21 也许是 Java 5(也许更早)以来我见过的最大的一次发布。它可能是有史以来最大的一次!
其中有大量值得关注的功能,包括面向数据编程(data-oriented programming)和虚拟线程(virtual threads)。
虚拟线程、结构化并发和域值 {#虚拟线程结构化并发和域值}
不过,虚拟线程才是真正重要的部分。
虚拟线程是一种在运行 IO 密集型服务时更有效利用云基础设施支出和硬件资源的方式。通过使用虚拟线程,你可以将现有的针对 java.io
中阻塞 IO API 编写的代码切换到虚拟线程,并实现更好的扩展性。通常情况下,这样做的效果是系统不再持续等待线程可用,从而平均响应时间减少,更好的是,你将看到系统同时处理更多的请求!这一点我怎么强调都不为过。虚拟线程非常棒!如果你正在使用 Spring Boot 3.2,你只需设置 spring.threads.virtual.enabled=true
即可享受到虚拟线程的好处!
虚拟线程是一系列新功能中的一部分,这些功能经过了半个多世纪的发展,旨在使 Java 成为我们所期望的高效且具有良好扩展性的编程语言。而且它正在发挥作用!虚拟线程是其中三个设计成协同工作的功能之一。虚拟线程是迄今为止以发布形式交付的唯一功能。
结构化并发(Structured concurrency)和域值(Scoped Value)都尚未登陆。结构化并发为构建并发代码提供了一个更优雅的编程模型,而域值则为 ThreadLocal<T>
提供了一个更高效、更通用的替代方案,在虚拟线程的环境中尤其有用,因为在虚拟线程中现在可以实际拥有数百万个线程。想象一下,每个线程都有重复的数据!
这些功能是 Java 22 的预览版。我不知道它们是否值得展示。在我看来,虚拟线程是最神奇的部分,它们之所以如此神奇,正是因为你并不需要真正了解它们!只要设置一个属性,就可以了。
虚拟线程为你提供了类似于 Python、Rust、C#、TypeScript、JavaScript 中的 async
/ await
,或者 Kotlin 中的 suspend
的惊人扩展性,但却不需要使用这些语言特性时所带来的冗长代码和繁琐操作。这是少数几次(也许除了 Go 的实现之外),Java 在结果上明显更好的情况。尽管 Go 的实现是理想的,但这仅仅是因为他们在 1.0 版本中就已经将其内置。事实上,Java 的实现更加引人注目,正是因为它与旧的平台线程模型共存。
隐式声明的 main 方法类 {#隐式声明的-main-方法类}
代码量减少了,不需要 class
定义,没有 public static void
,也没有不必要的 String[]
。
不幸的是,目前它与 Spring Boot 并不完全兼容。基本思想是,有一天你将能够只需具有顶层的 main
方法,作为应用的入口点,
void main() {
System.out.println("Hello, world!");
}
Super 调用前的执行语句 {#super-调用前的执行语句}
基本上,Java 在子类中在调用 super
构造函数之前不允许访问 this
关键字。这样的目标是为了避免与无效状态相关的一类错误。但这种限制有点过于严格,强迫开发人员在调用 super
方法之前使用私有 private static
辅助方法来进行任何繁琐的计算。下面的例子演示了这种技巧。这个例子是我从 JEP 页面上偷来的:
class Sub extends Super {
Sub(Certificate certificate) {
super(prepareByteArray(certificate));
}
// 辅助方法
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 Stream
和 Collector
时,你将创建大量 Lambda。事实上,在 Spring 中,有很多情况下你都会使用 Lambda。想想所有的 *Template
对象及其以回调为中心的方法就知道了。(JdbcClient
和 RowMapper<T>
也是如此)!
有趣的事实:Lambda 是在 2014 年发布的 Java 8 中首次引入的。(是的,那是十年前),但是,它们具有令人惊叹的特性,即几乎在一夜之间,之前近 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 ")
// 这!
.query((rs, _) -> new Customer(rs.getInt("id"), rs.getString("name")))
.list();
System.out.println("all: " + allCustomers);
}
}
该类使用 Spring 的 JdbcClient
查询底层数据库。它将结果逐一翻页,然后涉及我们的 Lambda,该 Lambda 符合 RowMapper<Customer>
类型,有助于将结果调整为符合我的 Domain 模型的 Record 。RowMapper<T>
接口(符合 Lambda)有一个方法 T mapRow(ResultSet rs, int rowNum) throws SQLException
,该方法需要两个参数:ResultSet
(我需要)和 rowNum
(我几乎永远不需要)。现在,多亏了 Java 22,我不需要指定这两个参数了。只需插入 _
,就像在 Kotlin 或 TypeScript 中一样。不错!
Gatherer API {#gatherer-api}
Gatherer 是另一个很好的功能,也是处于预览阶段。你可能对 Viktor Klang 有所了解,他在 Akka 方面做出了令人惊叹的工作,并在担任 Lightbend 公司期间对 Scala futures 做出了贡献。如今,他是 Oracle 的 Java 语言架构师之一,他一直在致力于新的 Gatherer API。顺便提一下,Stream API 也是在 Java 8 中引入的,它与 Lambda 表达式一起为 Java 开发人员提供了一个机会,大大简化和现代化他们现有的代码,并朝着更加面向函数式编程的方向发展。它模拟了 Stream 进行一系列转换的过程。但是,这个抽象中存在一些缺陷。Stream API 有许多非常方便的操作符,适用于 99% 的场景,但当你发现没有一个方便的操作符适用于某个特定场景时,可能会感到沮丧,因为没有简单的方法来插入一个新的操作符。在过去的十年中,有无数关于在 Stream API 中添加新操作符的建议,甚至在最初的 Lambda 提案中也进行了讨论和妥协,以使编程模型足够灵活,支持引入新的操作符。虽然作为预览功能,但它终于来了。Gatherer
提供了一个稍微低级一些的抽象,使你能够在Stream上插入各种新操作,而无需在任何时候将 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
。但你可以。事实上,我想知道这对 Eclipse Collections、Apache Commons Collections 或 Guava 这样的优秀项目意味着什么?它们会推出 Gatherer
吗?还有哪些其他项目可能?
类解析 API {#类解析-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() {
// 这才是重要的逻辑
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 社区开始的最具雄心的软件项目之一,我们很幸运能够参与其中并获得回报。从现在开始,我将在所有项目中使用 Java 22 和支持 Java 22 的 GraalVM,希望你也能这样做。
感谢你的阅读。
Ref:https://spring.io/blog/2024/03/19/hello-java-22