當Java 22遇到 SpringBoot 3.3.0!

公众号-JavaEdge發表於2024-03-23

工程 | JOSH LONG | 0條評論

Java 22釋出快樂!

Java 22 是一個重大的進步,是一個值得升級版本。有一些重大的最終釋出功能,如 Project Panama及一系列更優秀的預覽功能。我不可能覆蓋它們全部,但我確實想談談我最喜愛的一些。我們將會涉及到許多功能。如果你想在家裡跟著做,程式碼在這

我愛Java 22,當然,我也愛 GraalVM,它們都在釋出了新版本!Java 當然是我們最喜愛的執行時和語言,而 GraalVM 是一個高效能的 JDK 發行版,它支援更多語言並允許提前編譯(它們被稱為 GraalVM native images)。GraalVM 包含了 Java 22 新版的所有好東西,還有一些額外的工具,所以我總是推薦下載那個版本。我特別感興趣的是 GraalVM native image 的能力。生成的二進位制檔案幾乎可以立即啟動,並且與它們的 JRE 相比,消耗的 RAM 明顯少。GraalVM 不是新事物,但值得記住的是,Spring Boot 有一個很棒的引擎,支援將你的 Spring Boot 應用程式轉化為 GraalVM native images。

1 安裝

我正在使用一個出色的 Java 包管理器 SDKMAN。我還在執行帶有 macOS 的 Apple Silicon 晶片。所以,這個事實和我喜歡並鼓勵使用 GraalVM 的事實稍後會有些重要,所以不要忘了。將會有測試!

sdk install java 22-graalce

我還會設定它為你的預設選擇:

sdk default java 22-graalce

在繼續之前,開啟一個新的 shell,然後透過執行 javac --versionjava --version,和 native-image --version 來驗證一切是否正常。

如果你是在遙遠的未來閱讀這篇文章的(我們已經有飛行汽車了嗎?)而且有 50-graalce,那麼就盡情安裝那個版本!版本越新越好!

2 你總得從某處開始...

在這一點上,我想要開始構建了!所以,我去了網際網路上第二喜歡的地方,Spring Initializr - start.spring.io - 並生成了一個新的專案,使用以下規格:

  • 我選擇了 3.3.0-snapshot 版本的 Spring Boot。3.3 還沒有正式發行,但應該在短短几個月內就會。與此同時,不斷前進!這個版本對 Java 22 有更好的支援。
  • 我選擇了 Maven 作為構建工具。
  • 我新增了 GraalVM Native Support 支援,H2 Database,和 JDBC API 支援。

我在我的 IDE 中開啟了專案,像這樣:idea pom.xml。現在我需要配置一些 Maven 外掛以支援 Java 22 和一些我們將在本文中看到的預覽功能。這是我的完整配置的 pom.xml。它有點密集,所以我會在程式碼結束後來介紹一下。

COPY<?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>
        </pository>
    </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 將會正式釋出並支援 Java 22,所以可能這個構建檔案的一半會消失。(真是春天的清理!)

3 程式設計快速說明

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

好的,既然我們已經講過了,那就開始吧!

4 再見,JNI!

此版本終於等待了已久的 Project Panama 的釋出。我最期待的三個特性之一,另外兩個特性是:

  • 虛擬執行緒
  • GraalVM native images

它們至少已經成為現實六個月了。Project Panama 是讓我們能夠利用長期以來被拒之門外的 C 和 C++ 程式碼的星系。回想起來,如果它支援 ELF,我想象。例如 Rust 和 Go 程式可以編譯成與 C 相容的二進位制檔案,所以我想象(但沒有嘗試過)這意味著與這些語言的互操作也足夠容易。在本節中,當我提到“原生程式碼”時,我指的是以某種方式編譯的二進位制檔案,它們可以像 C 庫那樣被呼叫。

從歷史上看,Java 一直是孤立的。對於 Java 開發人員來說,重新使用原生 C 和 C++ 程式碼並不容易。這是有道理的。原生、特定於作業系統的程式碼只會破壞 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 {

    // 這是包私有的,因為我們稍後會需要它
    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!.

您可能想知道為什麼我沒有選擇更有趣的例子。事實證明,在所有os中你既能理所當然地享有,在計算機上也能感知到自己做了些什麼的東西幾乎沒有。IO 似乎是我能想到的所有東西,而且控制檯 IO 更容易理解。

但 GraalVM 原生映象咋樣呢?它並不支援你可能想做的每件事。至少目前,它不在蘋果晶片執行,只在 x86 晶片。我開發了這個例子,並設定了 GitHub 操作在 x86 Linux 環境中檢視結果。對於我們這些不使用英特爾晶片的 Mac 開發者來說,這有點遺憾,但我們大多數人不是將產品部署到蘋果裝置上,我們是部署到 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);
    }

}

然後我們需要連線所有的特性,透過將 --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 千分之一秒)內執行,佔用數十兆位元組的 RAM,並從 GraalVM 原生映象呼叫原生 C 程式碼!

我真的很高興,大家。我已經等這一天很久了。

但這確實感覺有點低階。歸根到底,你在使用一個 Java API 來以程式設計方式建立和維護原生程式碼中的結構。這有點像使用 JDBC 中的 SQL。JDBC 允許你在 Java 中操縱 SQL 資料庫記錄,但你不是在 Java 中編寫 SQL 並在 Java 中編譯它並在 SQL 中執行它。存在一個抽象增量;你將字串傳送到 SQL 引擎,然後以 ResultSet 物件的形式獲取回來的記錄。Panama 中的低階 API 也是如此。它起作用,但你沒有呼叫原生程式碼,你正在查詢符號和操縱記憶體。

所以,他們釋出了一個與之分離但相關的工具叫做 jextract。你可以指向一個 C 標頭檔案,如 stdio.hprintf 函式定義在其中,它會生成模仿底層 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 專案?😃

5 勇敢的新世界

Java 22 是一個驚人的新版本。它帶來了一系列巨大的功能和提升生活品質的改進。記住,不可能總是這樣美好!沒有人能每六個月就一貫地推出改變遊戲規則的新功能。這是不可能的。所以,我們不妨心存感激,盡情享受目前吧,好嗎? 😃 在我看來,上一個版本 Java 21,或許是我見過的自 Java 5 以來最重要的一次釋出,甚至可能是最早。這可能是最大的一次!

那裡有許多特性值得你關注,包括:

  • 資料導向程式設計
  • 虛擬執行緒

六月前為支援那次釋出所做的部落格中,覆蓋這些及更多內容,Hello, Java 21

6 虛擬執行緒、結構化併發和作用域值

虛擬執行緒是真正重要的部分。閱讀我剛才連結給你的部落格,往下翻。 (不要像 the Primeagen 那樣,他讀了文章但在還沒讀到最佳部分 - 虛擬執行緒之前就走神了!我的朋友……為什麼??)

如果你正在執行 I/O 繫結的服務,虛擬執行緒是提高你的雲基礎設施花費、硬體等的一個方法。它們使得你可以將現有的針對 java.io 中的阻塞 I/O 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 的實現更為傑出,精確地說是因為它與較老的平臺執行緒模型共存。

7 隱式宣告的類和例項主方法

這個預覽功能是巨大的生活質量提升,儘管結果程式碼更小,而我非常歡迎它。不幸的是,它目前還與 Spring Boot 不相容。基本概念是,總有一天你將能夠只有一個頂層 main 方法,而不需要今天 Java 中的所有儀式。作為應用程式的入口點,這不是很好嗎?沒有 class 定義,沒有 public static void,沒有不必要的 String[] 引數。

void main() {
    System.out.println("Hello, world!");
}

8 超類之前的語句

這是一個不錯的生活質量功能。基本上,Java 不允許你在子類中呼叫 super 建構函式之前訪問 this。其目的是為了避免與無效狀態相關的一類錯誤。但這有點過於嚴厲,並迫使開發者在想要在呼叫 super 方法之前進行任何非平凡計算時,不得不轉而使用 private static 輔助方法。這是有時所需的體操動作的一個例子。我從 the 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,目前還是預覽功能,將允許你將該方法直接內聯在建構函式本身,增強可讀性並消除程式碼冗餘。

9 未命名的變數和模式

未命名的變數和模式是另一個提升生活質量的功能。然而,它已經交付了。

當你在建立執行緒,或者使用 Java 8 的流和收集器時,你將建立很多 lambda。實際上,在 Spring 中有很多情況下你會使用 lambdas。只需考慮所有的 *Template 物件,及其以回撥為中心的方法。 JdbcClientRowMapper<T> 也跳入腦海!

有趣的事實:Lambda 首次在 2014 年的 Java 8 版本中介紹。 (是的,已經過去了一個十年!那時人們在做冰桶挑戰,世界痴迷於自拍棒、FrozenFlappy Bird。),但它們的驚人品質是幾乎前 20 年的 Java 程式碼在一夜之間如果方法期望單個方法介面實現就可以參與 lambdas。

Lambdas 是驚人的。它們在 Java 語言中引入了一個新的重用單元。最棒的部分是它們被設計為以某種方式嫁接到執行時的現有規則上,包括自動將所謂的功能介面或 SAMs(單抽象方法)介面適應到 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 ")
                // 這裡! 
            .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 中一樣,只需插入 _ 即可。不錯!

10 聚集者

Gatherers 是另一個在預覽中也很好的功能。 Viktor Klang,他在 Akka 上的了不起工作以及他在 Lightbend 期間對 Scala futures 的貢獻。如今,他是 Oracle 的一名 Java 語言架構師,他一直在研究的就是新的 Gatherer API。順便說一下,Stream API 也是在 Java 8 中引入的,這 - 順便說一下 - 給了 Java 開發者一個機會,與 lambdas 一起,大大簡化和現代化他們現有的程式碼,並向更多函數語言程式設計方向發展。

它構建了一個在值的流上進行一系列轉換的模型。然而,這個抽象模型並不盡完美。Streams API 提供了大量便利的方法,這些方法能夠滿足 99% 的使用場景,但當你遇到找不到合適方法的情況時,通常會感到挫敗,因為之前並沒有一種簡易的方式可以直接擴充套件新的操作。在過去十年間,關於為 Streams API 引入新操作的提案數不勝數,甚至在最初的 lambda 表示式提案中,也有討論和妥協,目的是讓程式設計模型有足夠的靈活性來支援新操作的加入。現在,這一目標作為一個預覽性質的功能終於實現了。Gatherers 提供了一個稍微更底層的抽象層次,使你能夠在不需要將 Stream 具體化為 Collection 的情況下,在 Streams 上引入多種新操作。以下是一個我毫不掩飾地直接從 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 能夠聚集到同一個地方。

11 Class Parsing API

又一個令人期待的預覽性特性,這是 JDK 新增的部分,非常適合框架和基礎架構開發人員。它可以解答例如如何構建 .class 檔案和如何讀取 .class 檔案的問題。目前市場上有很多好用但不相容,總是稍微有點落後的工具,比如 ASM(這個領域裡的重量級解決方案),ByteBuddy,CGLIB 等。JDK 本身在其程式碼庫中就包含了三種此類解決方案!這類庫在整個行業中隨處可見,並且對於像 Spring 這樣的框架的開發來說至關重要,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 檔案。這個 JSON 檔案記錄著我正在讀取的資源資訊,比如具體來說就是 DefaultCustomerService.class 檔案的資料。不過,這些都是幕後的技術細節,主要是為了在 GraalVM 上進行本地映象編譯的時候使用。 而程式碼底部的部分則頗有意思,我們對 ClassElement 例項進行了列舉,並透過模式匹配的方法一一提取了各個要素。這真是太棒了!

12 String Templates

又一項預覽特性的加入,String templates 為 Java 帶來了字串插值功能!Java 中的多行字串(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()}
            """);
    }

}

13 總結

作為一名 Java 和 Spring 開發者,現在是一個前所未有的好時機!我一直強調這一點。我們彷彿獲得了一個嶄新的語言和執行時環境,這一進步 - 奇妙地 - 保持了對歷史版本的相容。這是我目睹 Java 社群所開展的最具雄心壯志的軟體專案之一,我們很幸運能夠見證其成果的誕生。從現在起,我打算將 Java 22 和支援 Java 22 的 GraalVM 用於我的所有開發工作,我希望您也能跟我一起
關注我,緊跟本系列專欄文章,咱們下篇再續!

作者簡介:魔都技術專家兼架構,多家大廠後端一線研發經驗,各大技術社群頭部專家博主。具有豐富的引領團隊經驗,深厚業務架構和解決方案的積累。

負責:

  • 中央/分銷預訂系統效能最佳化
  • 活動&優惠券等營銷中臺建設
  • 交易平臺及資料中臺等架構和開發設計
  • 車聯網核心平臺-物聯網連線平臺、大資料平臺架構設計及最佳化

目前主攻降低軟體複雜性設計、構建高可用系統方向。

參考:

  • 程式設計嚴選網

本文由部落格一文多發平臺 OpenWrite 釋出!

相關文章