領先一步
VMware 提供培訓和認證,助您快速提升。
瞭解更多更新 我後來就這個主題釋出了一個 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 --version
、java --version
和 native-image --version
來驗證一切是否正常工作。
如果你在遙遠的未來(我們有飛行汽車了嗎?)閱讀此文,並且看到了 50-graalce
,那麼務必安裝它!版本越高越好!
此時,我想開始構建了!所以,我去了網上我第二喜歡的地方,Spring Initializr - start.spring.io - 並按照以下規範生成了一個新專案
3.3.0-snapshot
版本。3.3 尚未 GA(普遍可用),但應該在幾個月內釋出。與此同時,我們繼續前進!此版本對 Java 22 有更好的支援。Maven
作為構建工具。GraalVM Native Support
支援、H2 Database
和 JDBC 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-plugin
和 maven-compiler-plugin
以支援預覽特性。spring-javaformat-maven-plugin
以支援格式化我的原始碼。org.graalvm.sdk:graal-sdk:23.1.2
和 org.graalvm.nativeimage:svm:23.1.2
,這兩個依賴項僅用於建立我們稍後需要的 GraalVM Feature
實現。<configuration>
部分為 native-maven-plugin
和 spring-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);
}
});
}
// ...
好的,說明完了.. 繼續!
此版本見證了期待已久的 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 的實現更值得稱讚,恰恰因為它與舊的平臺執行緒模型共存。
這個預覽特性極大地提升了開發體驗,儘管生成的程式碼量更少,我還是熱烈歡迎它。遺憾的是,目前它還不能很好地與 Spring Boot 一起使用。基本思想是有一天你能夠直接擁有一個頂級 main 方法,而無需當今 Java 中固有的所有儀式感。將它作為應用程式的入口點豈不是很好?沒有 class
定義,沒有 public static void
,沒有不必要的 String[]
args。
void main() {
System.out.println("Hello, world!");
}
這是一個不錯的質量提升特性。基本上,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
物件,以及它們以回撥為中心的方法。JdbcClient
和 RowMapper<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 是另一個不錯的特性,目前也在預覽階段。你可能認識我的朋友 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 的雙關語)
又一個非常棒的預覽特性,這個 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 及更多內容。