2024 年的 Bootiful Spring Boot (第一部分)

工程 | Josh Long | 2024 年 3 月 11 日 | ...

注意:程式碼位於我的 Github 帳戶:github.com/joshlong/bootiful-spring-boot-2024-blog

嗨,Spring 的粉絲們!我是 Josh Long,我在 Spring 團隊工作。我很高興今年能在 Microsoft 的 JDConf 上發表主題演講。我是一位 Kotlin GDE 和 Java Champion,我認為現在是成為 Java 和 Spring Boot 開發人員的最佳時機。我這麼說完全了解我們今天所處的位置。距離 Spring Framework 的最早版本發布已經超過 21 年,距離 Spring Boot 的最早版本發布也已經超過 11 年。今年是 Spring Framework 發布 20 週年,也是 Spring Boot 發布 10 週年。因此,當我說現在是成為 Java 和 Spring 開發人員的最佳時機時,請記住我已經在這領域待了幾十年了。我熱愛 Java 和 JVM,我也熱愛 Spring,這一切都非常棒。

但現在是最好的時機。以前從未如此接近。所以,讓我們像往常一樣,訪問我在網路上第二喜歡的地方(僅次於生產環境),start.spring.io,來開發一個新的應用程式,我們就會明白我的意思。點擊 Add Dependencies 並選擇 WebSpring Data JDBCOpenAIGraalVM Native SupportDocker ComposePostgresDevtools

給它一個 artifact name。我把我的服務叫做... "service"。我很擅長命名。這點遺傳自我父親。我的父親也很擅長命名。當我還是個小男孩的時候,我們有一隻小白狗,我的父親給牠取名為小白狗。牠是我們家的寵物好幾年。但在大約十年後,牠失蹤了。我不確定牠後來怎麼樣了。也許牠找到了一份工作;我不知道。但後來奇蹟般地,另一隻小白狗出現在我們家紗門前。所以我們收留了牠,我的父親給牠取名為 Too。或者 Two。我不知道。總之,我非常擅長命名。話雖如此,我的媽媽總是告訴我,我很幸運她給我取了這個名字...而且,是的,這可能是真的。

總之,選擇 Java 21。這部分是關鍵。如果你不使用 Java 21,你就不能使用 Java 21。所以,你需要 Java 21。但我們也將使用 GraalVM 的 native image 功能。

還沒有安裝 Java 21 嗎?下載它!使用超棒的 SDKMAN 工具:sdk install java 21-graalce。然後將它設為預設值:sdk default java 21-graalce。打開一個新的 shell。下載 .zip 檔案。

Java 21 非常棒。它比 Java 8 好多了。它在技術上在各方面都更勝一籌。它更快、更穩健、語法更豐富。它在道德上也更優越。當你的孩子們看到你在生產環境中使用 Java 8 時,你不會喜歡他們眼中那種羞愧和恥辱的表情。別這樣做。成為你想要在世界上看到的改變。使用 Java 21。

你會得到一個 zip 檔案。解壓縮並在你的 IDE 中打開它。

我正在使用 IntelliJ IDEA,它安裝了一個叫做 idea 的命令行工具。

cd service
idea build.gradle 
# idea pom.xml if you're using Apache Maven

如果你正在使用 Visual Studio Code,請務必在 *Visual Studio Code Marketplace* 上安裝 Spring Boot Extension Pack

這個新的應用程式將與資料庫進行通訊;它是一個以資料為中心的應用程式。在 *Spring Initializr* 上,我們添加了對 PostgreSQL 的支援,但現在我們需要連接到它。我們最不想要的就是一個冗長的 README.md,其中有一個標題為*開發的一百個簡單步驟*的章節。我們想要過那種*`git clone` & 執行*的生活!

為此,Spring Initializr 產生了一個 Docker Compose compose.yml 檔案,其中包含 Postgres 的定義,這是一個很棒的 SQL 資料庫。

Docker Compose 檔案,compose.yaml

services:
  postgres:
    image: 'postgres:latest'
    environment:
      - 'POSTGRES_DB=mydatabase'
      - 'POSTGRES_PASSWORD=secret'
      - 'POSTGRES_USER=myuser'
    ports:
      - '5432'

更棒的是,Spring Boot 被配置為在 Spring Boot 應用程式啟動時自動執行 Docker Compose (docker compose up) 配置。無需配置像 spring.datasource.urlspring.datasource.password 等連接詳細資訊。這一切都藉由 Spring Boot 超棒的自動配置完成。你一定會喜歡它!而且,為了不留下任何混亂,Spring Boot 也會在應用程式關閉時關閉 Docker 容器。

我們希望盡可能快速地行動。為此,我們在 *Spring Initializr* 上選擇了 DevTools。它可以讓我們快速行動。這裡的核心概念是,重新啟動 Java 非常慢。然而,重新啟動 Spring 卻非常快。那麼,如果我們有一個進程監控我們的專案資料夾,並且可以注意到新編譯的 .class 檔案,將它們載入到 classloader 中,然後建立一個新的 Spring ApplicationContext,丟棄舊的,並給我們一種即時重載的錯覺,那會怎麼樣呢?這正是 Spring 的 DevTools 所做的。在開發期間執行它,你會看到你的重新啟動時間大幅縮短!

之所以有效,是因為,再次強調,Spring 的啟動速度非常快...除了你在每次重新啟動時都啟動一個 PostgreSQL 資料庫的情況。我喜歡 PostgreSQL,但是,嗯,是的,它不是設計成每次你在調整方法名稱、修改 HTTP 端點路徑或微調一些 CSS 時都要不斷重新啟動的。因此,讓我們配置 Spring Boot 簡單地啟動 Docker Compose 檔案,並讓它保持執行,而不是每次都重新啟動。

將屬性添加到 application.properties

spring.docker.compose.lifecycle-management=start_only

我們將從一個簡單的 record 開始。

package com.example.service;

import org.springframework.data.annotation.Id;

// look mom, no Lombok!
record Customer(@Id Integer id, String name) {
}

我喜歡 Java records!你也應該喜歡!不要忽略 records。這個不起眼的 record 不僅僅是一種比 Lombok 的 @Data 註釋更好的方法,它實際上是 Java 21 中達到頂峰的一系列功能的一部分,這些功能共同支援一種叫做資料導向程式設計的東西。

Java 語言架構師 Brian Goetz 在他 2022 年的 InfoQ 文章「資料導向程式設計」 中談到了這一點。

Java 已經主宰了 monolith 的世界,原因在於它強大的存取控制、良好且快速的編譯器、隱私保護等等。Java 可以輕鬆地建立相對模組化、可組合的 monolith 應用程式。Monolith 應用程式通常是龐大、蔓延的程式碼庫,而 Java 支援它。實際上,如果你想要模組化並希望良好地組織你大型的 monolith 程式碼庫,請查看 Spring Modulith 專案。

但情況已經改變。現在,我們在系統中表達變化的方式不是透過抽象類型深度層次的特殊化實作(透過動態派送和多型),而是透過經常透過 HTTP/REST、gRPC、像 Apache Kafka 和 RabbitMQ 這樣的訊息傳遞基底等方式在線路上傳送的 ad-hoc 訊息。重點是資料!

Java 已經發展到支援這些新的模式。讓我們看看四個關鍵功能 - records、模式匹配、智慧 switch 表達式和 sealed types - 來看看我的意思。假設我們在一個受到嚴格監管的行業工作,比如金融。

想像一下我們有一個叫做 Loan 的介面。顯然,貸款是受到嚴格監管的金融工具。我們不希望有人隨便添加一個 Loan 介面的匿名內部類實作,繞過我們辛辛苦苦建立在系統中的驗證和保護。

所以,我們將使用 sealed types。Sealed types 是一種新型的存取控制或可見性修飾詞。

package com.example.service;

sealed interface Loan permits SecuredLoan, UnsecuredLoan {

}

record UnsecuredLoan(float interest) implements Loan {
}

final class SecuredLoan implements Loan {

}

在範例中,我們明確規定系統中有兩種 Loan 的實作:SecuredLoanUnsecuredLoan。Classes 預設是開放子類化的,這違反了 sealed 階層所暗示的保證。所以,我們明確地將 SecuredLoan 設為 finalUnsecuredLoan 被實作為一個 record,並且隱含地是 final。

Records 是 Java 對 tuples 的回答。它們就是 tuples。只不過 Java 是一種名詞語言:事物有名字。這個 tuple 也有一個名字:UnsecuredLoan。如果我們同意它們所暗示的契約,Records 會給我們很大的力量。Records 的核心概念是,物件的 identity 等於欄位的 identity,它們在 record 中被稱為 'components'。所以在這種情況下,record 的 identity 等於 interest 變數的 identity。如果我們同意這一點,那麼編譯器可以給我們一個 constructor、可以給我們每個 component 的儲存空間、可以給我們一個 toString 方法、一個 hashCode 方法和一個 equals 方法。而且它會給我們 constructor 中 component 的 accessors。很棒!而且,它支援 destructuring!語言知道如何提取 record 的狀態。

現在,假設我想要為每種 Loan 顯示一條訊息。我將編寫一個方法。這是最初的天真實作。

 @Deprecated
    String badDisplayMessageFor(Loan loan) {
        var message = "";
        if (loan instanceof SecuredLoan) {
            message = "good job! ";
        }
        if (loan instanceof UnsecuredLoan) {
            var usl = (UnsecuredLoan) loan;
            message = "ouch! that " + usl.interest() + "% interest rate is going to hurt!";
        }
        return message;
    }

這可行,但並沒有充分發揮它的作用。

我們可以清理它。讓我們利用模式匹配,像這樣

 @Deprecated
    String notGreatDisplayMessageFor(Loan loan) {
        var message = "";
        if (loan instanceof SecuredLoan) {
            message = "good job! ";
        }
        if (loan instanceof UnsecuredLoan usl) {
            message = "ouch! that " + usl.interest() + "% interest rate is going to hurt!";
        }
        return message;
    }

更好了。請注意,我們正在使用模式匹配來匹配物件的形狀,然後將明確可轉換的東西提取到一個變數 usl 中。但我們甚至真的不需要 usl 變數,不是嗎?相反,我們想要 dereference interest 變數。所以我們可以改變模式匹配來提取該變數,像這樣。

 @Deprecated
    String notGreatDisplayMessageFor(Loan loan) {
        var message = "";
        if (loan instanceof SecuredLoan) {
            message = "good job! ";
        }
        if (loan instanceof UnsecuredLoan(var interest) ) {
            message = "ouch! that " + interest + "% interest rate is going to hurt!";
        }
        return message;
    }

如果我註解掉其中一個分支會發生什麼?什麼也不會發生!編譯器不在乎。我們沒有處理此程式碼可能通過的其中一個關鍵路徑。

同樣地,我將這個值儲存在一個變數 message 中,並且我正在將它作為某些條件的副作用分配。如果我可以刪除中間值並只返回一些表達式,那不是很好嗎?讓我們看看使用智慧 switch 表達式的更清晰的實作,這是 Java 中的另一個巧妙的新奇事物。

 String displayMessageFor(Loan loan) {
        return switch (loan) {
            case SecuredLoan sl -> "good job! ";
            case UnsecuredLoan(var interest) -> "ouch! that " + interest + "% interest rate is going to hurt!";
        };
    }

這個版本使用智慧 switch 表達式來回傳值和模式比對。如果你註解掉其中一個分支,編譯器會發出警告,因為——感謝密封類型——它知道你沒有窮盡所有可能的選項。太棒了!編譯器為我們做了很多工作!結果更簡潔也更具表現力。大多數時候是這樣。

所以,回到我們的例行程式。為 Spring Data JDBC 儲存庫新增一個介面,以及一個 Spring MVC 控制器類別。然後啟動應用程式。請注意,這需要非常長的時間!那是因為在幕後,它正在使用 Docker daemon 來啟動 PostgreSQL 實例。

但從今以後,我們將使用 Spring Boot 的 DevTools。你只需要重新編譯。如果應用程式正在執行,並且你正在使用 Eclipse 或 Visual Studio Code,你只需要儲存檔案:在 macOS 上使用 CMD+S。IntelliJ IDEA 沒有 Save 選項;在 macOS 上使用 CMD+Shift+F9 強制建置。很好。

好的,我們有一個 HTTP Web 端點在監管一個資料庫,但資料庫中沒有任何東西,所以這肯定會失敗。讓我們用一些 schema 和一些範例資料來初始化我們的資料庫。

新增 schema.sqldata.sql

我們的應用程式的 DDL,schema.sql

create table if not exists customer  (
    id serial primary key ,
    name text not null
) ;

應用程式的一些範例資料,data.sql

delete from customer;
insert into customer(name) values ('Josh') ;
insert into customer(name) values ('Madhura');
insert into customer(name) values ('Jürgen') ;
insert into customer(name) values ('Olga');
insert into customer(name) values ('Stéphane') ;
insert into customer(name) values ('Dr. Syer');
insert into customer(name) values ('Dr. Pollack');
insert into customer(name) values ('Phil');
insert into customer(name) values ('Yuxin');
insert into customer(name) values ('Violetta');

請務必告訴 Spring Boot 在啟動時執行 SQL 檔案,方法是將以下屬性新增至 application.properties

spring.sql.init.mode=always

重新載入應用程式:在 macOS 上使用 CMD+Shift+F9。在我的電腦上,重新載入的時間約為 1/3,或比重新啟動 JVM 和應用程式本身少 66%。太棒了。

它已經啟動並執行。造訪 http://localhost:8080/customers 以查看結果。它成功了!當然,它成功了。這是一個演示。它本來就會成功。

這一切都是非常標準的東西。你十年前就可以做類似的事情。請注意,當時的程式碼會更加冗長。從那時起,Java 已經有了很大的改進。當然,速度也無法比擬。當然,現在的抽象也更好。但你本來可以做類似的事情——一個 Web 應用程式監管一個資料庫。

事物在變化。總是有新的領域。現在,新的領域是人工智慧,或 AI。AI:因為尋找好的舊智慧顯然還不夠困難。

人工智慧是一個巨大的產業,但大多數人想到人工智慧時,指的是利用人工智慧。你不需要使用 Python 來使用大型語言模型 (LLM),就像大多數人不需要使用 C 來使用 SQL 資料庫一樣。你只需要與 LLM 整合,而在這方面,Java 在選擇和能力方面是首屈一指的。

在我們上次的重大 SpringOne 開發者活動(2023 年)中,我們宣布了 Spring AI,一個旨在使整合和使用人工智慧盡可能容易的新專案。

你會想要擷取資料,例如來自帳戶、檔案、服務,甚至是一組 PDF。你會想要將它們儲存在向量資料庫中,以便輕鬆檢索以支援相似性搜尋。然後你會想要與 LLM 整合,並從該向量資料庫為其提供資料。

當然,對於你可能想要的任何 LLM,都有用戶端綁定——Amazon BedrockAzure OpenAIGoogle Vertex Google GeminiOllamaHuggingFace,當然還有 OpenAI 本身,但這僅僅是開始

所有為 LLM 提供支援的知識都已嵌入模型中,然後該模型會告知 LLM 對世界的理解。但是該模型有一個到期日,在此之後其知識就會過時。如果模型是在兩週前建立的,它就不會知道昨天發生的事情。因此,如果你想建立一個自動助理,例如處理使用者關於其銀行帳戶的請求,那麼 LLM 需要了解最新情況。

你可以在提出的請求中新增資訊,並將其用作上下文來告知回應。如果只有這麼簡單,那就不會那麼糟糕。還有另一個問題。不同的 LLM 支援不同的權杖視窗大小。權杖視窗決定了你可以為給定的請求傳送和接收多少資料。視窗越小,你可以傳送的資訊就越少,LLM 在其回應中的資訊也就越少。

你可以在這裡做的一件事是將資料放入向量儲存庫中,例如 pgvectorNeo4jWeaviate 或其他,然後將 LLM 連接到該向量資料庫。向量儲存庫讓你能夠在給定一個或一組詞的情況下,找到與它們相似的其他事物。它將資料儲存為數學表示形式,並讓你查詢相似的事物。

擷取、豐富、分析和消化資料以告知 LLM 回應的整個過程稱為檢索增強生成 (RAG),Spring AI 支援所有這些。有關更多資訊,請參閱我做的這個 Spring Tips 影片(關於 Spring AI)。但是,我們不會在這裡利用所有這些功能。只有一個。

我們在 Spring Initializr 上新增了 OpenAI 支援,因此 Spring AI 已經在類別路徑上。新增一個新的控制器,像這樣

一個由 AI 驅動的 Spring MVC 控制器

package com.example.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.Map;

@Controller
@ResponseBody
class StoryController {

    private final ChatClient singularity;

    StoryController(ChatClient singularity) {
        this.singularity = singularity;
    }

    @GetMapping("/story")
    Map<String, String> story() {
        var prompt = """
                Dear Singularity,

                Please write a story about the good folks of San Francisco, capital of all things Artificial Intelligence,
                and please do so in the style of famed children's author Dr. Seuss.

                Cordially,
                Josh Long
                """;
        var reply = this.singularity.call(prompt);
        return Map.of("message", reply);
    }

}

非常簡單!注入 Spring AI 的 ChatClient,使用它向 LLM 發送請求,取得回應,並將其作為 JSON 回傳給 HTTP 客戶端。

你需要使用屬性 spring.ai.openai.api-key= 配置與 OpenAI API 的連線。我在執行程式之前將其匯出為環境變數 SPRING_AI_OPENAI_API_KEY。我不會在這裡重現我的金鑰。請原諒我沒有洩漏我的 API 憑證。

CMD+Shift+F9 重新載入應用程式,然後造訪端點:http://localhost:8080/story。LLM 可能需要幾秒鐘才能產生回應,所以準備好那杯咖啡、一杯水,或任何東西,以便快速但令人滿意地啜飲一口。

story time ai response
我在瀏覽器中的 JSON 回應,已啟用 JSON 格式化外掛程式

就在那裡!我們生活在奇蹟的時代!奇異點的時代!你現在可以做任何事了。

但它的確花了幾秒鐘,不是嗎?我不吝惜電腦那些時間!它做得非常出色!我無法更快地做到。看看它產生的故事!這是一件藝術品。

但它的確花了一段時間。這對我們的應用程式具有可擴展性的影響。當我們呼叫 LLM 時,幕後發生的事情是我們正在進行網路呼叫。在程式碼的深處,有一個 java.net.Socket,我們從中取得了一個 java.io.InputStream,它代表來自服務的資料的 byte 陣列。我不知道你是否還記得直接使用 InputStream。這是一個範例

    try (var is = new FileInputStream("afile.txt")) {
        var next = -1;
        while ((next = is.read()) != -1) {
            next = is.read();
            // do something with read
        }
    }

看看我們從 InputStream 中讀取位元組的部分,方法是呼叫 InputStream.read 嗎?我們稱其為阻塞操作。如果我們在第四行呼叫 InputStream.read,那麼我們必須等到呼叫回傳,才能到達第五行。

如果我們連接的服務只是回傳了太多的資料呢?如果服務已關閉呢?如果它永遠不會回傳呢?如果我們卡住了,永遠在等待呢?如果呢

如果只發生一次,這很乏味。但如果這可能發生在系統中用於處理 HTTP 請求的每個執行緒上,那麼這對我們的服務來說就是一種生存威脅。這種情況經常發生。這就是為什麼可以登入到一個其他方面沒有回應的 HTTP 服務,並發現 CPU 基本上處於休眠狀態——閒置!——什麼也沒做或幾乎什麼也沒做。執行緒池中的所有執行緒都卡在等待狀態,等待沒有到來的東西。

這是一種對我們為之付費的寶貴 CPU 容量的巨大浪費。即使該方法最終會回傳,最好的情況仍然不好。即使該方法最終會回傳,仍然表示處理該請求的執行緒無法用於系統中的任何其他物件。該方法正在壟斷該執行緒,因此系統中的任何其他人都無法使用它。如果執行緒便宜且豐富,這就不會是問題。但它們不是。在 Java 的大部分生命週期中,每個新的執行緒都與一個作業系統執行緒一對一配對。而且這並不便宜。每個執行緒都有一定的記帳開銷。一到兩 MB。因此你無法建立太多執行緒,而且你還在浪費你所擁有的少數執行緒。太可怕了!誰還需要睡覺?

一定有更好的方法。

你可以使用非阻塞 IO。像引起痔瘡且複雜的 Java NIO 函式庫一樣。這是一個選項,就像與一群臭鼬一起生活是一個選項一樣:它很臭!無論如何,我們大多數人都沒有以非阻塞 IO 或常規 IO 的方式思考。我們生活在抽象階梯的更高層次上。我們可以使用反應式編程。我熱愛反應式編程。我甚至寫了一本書——反應式 Spring。但如果你不習慣像函數式程式設計師一樣思考,就不太清楚如何使其工作。這是一種不同的範例,意味著需要重寫你的程式碼。

如果我們能同時擁有非阻塞的蛋糕,又能吃掉它,那會怎麼樣? 透過 Java 21,現在我們能做到了! 有一個叫做虛擬執行緒的新功能,讓這件事變得超級簡單! 如果你在其中一個新的虛擬執行緒上執行阻塞性的操作,運行時會偵測到你正在做一個阻塞性的事情 - 像是 java.io.InputStream.readjava.io.OutputStream.writejava.lang.Thread.sleep - 並將該阻塞的、閒置的活動從執行緒上移開,放到記憶體中。 然後,它基本上會設定一個睡眠的計時器,或者監控 IO 的檔案描述符,並讓運行時在這段時間內將執行緒重新用於其他事情。 當阻塞性操作完成時,運行時會將它移回執行緒,並讓它從開始的地方繼續,而且幾乎不需要更改你的程式碼。 這很難理解,所以讓我們透過一個範例來看看。 我非常無恥地從 Oracle 開發者倡導者 José Paumard 那裡借用了這個範例。

這個範例示範了建立 1,000 個執行緒,並在每個執行緒上睡眠 400 毫秒,同時記錄這 1,000 個執行緒中的第一個執行緒的名稱。

package com.example.service;

import org.springframework.boot.ApplicationRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.ArrayList;
import java.util.Set;
import java.util.concurrent.ConcurrentSkipListSet;

@Configuration
class Threads {

    private static void run(boolean first, Set<String> names) {
        if (first)
            names.add(Thread.currentThread().toString());
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    }

    @Bean
    ApplicationRunner demo() {
        return args -> {

            // store all 1,000 threads
            var threads = new ArrayList<Thread>();

            // dedupe elements with Set<T>
            var names = new ConcurrentSkipListSet<String>();

            // merci José Paumard d'Oracle
            for (var i = 0; i < 1000; i++) {
                var first = 0 == i;
                threads.add(Thread.ofPlatform().unstarted(() -> run(first, names)));
            }

            for (var t : threads)
                t.start();

            for (var t : threads)
                t.join();

            System.out.println(names);
        };
    }

}

我們使用 Thread.ofPlatform 工廠方法來建立普通的平台執行緒,其本質與自 1990 年代 Java 首次亮相以來我們所建立的執行緒相同。 該程式建立 1,000 個執行緒。 在每個執行緒中,我們睡眠 100 毫秒,總共四次。 在這之間,我們會測試我們是否在 1,000 個執行緒中的第一個執行緒上,如果是,我們會透過將目前執行緒的名稱新增到集合中來記錄它。 集合會將其元素去重; 如果相同的名稱出現多次,集合中仍然只會有一個元素。

執行該程式 (CMD+Shift+F9!),你會看到該程式的物理特性沒有改變。 Set<String> 中只有一個名稱。 為什麼不是這樣呢? 我們只測試了同一個執行緒,一遍又一遍。

現在,更改該建構子以使用虛擬執行緒Thread.ofVirtual。 超級容易更改。 現在執行該程式。 CMD+Shift+F9。

你會看到集合中有一個以上的元素。 你完全沒有更改程式碼的核心邏輯。 實際上,你甚至只需要更改一件事情,但是現在,無縫地,在幕後,編譯器和運行時重寫了你的程式碼,因此當虛擬執行緒上發生阻塞性操作時,運行時會無縫地將你移開,並在阻塞性操作結束後將你放回執行緒上。 這表示你之前存在的執行緒現在可用於系統的其他部分。 你的可擴展性將會突飛猛進!

你可能會抗議,我不想更改我的所有程式碼。 首先,這是一個荒謬的論點,更改非常簡單。 你可能會抗議:我也不想從事建立執行緒的業務。 好問題。 Tony Hoare 在 1970 年代寫道,NULL 是一個價值 10 億美元的錯誤。 他錯了。 事實上,是 PHP。 但是,他也詳細談到了使用執行緒建立系統是多麼站不住腳。 你會想要使用更高階的抽象概念,例如 saga、actor,或者至少是一個 ExecutorService

還有一個新的虛擬執行緒執行器:Executors.newVirtualThreadPerTaskExecutor。 讚! 如果你使用 Spring Boot,可以很容易地覆寫該類型的預設 bean,以在系統的某些部分中使用。 Spring Boot 將會引入它並轉而使用它。 夠容易的。 但是如果你使用 Spring Boot 3.2。 你當然正在使用 Spring Boot 3.2,對吧? 你知道每個版本只支援大約一年,對吧? 請務必查看給定的 Spring 專案的支援策略。 如果你正在使用 3.2,那麼你只需要將一個屬性新增到 application.properties,我們就會為你插入虛擬執行緒支援。

spring.threads.virtual.enabled=true

讚! 無需更改程式碼。 現在,你會看到可擴展性得到很大的提升,並且如果你的服務是 IO 密集型的,則可能能夠縮減負載平衡器中某些執行個體的規模。 我的建議是什麼? 告訴你的老闆你要為公司節省一大筆錢,但堅持要將這筆錢放進你的薪水中。 然後部署此變更。 瞧!

好的,我們正在快速前進。 我們有 git clone & 執行能力。 我們有 Docker compose 支援。 我們有 DevTools。 我們有一種非常好的語言和語法。 我們已經實現了奇異點。 我們正在快速前進。 我們有可擴展性。 將 Spring Boot Actuator 新增到建置中,現在你就有了可觀察性。 我認為現在是時候轉向生產環境了。

我想將這個應用程式打包起來,使其盡可能高效。 在這裡,我的朋友們,我們需要考慮幾個問題。 首先,我們如何容器化該應用程式? 很簡單。 使用 Buildpacks。 很簡單。 請記住,朋友不會讓朋友編寫 Dockerfile。 使用 Buildpacks。 Spring Boot 也開箱即用地支援它們:./gradlew bootBuildImage./mvnw spring-boot:build-image。 但這不是新的東西,所以下一個問題。

我們如何使這東西盡可能高效和最佳化? 在我們深入探討這個問題之前,我的朋友們,重要的是要記住 Java 已經非常非常非常高效。 我喜歡 這篇部落格,來自 2018 年,在 COVID 疫情之前,或者說 BC (Before COVID)。

它研究了哪些語言使用的能量最少,或者說是最節能的。 C 是最節能的。 它使用的電力最少。 1.0。 它是基準線。 它是高效的...​ 對於機器。 不是人! 絕對不是人。

然後我們有 Rust 及其零成本抽象。 做得好。

然後我們有 C++... gross

C++ 真令人厭惡! 繼續...

然後我們有 Ada 語言,但是... 誰在乎?

然後我們有 Java,它接近 2.0。 讓我們四捨五入,說 2.0。 Java 是 C 的兩倍 - 兩倍! - 的效率低。 或者說是 C 的一半效率。

到目前為止還好嗎? 很好。 儘管如此,它仍然是前 5 名最有效率的語言!

如果你向下滾動列表,你會看到一些驚人的數字。 Go 和 C# 大約在 3.0+ 範圍內。 向下滾動到這裡,我們有 JavaScript 和 TypeScript,其中一個 - 令我無比困惑 - 的效率比另一個四倍!

然後我們有 PHP 和 Hack,越少說越好。 繼續!

然後我們有 JRuby 和 Ruby。 朋友們,請記住 JRubyRuby,用 Java 編寫。 而 Ruby 是 Ruby,用 C 編寫。 然而,JRuby 的效率幾乎比 Ruby 高三分之一! 僅僅因為它是用 Java 編寫的,並且在 JVM 上運行。 JVM 是一個了不起的工具。 絕對驚人。

然後... 我們有 Python。 這,嗯,這讓我感到非常難過! 我 Python! 自 1990 年代以來我一直在使用 Python! 當我第一次學習 Python 時,Bill Clinton 是總統! 但是這些數字不是很好。 想一想。 75.88。 讓我們四捨五入到 76。我不擅長數學。 但是你知道什麼擅長數學嗎? 該死的 Python! 讓我們問它。

python doing math

38! 這意味著,如果你在 Java 中運行一個程式,並且運行它所需的能量的產生會產生一些碳,這些碳最終會被困在大氣中,升高溫度,而升高的溫度反過來會殺死一棵樹,那麼 Python 中的等效程式將會殺死三十八棵樹! 那是一片森林! 這比 Bitcoin 更糟糕! 我們需要對此採取一些措施,而且要盡快。 我不知道是什麼,但需要採取一些措施。

無論如何,我想說的是 Java 已經很棒了。 我認為這是因為人們認為理所當然的兩件事:垃圾回收和即時 (JIT) 編譯器。

垃圾回收,嗯,我們都知道它是什麼。 甚至 白宮也在其最近關於保護軟體以保護網路空間基礎的報告中讚賞了像 Java 這樣的垃圾回收、記憶體安全的語言

Java 程式語言垃圾回收器讓我們可以編寫平庸的軟體,並且在某種程度上逃脫懲罰。 它很棒! 也就是說,我確實反對它是原始 Java 垃圾回收器的概念! 這項榮譽屬於其他地方,例如可能 Jesslyn

而 JIT 是另一個了不起的工具。 它會分析你的應用程式中經常存取的程式碼路徑,並將它們轉換為作業系統和架構特定的原生程式碼。 但它只能對你的某些程式碼執行此操作。 它需要知道,當你編譯程式碼時起作用的類型,是在你運行程式碼時唯一會起作用的類型。 而 Java 中的某些東西 - 這是一種非常動態的語言,其運行時更像 JavaScript、Ruby 和 Python - 允許 Java 程式執行會違反此約束的行為。 像是序列化、JNI、反射、資源載入和 JDK 代理。 請記住,使用 Java,可以有一個 String,其內容是一個 Java 原始碼檔案,將該字串編譯成檔案系統上的 .class 檔案,將該 .class 檔案載入到 ClassLoader 中,以反射方式建立該類別的實例,然後 - 如果該類別是一個介面 - 建立該類別的 JDK 代理。 如果該類別實作 java.io.Serializable,則可以透過網路 socket 將該類別實例寫入,並將其載入到另一個 JVM 上。 你可以完成所有這些操作,而無需對任何超出 java.lang.Object 範圍的內容進行明確的類型化引用! 這是一種了不起的語言,這種動態特性使其成為一種非常高效的語言。 它也阻礙了 JIT 嘗試進行最佳化。

儘管如此,JIT 在它可以執行的地方做得非常出色。 結果不言自明。 因此,人們想知道:為什麼我們不能主動地 JIT 整個程式呢? 提前? 我們可以。 有一個名為 GraalVM 的 OpenJDK 發行版本,它有許多優點,可以透過額外的工具(例如 native-image 編譯器)來擴展 OpenJDK 發行版本。 本機映像編譯器很棒。 但是這個本機映像編譯器也有相同的約束。 它無法對非常動態的事物執行其魔法。 這是一個問題。 因為大多數程式碼 - 你的單元測試函式庫、你的 Web 框架、你的 ORM、你的記錄函式庫... 所有東西! - 都會使用一種或所有這些動態行為。

這裡有個逃生艙口。你可以將組態以 .json 檔案的形式,放置在 GraalVM 編譯器可識別的目錄中:src/main/resources/META-INF/native-image/$groupId/$artifactId/\*.json。這些 .json 檔案有兩個問題。

首先,"JSON" 這個詞聽起來很蠢。我不喜歡說 "JAY-SAWN" 這個字。身為一個成年人,我簡直不敢相信我們會這樣跟彼此說話。我會說法語,在法語中,你會把它發音成 jeeesã。所以,.gison。好多了。閩南語有一個詞 - gingsong (歡喜),這也可以用。所以你可以有 .gingsong。選擇你的隊伍吧!無論如何,.json 不應該存在。我是 .gison 隊的,但這並不重要。

第二個問題是,嗯,需要這麼多的配置!再次強調,想想你的程式在哪些地方會做那些有趣的、動態的事情,像是反射、序列化、JDK 代理、資源載入和 JNI!簡直無窮無盡。你的網頁框架。你的測試函式庫。你的資料存取技術。我沒有時間為每個程式撰寫手工打造的組態檔案。我甚至沒有足夠的時間完成這篇部落格文章!

所以,我們將使用在 3.0 中引入的 Spring Boot ahead-of-time (AOT) 引擎。 AOT 引擎會分析你的 Spring 應用程式中的 beans,並為你產生必要的組態檔案。太棒了!甚至還有一個完整的元件模型,你可以使用它來擴展 Spring 到編譯時期。我不會在這裡深入探討所有這些,但你可以閱讀我的免費電子書或觀看我的免費 YouTube 影片,介紹所有關於 Spring 和 AOT 的內容。基本上內容是一樣的,只是消費方式不同。

所以,讓我們用 Gradle 啟動建置,./gradlew nativeCompile,或者如果你使用 Apache Maven,則使用 ./mvnw -Pnative native:compile。你可能想要跳過這次的測試...這次的建置需要一點時間。請記住,它正在分析你的程式碼庫中的所有內容 - 無論是類別路徑上的函式庫、JRE 還是程式碼中的類別 - 以確定它應該保留哪些類型以及應該丟棄哪些類型。結果是一個精簡、高效、快如閃電的執行時期機器,但代價是非常、非常慢的編譯時間。

事實上,它花的時間太長了,有點阻塞了我的流程。它讓我停滯不前,等待著。我就像之前這篇部落格文章中的平台執行緒一樣:被阻塞!我感到無聊。等待。等待。我現在終於理解了這張著名的 XKCD 漫畫

有時候我會開始哼音樂。或主題曲。或電梯音樂。你知道電梯音樂聽起來是什麼樣的,對吧? 不停歇,無止盡。所以,我想,如果每個人都聽到電梯音樂,那不是很好嗎? 所以我問了。我得到了一些很棒的回應。

有人建議,從我們的朋友那裡,我們應該播放任天堂 64 遊戲機的配樂,即皮爾斯·布洛斯南首次飾演詹姆士·龐德的電影黃金眼的電梯音樂。我喜歡它。

adinn elevator music
黃金眼有一些很棒的電梯音樂!

一個回應建議說,發出嗶嗶聲會很有用。非常同意。我那愚蠢的微波爐在完成時會發出 ding! 的聲音。為什麼我的數百萬行程式碼編譯器不能?

ivan beeps
DING!

然後我們得到了來自另一位我最喜歡的醫生,Niephaus 博士的回應,他在 GraalVM 團隊工作。他說,添加電梯音樂只會解決症狀,而不是問題的根本原因,也就是讓 GraalVM 在時間和記憶體方面更加高效。

doctor
niephaus

好的。但他確實分享了這個有前景的原型!

graalvm
prototype

我確信它很快就會被合併了...

總之!如果你檢查編譯,現在應該完成了。它位於 ./build/native/nativeCompile/ 資料夾中,名為 service。在我的機器上,編譯花了 52 秒。唉!

執行它。它會失敗,因為,再次強調,我們正在過著 git clone & 執行的生活方式!我們沒有指定任何連線憑證!所以,請使用環境變數執行它,指定你的 SQL 資料庫連線詳細資訊。這是我在我的機器上使用的腳本。這僅適用於 Unix 風格的作業系統,並且適用於 Maven 或 Gradle。

#!/usr/bin/env bash

export SPRING_DATASOURCE_URL=jdbc:postgresql://localhost/mydatabase
export SPRING_DATASOURCE_PASSWORD=secret
export SPRING_DATASOURCE_USERNAME=myuser

SERVICE=.
MVN=$SERVICE/target/service
GRADLE=$SERVICE/build/native/nativeCompile/service
ls -la $GRADLE && $GRADLE || $MVN

在我的機器上,它在 ~100 毫秒內啟動!像火箭一樣!而且,顯然,如果我使用 Spring Cloud Function 來建置 AWS Lamba 風格的 functions-as-a-service,它會更快,因為我不需要打包一個 HTTP 伺服器。 實際上,如果純粹的啟動速度是真正想要的,那麼我甚至可以使用 Spring 對 Project CRaC 的驚人支援。 那與這裡無關。我真的不太關心這個,因為這是一個獨立的、長期運行的服務。 我關心的是資源使用量,由 Resident Set Size (RSS) 表示。 請注意進程識別碼 (PID) - 它會在日誌中。 如果 PID 是,比如說,55,那麼使用 ps 公用程式(幾乎所有 Unix 上都有)來取得 RSS,如下所示

ps -o rss 55

它將以千位元組為單位輸出一個數字; 除以一千,你就會得到以百萬位元組為單位的數字。 在我的機器上,它僅需 100MB 多一點即可運行。 你無法在這麼小的記憶體中運行 Slack! 我敢打賭,你在 Chrome 中有單獨的瀏覽器標籤佔用這麼多,甚至更多!

所以,我們有一個程式,它盡可能簡潔,同時易於開發和迭代。 並且它使用虛擬執行緒來為我們提供無與倫比的可擴展性。 它作為一個獨立的、自包含的、作業系統和架構特定的原生映像運行。 喔! 而且,它支援該死的奇異點!

我們生活在一個驚人的時代。 從未有過比現在更好的成為 Java 和 Spring 開發人員的時代。 我希望我也說服了你。

資源

我和 Spring 團隊的其他成員將在 Microsoft 的 JDConf 2024 上發表演講!

註冊並參加我的會議 JDConf: Bootiful Spring Boot 3

在 JDConf 上參加的其他 Spring 會議:

如果你喜歡這篇部落格,我希望你訂閱我們的 YouTube 頻道,我在那裡的 Spring Tips 播放列表中每週都會有新影片。 而且,當然,你可以在 Biodrop 上找到我的 Twitter/X、網站、YouTube 頻道、書籍、Podcast 等等。 謝謝!

取得 Spring 電子報

與 Spring 電子報保持聯繫

訂閱

搶先一步

VMware 提供培訓和認證來加速你的進展。

了解更多

取得支援

Tanzu Spring 在一個簡單的訂閱中提供 OpenJDK™、Spring 和 Apache Tomcat® 的支援和二進位檔。

了解更多

即將舉行的活動

查看 Spring 社群中所有即將舉行的活動。

查看全部