領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多更新 我最近釋出了一個關於這個主題的 *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 --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 實現。native-maven-plugin 和 spring-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);
}
});
}
// ...
好的,建立在這一點上……繼續前進!
此次釋出見證了 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 的實現之所以更值得稱道,正是因為它與舊的平臺執行緒模型並存。
這個預覽功能是巨大的生活質量提升,即使生成的程式碼更小,我也非常歡迎它。不幸的是,目前它與 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 的流和收集器時,你會建立很多 lambda。事實上,在 Spring 中有很多情況下你會用到 lambda。想想所有的 *Template 物件及其以回撥為中心的方法。JdbcClient 和 RowMapper<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 是另一個不錯的預覽功能。你可能認識我的朋友 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,嗯,*聚合*到一個地方。
又一個非常不錯的預覽功能,這個新的 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 以及更多內容。