當Java 22遇到 SpringBoot 3.3.0!

工程 | 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 發佈!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章