搶先一步
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 images)。 GraalVM 包含新的 Java 22 版本的所有優點,以及一些額外的實用程式,因此我總是建議直接下載該版本。 我特別感興趣的是 GraalVM native image 功能。 與其 JRE 版本相比,產生的二進位檔案幾乎立即啟動,並佔用更少的 RAM。 GraalVM 並非新事物,但值得記住的是,Spring Boot 有一個很棒的引擎來支援將您的 Spring Boot 應用程式轉換為 GraalVM native images。
這是我所做的。
我正在使用很棒的 SDKMAN Java 套件管理器。 我也在 Apple Silicon 晶片上執行 macOS。 這以及我喜歡並鼓勵使用 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 將成為 GA 並支援 Java 22,因此也許此建構檔案的一半將消失。 (談論 Spring 清理!)
在整篇文章中,我將參考一個名為 LanguageDemonstrationRunner
的 functional interface 類型。 這只是一個我建立的 functional interface,它被聲明為會拋出 Throwable
,這樣我就不必擔心它了。
package com.example.demo;
@FunctionalInterface
interface LanguageDemonstrationRunner {
void run() throws Throwable;
}
我有一個 ApplicationRunner
,它反過來會注入我 functional interface 的所有實作,然後調用它們的 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 images,至少已經存在六個月了。 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!
大多數人不會。 我們只是不得不以一種慣用的 Java 風格方式重新發明一切。 對於您可能想要做的幾乎任何事情,都可能存在一個純 Java 解決方案,它可以在 Java 運行的任何地方運行。 它運作良好,直到它不再運作。 Java 錯過了這裡的關鍵機會。 想像一下,如果 Kubernetes 是用 Java 建構的? 想像一下,如果當前的人工智慧革命是由 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 images 呢? 它並不支援您可能想要做的每件事。 而且,至少目前來說,它並不在 Apple Silicon 上運行,僅在 x86 晶片上運行。 我開發了這個例子並設定了 一個 GitHub Action,以在 x86 Linux 環境中查看結果。 對於我們這些未使用 Intel 晶片的 Mac 開發人員來說,這有點可惜,但我們大多數人並未將其部署到生產中的 Apple 設備,而是將其部署到 Linux 和 x86,因此這並不是一個決定性的因素。
還有一些其他的 限制。 例如,GraalVM native images 僅支援我們組合中的第一個 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 native image Maven 插件組態來告訴 GraalVM。我們還需要解鎖 foreign API 的支援並解鎖實驗性功能。(我不明白為什麼這在 GraalVM native image 中是實驗性的,而它在 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 native image,然後執行它。這個應用程式,我提醒您,它有 Spring JDBC 支援、一個完整且嵌入式的 SQL 99 相容 Java 資料庫叫做 H2,以及 classpath 上的所有東西 - 在 0.031 秒(31 毫秒,或 31 千分之一秒)內執行,佔用數十 MB 的 RAM,並從 GraalVM native image 呼叫原生 C 程式碼!
我太高興了,各位。我等這一天等太久了。
但這確實感覺有點低階。歸根究底,您正在使用 Java API 以程式化的方式在原生程式碼中建立和維護結構。這有點像從 JDBC 使用 SQL。JDBC 讓您可以在 Java 中操作 SQL 資料庫記錄,但您不是在 Java 中編寫 SQL 並在 Java 中編譯它並在 SQL 中執行它。有一個抽象差異;您正在將字串發送到 SQL 引擎,然後將記錄作為 ResultSet
物件取回。Panama 中的低階 API 也是如此。它可以運作,但您不是在呼叫原生程式碼,您是在使用字串尋找符號並操作記憶體。
所以,他們發布了一個獨立但相關的工具叫做 jextract
。您可以將它指向一個 C header 檔案,例如定義了 printf
函數的 stdio.h
,它將產生 Java 程式碼,模仿底層 C 程式碼的呼叫簽章。我沒有在這個範例中使用它,因為產生的 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
想一下。我們有簡單的外部函數交互操作,virtual threads 給了我們驚人的可擴展性,以及靜態連結、閃電般快速、RAM 高效率、自我包含的 GraalVM native image 二進位檔。再告訴我一次,你為什麼還要用 Go 開始一個新專案? :-)
Java 22 是一個很棒的新版本。它帶來了大量的功能和生活品質的改進。請記住,它不可能總是這麼好!沒有人可以持續每六個月推出改變典範的新功能。這是不可能的。所以,讓我們心存感激並享受它,好嗎? :) 上一個版本,Java 21,在我看來,可能是我見過的最大的一個版本,也許自 Java 5 以來,甚至更早。它可能是史上最大的!
有很多功能值得您關注,包括資料導向程式設計和virtual threads。
我在六個月前寫了一篇部落格來支持這個版本的發布,Hello, Java 21,其中涵蓋了這一點以及更多內容。
Virtual threads 是真正重要的一點。閱讀我剛才連結給您的部落格,在底部附近。(不要像 the Primeagen 一樣,他讀了這篇文章,但設法在甚至沒有到達最好的部分 - virtual threads 之前就繼續前進了!我的朋友...為什麼??)
如果您正在運行 IO 繫結的服務,virtual threads 是一種榨取更多雲端基礎設施支出、硬體等的方式。它們讓您可以採用針對 java.io
中的阻塞 IO API 編寫的現有程式碼,切換到 virtual threads,並處理更好的規模。通常的效果是您的系統不再不斷等待線程可用,因此平均回應時間會縮短,而且,更好的是,您會看到系統同時處理更多請求!我再怎麼強調都不夠。Virtual threads 太棒了!而且如果您正在使用 Spring Boot 3.2,您只需要指定 spring.threads.virtual.enabled=true
即可從中受益!
Virtual threads 是一系列新功能的一部分,這些功能已經醞釀了五年多,旨在使 Java 成為我們都知道它應該成為的精簡、高效的規模機器。而且它正在運作!到目前為止,Virtual threads 是唯一以發布形式交付的功能。
結構化並行和範圍值都尚未實現。結構化並行為您提供了一個更優雅的程式設計模型來構建並行程式碼,而範圍值為 ThreadLocal<T>
提供了一種高效且更通用的替代方案,特別是在 virtual threads 的上下文中,您現在可以實際擁有數百萬個線程。想像一下為每個線程複製資料!
這些功能在 Java 22 中是預覽版。我不知道它們是否值得展示。在我看來,Virtual threads 是神奇的部分,它們之所以如此神奇,正是因為您實際上不需要了解它們!只需設定一個屬性,您就可以開始了。
Virtual threads 為您提供了像 Python、Rust、C#、TypeScript、JavaScript 中的 async
/await
或 Kotlin 中的 suspend
之類的驚人規模,但沒有使用這些語言功能所需的程式碼固有的冗長和繁忙工作。這是為數不多的,除了也許 Go 的實作之外,Java 在結果上直接更好的情況之一。Go 的實作是理想的,但僅僅是因為他們將其內建到 1.0 版本中。事實上,Java 的實作更值得注意的是,它與舊的 platform threads 模型共存。
這個預覽功能是生活品質上的巨大勝利,即使產生的程式碼更小,我還是熱烈歡迎它。不幸的是,它目前還不能真正與 Spring Boot 搭配使用。基本想法是有一天您將能夠只擁有一個頂層 main 方法,而無需今天 Java 中固有的所有儀式。如果這成為您應用程式的入口點不是很好嗎?沒有 class
定義,沒有 public static void
,沒有不需要的 String[]
args。
void main() {
System.out.println("Hello, world!");
}
這是一個很好的生活品質功能。基本上,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 streams 和 collectors 時,您將建立大量的 lambdas。事實上,在 Spring 中有很多情況您將使用 lambdas。想想所有的 *Template
物件,以及它們以回呼為中心的方法。JdbcClient
和 RowMapper<T>
,呃... spring 也會浮現在腦海中!
有趣的事實:Lambdas 最初是在 2014 年的 Java 8 版本中引入的。(是的,那是十年前的事了!人們在做冰桶挑戰,全世界都沉迷於自拍棒、冰雪奇緣和 Flappy Bird。),但它們具有驚人的品質,即幾乎 20 年的 Java 程式碼可以在一夜之間參與 lambdas,如果方法期望單一方法介面實作的話。
Lambdas 很棒。它們在 Java 語言中引入了一個新的重用單元。最好的部分是,它們的設計方式可以某種程度上嫁接到運行時的現有規則上,包括自動將所謂的函數介面或 SAM (single abstract method) 介面調整為 lambdas。我唯一對它們的抱怨是,必須將 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。順帶一提,Java 8 中引入的 Stream API,讓 Java 開發人員有機會透過 lambda 簡化並現代化他們現有的程式碼,並朝向更以函數式程式設計為中心的方向發展。它模擬了對值串流的一系列轉換。但是,這個抽象概念存在一些缺陷。Streams API 有許多非常方便的運算子,適用於 99% 的情況,但是當您發現某些沒有方便運算子的東西時,可能會令人沮喪,因為沒有簡單的方法可以插入一個。在過去的十年中,有無數個關於向 Streams 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>
都需要一個初始化器和一個整合器。它會帶有一個預設的組合器和一個預設的終結器,但您可以覆寫這兩個。此實作會讀取所有這些數字條目,並為每個條目建立一個字串,然後在每個後續字串之後累積。結果是您會得到 1
,然後是 12
,然後是 123
,然後是 1234
等等。
上面的範例演示了 gatherers 也是可組合的。我們實際上使用了兩個 Gatherer
:一個執行掃描,另一個將每個項目對應到大寫,並且它是同時執行的。
還是不太了解?我感覺這樣也沒關係。我想這對大多數人來說有點太深入了。我們大多數人不需要編寫自己的 Gatherers。但是您可以。我的朋友 Gunnar Morling 前幾天就這樣做了。Gatherers 方法的優點是,現在社群可以解決自己的問題。我想知道這對像 Eclipse Collections、Apache Commons Collections 或 Guava 這樣的優秀專案意味著什麼?他們會發布 Gatherers 嗎?還有哪些其他專案可能會發布?我很想看到許多常識性的 gatherers,嗯,被收集到一個地方。
另一個非常棒的預覽功能,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
,它會產生一個 .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 和 GraalVM 支援 Java 22 於所有事情,我希望您也會這樣做。感謝您的閱讀,如果您喜歡它,請隨時查看我們的 Youtube 頻道和我的Spring Tips播放列表,我一定會涵蓋 Java 22 以及更多內容。