領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多注意:程式碼在此我的 Github 賬戶上:github.com/joshlong/bootiful-spring-boot-2024-blog。
大家好,Spring 的粉絲們!我是 Josh Long,我隸屬於 Spring 團隊。我很激動今年能作為主題演講者並在 微軟的 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——來開發一個新應用程式,您就會明白我的意思。點選“新增依賴項”,然後選擇“Web”、“Spring Data JDBC”、“OpenAI”、“GraalVM Native Support”、“Docker Compose”、“Postgres”和“Devtools”。
給它一個專案名稱。我給我的服務起了個名字……“service”。我起名字很在行。我繼承了我爸爸的這項天賦。我爸爸起名字也很棒。我小時候,我們家有一隻很小的白色小狗,我爸爸給它起了個名字叫White Dog。它做了好幾年的家庭寵物。但大約十年後,它失蹤了。我不知道後來發生了什麼。也許它找了份工作;我不知道。但後來,奇蹟般地,另一隻白色小狗出現在我們家裝有紗窗的門前。於是我們收留了它,我爸爸給它起了個名字叫Too。或者Two。我不知道。總之,起名字這件事,我真的很在行。不過,我媽媽一直告訴我,我能活下來,全靠她給我起了名字……嗯,這大概是真的。
無論如何,選擇 Java 21。這一點至關重要。如果您不使用 Java 21,就無法使用 Java 21。所以,您需要 Java 21。但我們也將使用 GraalVM 來發揮其原生映象的能力。
還沒有安裝 Java 21?下載它!使用非常棒的 SDKMAN 工具:sdk install java 21-graalce。然後將其設為預設:sdk default java 21-graalce。開啟一個新的終端。下載 .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.url 和 spring.datasource.password 等。所有這些都透過 Spring Boot 令人驚歎的自動配置來完成。您一定會喜歡!而且,為了不留下爛攤子,Spring Boot 在應用程式關閉時也會關閉 Docker 容器。
我們希望儘可能快地推進。為此,我們在 Spring Initializr 上選擇了 DevTools。它將使我們能夠快速推進。核心理念是,Java 的重啟速度相當慢。然而,Spring 的重啟速度非常快。那麼,如果我們有一個監控專案資料夾的程序,能夠注意到新編譯的 .class 檔案,將它們載入到類載入器中,然後建立一個新的 Spring ApplicationContext,丟棄舊的,從而給我們一種即時重新載入的體驗呢?這正是 Spring 的 DevTools 所做的。在開發過程中執行它,您會發現重啟時間大大縮短!
之所以能這樣工作,再次強調,是因為 Spring 啟動速度非常快……除非,您每次重啟時都要啟動一個 PostgreSQL 資料庫。我喜歡 PostgreSQL,但是,呃,是的,它不適合在每次您調整方法名稱、修改 HTTP 端點路徑或最佳化一些 CSS 時都進行持續的重啟。所以,讓我們配置 Spring Boot 僅啟動 Docker Compose 檔案,並讓它一直執行,而不是每次都重啟。
將此屬性新增到 application.properties
spring.docker.compose.lifecycle-management=start_only
我們將從一個簡單的記錄開始。
package com.example.service;
import org.springframework.data.annotation.Id;
// look mom, no Lombok!
record Customer(@Id Integer id, String name) {
}
我喜歡 Java 記錄!您也應該喜歡!不要忽視記錄(records)。這個不起眼的小 record 不僅僅是比 Lombok 的 @Data 註解更好的實現方式,它實際上是一系列功能的一部分,這些功能在 Java 21 中得以完善,並且結合在一起,支援所謂的面向資料程式設計。
Java 語言架構師 Brian Goetz 在他 2022 年的 InfoQ 文章“面向資料程式設計” 中談到了這一點。
據分析,Java 在單體(monolith)領域佔據主導地位,這歸因於其強大的訪問控制、良好且快速的編譯器、隱私保護等等。Java 使得建立相對模組化、可組合的單體應用程式變得容易。單體應用程式通常是大型、龐雜的程式碼庫,而 Java 支援這一點。事實上,如果您想要模組化並想好好組織您龐大的單體程式碼庫,可以看看 Spring Modulith 專案。
但情況已經發生了變化。如今,我們在系統中表達變化的方式不是透過動態分派和多型性來表達深層抽象型別層次結構的特定實現,而是透過網路上經常臨時傳送的訊息,透過 HTTP/REST、gRPC、Apache Kafka 和 RabbitMQ 等訊息傳遞層。關鍵在於資料,笨蛋!
Java 已經發展到支援這些新模式。讓我們來看看四個關鍵特性——記錄(records)、模式匹配(pattern matching)、智慧 switch 表示式(smart switch expressions)和密封型別(sealed types)——來理解我的意思。假設我們從事一個監管嚴格的行業,例如金融。
想象一下我們有一個名為 Loan 的介面。顯然,貸款是受到嚴格監管的金融工具。我們不希望有人隨意新增 Loan 介面的匿名內部類實現,繞過我們為了構建系統而付出的驗證和保護。
所以,我們將使用 sealed 型別。密封型別是一種新的訪問控制或可見性修飾符。
package com.example.service;
sealed interface Loan permits SecuredLoan, UnsecuredLoan {
}
record UnsecuredLoan(float interest) implements Loan {
}
final class SecuredLoan implements Loan {
}
在示例中,我們明確規定系統中 Loan 有兩種實現:SecuredLoan 和 UnsecuredLoan。類預設對子類化是開放的,這違反了密封層次結構所暗示的保證。因此,我們明確地將 SecuredLoan 設為 final。UnsecuredLoan 實現為一個記錄(record),並且是隱式 final 的。
記錄(Records)是 Java 對元組(tuples)的回應。它們就是元組。只是 Java 是一種命名語言:事物都有名稱。這個元組也有一個名字:UnsecuredLoan。如果我們同意它們所隱含的契約,記錄(records)就能給我們帶來巨大的力量。記錄(records)的核心理念是,物件的標識與其欄位(稱為“元件”)的標識相等。所以在這個例子中,記錄(record)的標識等於 interest 變數的標識。如果我們同意這一點,那麼編譯器就可以為我們提供建構函式,為每個元件提供儲存空間,提供 toString 方法、hashCode 方法和 equals 方法。它還會為建構函式中的元件提供訪問器。很棒!而且,它還支援解構!語言知道如何提取記錄(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 變數,對吧。相反,我們想解引用 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 守護程序來啟動 PostgreSQL 例項。
但從此以後,我們將使用 Spring Boot 的 DevTools。您只需要重新編譯。如果應用程式正在執行,並且您正在使用 Eclipse 或 Visual Studio Code,您只需要儲存檔案:macOS 上的 CMD+S。IntelliJ IDEA 沒有 Save 選項;使用 macOS 上的 CMD+Shift+F9 強制構建。很棒。
好了,我們有一個 HTTP Web 端點在監視資料庫,但資料庫裡什麼都沒有,所以這肯定會失敗。讓我們用一些模式和一些示例資料來初始化我們的資料庫。
新增 schema.sql 和 data.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');
確保透過將以下屬性新增到 application.properties 來告訴 Spring Boot 在啟動時執行 SQL 檔案。
spring.sql.init.mode=always
重新載入應用程式:macOS 上的 CMD+Shift+F9。在我的電腦上,這個過載時間大約是重啟 JVM 和應用程式本身所需時間的 1/3,或者說慢了 66%。太棒了。
它已經啟動並運行了。訪問 https://:8080/customers 檢視結果。成功了!當然,成功了。這是個演示。它總是會成功的。
這一切都是非常標準的操作。十年前您也可以做類似的事情。不過,程式碼會冗長得多。Java 自那以後取得了長足的進步。當然,速度也無法相比。當然,現在的抽象更好。但是,您本可以做類似的事情——一個監視資料庫的 Web 應用程式。
事物在變化。總有新的前沿。現在,新的前沿是人工智慧,即 AI。AI:因為對智慧的追求顯然還不夠困難。
AI 是一個巨大的產業,但大多數人在想到 AI 時想到的是利用 AI。您不需要使用 Python 就可以使用大型語言模型(LLM),就像大多數人不需要使用 C 就可以使用 SQL 資料庫一樣。您只需要整合 LLM,而 Java 在選擇和功能方面無與倫比。
在我們上次的盛會 SpringOne 開發者活動(2023 年)上,我們宣佈了 Spring AI,這是一個旨在讓整合和使用 AI 變得儘可能容易的新專案。
您需要攝入資料,例如來自賬戶、檔案、服務,甚至是一組 PDF。您需要將它們儲存在向量資料庫中以便於檢索,以支援相似性搜尋。然後,您需要與 LLM 整合,並向其提供來自該向量資料庫的資料。
當然,您可能想要的任何 LLM 都有客戶端繫結——Amazon Bedrock、Azure OpenAI、Google Vertex以及Google Gemini、Ollama、HuggingFace,當然還有 OpenAI 本身,但這僅僅是開始。
為 LLM 提供動力的所有知識都內嵌在模型中,然後該模型會影響 LLM 對世界的理解。但該模型有某種過期的日期,之後其知識就會過時。如果模型是在兩週前構建的,它就不會知道昨天發生的事情。因此,如果您想構建一個自動助手,例如處理使用者關於其銀行賬戶的請求,那麼在處理請求時,該 LLM 需要了解世界的最新狀態。
您可以在請求中新增資訊,並將其用作上下文來指導響應。如果只是這麼簡單,那倒還好。還有一個難點。不同的 LLM 支援不同的令牌視窗大小。令牌視窗決定了您在給定請求中可以傳送和接收多少資料。視窗越小,您可以傳送的資訊就越少,LLM 的響應也會越不全面。
您可以在這裡做的一件事是,將資料放入向量儲存,如 pgvector、Neo4j、Weaviate 等,然後將您的 LLM 連線到該向量資料庫。向量儲存使您能夠給定一個詞或一組詞,找到與它們相似的其他事物。它將資料儲存為數學表示,並允許您查詢相似的事物。
這個將資料攝入、豐富、分析和消化以指導 LLM 響應的整個過程稱為檢索增強生成(RAG),Spring AI 支援所有這些。想了解更多,請參閱我關於 Spring AI 的這個 Spring Tips 影片。不過,我們在這裡不會利用所有這些功能。只利用一個。
我們在 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 客戶端。
您需要透過一個屬性來配置與 OpenAI API 的連線:spring.ai.openai.api-key=。我在執行程式之前將其匯出為環境變數 SPRING_AI_OPENAI_API_KEY。我不會在這裡複製我的金鑰。請原諒我沒有洩露我的 API 憑證。
按 CMD+Shift+F9 重新載入應用程式,然後訪問端點:https://:8080/story。LLM 可能需要幾秒鐘才能生成響應,所以準備好您的咖啡、水或其他飲品,以便快速但令人滿意地啜飲。
就是這樣!我們生活在一個奇蹟的時代!自由放飛的奇點時代!您現在可以做任何事情。
但是,確實花了點時間,對吧?我並不介意計算機花費這些時間!它做得非常出色!我無法做得更快。看看它生成的那個故事!它是一件藝術品。
但確實花了一些時間。這給我們的應用程式帶來了可擴充套件性問題。在後臺,當我們呼叫 LLM 時,我們實際上是在進行網路呼叫。在程式碼深處,有一個 java.net.Socket,我們從中獲得了一個 java.io.InputStream,它代表了來自服務的資料位元組陣列。我不知道您是否還記得直接使用 InputStream。這裡有一個例子
try (var is = new FileInputStream("afile.txt")) {
var next = -1;
while ((next = is.read()) != -1) {
next = is.read();
// do something with read
}
}
看到我們在第 4 行透過呼叫 InputStream.read 從 InputStream 中讀取位元組的部分了嗎?我們稱之為阻塞操作。如果我們呼叫 InputStream.read,那麼我們必須等到該呼叫返回後才能進行第 5 行的操作。
如果我們要連線的服務返回的資料太多怎麼辦?如果服務宕機了怎麼辦?如果它永遠不返回怎麼辦?如果我們陷入了無限等待怎麼辦?萬一呢?
如果這種情況只發生一次,那會很乏味。但如果它發生在用於處理 HTTP 請求的系統中的每個執行緒上,那將是對我們服務的生存威脅。這種情況經常發生。這就是為什麼有時您可以登入到一個完全無響應的 HTTP 服務,卻發現 CPU 基本處於休眠狀態——空閒!——什麼也沒做或做得很少。執行緒池中的所有執行緒都處於等待狀態,等待著永遠不會來的東西。
這是對我們付費購買的寶貴 CPU 容量的巨大浪費。即使是最好的情況也不理想。即使方法最終會返回,這也意味著處理該請求的執行緒無法用於系統中的其他任何事情。該方法壟斷了該執行緒,因此係統中沒有人可以使用它。如果執行緒便宜且充裕,這不成問題。但它們不是。在 Java 的大部分生命週期中,每個新執行緒都與作業系統執行緒一對一對應。而且它並不便宜。每個執行緒都有一定數量的記賬開銷。一到兩兆位元組。因此,您無法建立很多執行緒,而且您還浪費了本已稀少的執行緒。太可怕了!誰需要睡覺呢?
一定有更好的方法。
您可以使用非阻塞 IO。例如,那些讓人痛苦且複雜的 Java NIO 庫。這是一種選擇,就像與一群臭鼬一起生活是一種選擇一樣:太糟糕了!大多數人不會以非阻塞 IO 或常規 IO 的方式思考。我們生活在更高的抽象層面。我們可以使用響應式程式設計。我*喜歡*響應式程式設計。我甚至寫了一本關於它的書——Reactive Spring。但如果您不習慣像函式式程式設計師那樣思考,那麼如何讓它工作就不是那麼顯而易見了。這是一種不同的正規化,意味著重寫您的程式碼。
如果我們能兩全其美怎麼辦?使用 Java 21,現在我們可以了!有一個名為虛擬執行緒(virtual threads)的新功能,可以使這些事情變得容易得多!如果您在這些新的虛擬執行緒上執行阻塞操作,執行時就會檢測到您正在執行阻塞操作——例如 java.io.InputStream.read、java.io.OutputStream.write 和 java.lang.Thread.sleep——然後將該阻塞、空閒的活動從執行緒中移出並放入 RAM。然後,它基本上會為睡眠設定一個定時器,或監視 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 毫秒,共四次。在此期間,我們測試是否在 1000 個執行緒中的第一個執行緒上,如果是,我們透過將當前執行緒的名稱新增到集合中來記錄它。集合會去重其元素;如果出現相同的名稱,集合中仍只有一個元素。
執行程式(CMD+Shift+F9!),您將看到程式的物理行為沒有改變。Set<String> 中只有一個名稱。為什麼沒有呢?我們只是一遍又一遍地測試同一個執行緒。
現在,將該建構函式更改為使用虛擬執行緒:Thread.ofVirtual。非常簡單的更改。現在執行程式。CMD+Shift+F9。
您會看到集合中有多個元素。您根本沒有改變程式碼的核心邏輯。事實上,您甚至只需要更改一件事,但現在,在幕後,編譯器和執行時無縫地重寫了您的程式碼,以便當虛擬執行緒上發生阻塞操作時,執行時會無縫地將您移出,並在阻塞操作完成後將您移回執行緒。這意味著您之前存在的執行緒現在可用於系統的其他部分。您的可擴充套件性將呈指數級增長!
您可能會抗議,我不想更改我所有的程式碼。首先,這是一個荒謬的論點,更改微不足道。您可能會抗議:我也不想建立執行緒。說得好。Tony Hoare 在 1970 年代寫道,NULL 是十億美元的錯誤。他錯了。實際上是 PHP。但是,他也詳細討論了使用執行緒構建系統是多麼不可行。您將需要使用更高級別的抽象,如 sagas、actors,或者至少是 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。容易。請記住,朋友不讓朋友寫 Dockerfiles。使用 Buildpacks。Spring Boot 原生支援它們:./gradlew bootBuildImage 或 ./mvnw spring-boot:build-image。不過,這不算新,所以下一個問題。
我們如何讓這個東西儘可能高效和最佳化?在我們深入研究之前,我的朋友們,重要的是要記住,Java 已經非常非常非常高效了。我喜歡 2018 年(COVID 疫情前,或公元前)的這篇 部落格。
它研究了哪些語言消耗的能源最少,或者說能源效率最高。C 語言能源效率最高。它消耗的電力最少。1.0。這是基線。它對機器很有效。對人無效!絕對不是對人。
然後我們有 Rust 及其零成本抽象。幹得好。
然後我們有 C++... 
C++ 真噁心!繼續……
然後我們有 Ada 語言,但是……誰在乎呢?
然後我們有 Java,它的效率接近 2.0。我們就約等於 2.0 吧。Java 的效率是 C 的兩倍——兩次!——或者說是 C 的一半。
到目前為止還不錯?太棒了。它仍然排在前 5 名最高效的語言之列!
如果向下滾動列表,您會看到一些驚人的數字。Go 和 C# 的分數在 3.0+ 範圍內。向下滾動,我們有 JavaScript 和 TypeScript,其中一個——令我費解——效率是另一個的四倍!
然後我們有 PHP 和 Hack,對它們少說為妙。繼續!
然後我們有 JRuby 和 Ruby。朋友們,請記住 JRuby 是用 Java 編寫的 Ruby。而 Ruby 是用 C 編寫的 Ruby。儘管如此,JRuby 的效率幾乎比 Ruby 高三分之一!僅僅因為它用 Java 編寫並在 JVM 上執行。JVM 是一個了不起的工具。絕對非凡。
然後……我們有了 Python。而這,嗯,這讓我非常傷心!我*喜歡* Python!我從 1990 年代就開始使用 Python!比爾·克林頓擔任總統時我才學會 Python!但這些數字並不好。想想看。75.88。我們約等於 76。我不擅長數學。但您知道什麼很擅長嗎?該死的 Python!讓我們問問它。

38!這意味著,如果您執行一個 Java 程式,執行它所需的能量產生一些碳排放,最終會困在大氣中,提高溫度,而升高的溫度又會殺死一棵樹,那麼在 Python 中執行的等效程式將殺死三十八棵樹!那是一片森林!那比比特幣還糟糕!我們需要儘快採取行動。我不知道是什麼,但必須做點什麼。
總之,我想說的是,Java 已經令人驚歎。我認為這是因為人們理所當然的兩件事:垃圾回收和即時(JIT)編譯器。
垃圾回收,我們都知道它是什麼。嘿,甚至白宮在其關於保護軟體以確保網路空間構建塊安全的最新報告中也讚賞像 Java 這樣的垃圾回收、記憶體安全的語言。
Java 程式語言的垃圾回收器讓我們能夠編寫平庸的軟體並勉強矇混過關。這太棒了!不過,我確實不同意它是原始的 Java 垃圾回收器的說法!那份榮譽屬於其他地方,比如 Jesslyn。
JIT 也是另一個了不起的工具。它分析應用程式中經常訪問的程式碼路徑,並將它們轉換為特定於作業系統和體系結構的本機程式碼。但它只能對您的一部分程式碼進行此操作。它需要知道在編譯程式碼時使用的型別是執行時使用的唯一型別。而 Java 中的一些東西——一種非常動態的語言,其執行時更像 JavaScript、Ruby 和 Python 的執行時——允許 Java 程式執行違反此約束的操作。例如序列化、JNI、反射、資源載入和 JDK 代理。請記住,使用 Java,您可以有一個String,其內容是一個 Java 原始碼檔案,將該字串編譯到檔案系統上的.class 檔案,將.class 檔案載入到ClassLoader中,透過反射建立一個該類的例項,然後——如果該類是一個介面——建立一個該類的 JDK 代理。並且,如果該類實現了java.io.Serializable,就可以將該類例項寫入網路套接字並在另一個 JVM 上載入。您可以在不曾明確鍵入任何超過java.lang.Object 的引用型別的情況下完成所有這些操作!這是一種了不起的語言,而這種動態性使其成為一種非常高效的語言。它也阻礙了 JIT 的最佳化嘗試。
不過,JIT 在力所能及的範圍內做得非常出色。結果不言而喻。所以,有人不禁要問:為什麼我們不能主動 JIT 整個程式?提前進行?我們可以。有一個名為 GraalVM 的 OpenJDK 發行版,它提供了許多便利功能,透過native-image 編譯器等額外工具擴充套件了 OpenJDK 版本。原生映象編譯器非常棒。但這個原生映象編譯器有同樣的限制。它無法對非常動態的東西發揮其魔力。這成了一個問題。因為大多數程式碼——您的單元測試庫、您的 Web 框架、您的 ORM、您的日誌庫……所有東西!——都使用了一個或多個這些動態行為。
有一個解決辦法。您可以以 .json 檔案的形式提供配置,放在一個眾所周知的目錄中:src/main/resources/META-INF/native-image/$groupId/$artifactId/\*.json。這些 .json 檔案有兩個問題。
首先,“JSON”這個詞聽起來很蠢。我不喜歡說“JAY-SAWN”這個詞。作為一個成年人,我無法相信我們對彼此說這些話。我說法語,而在法語中,你會讀成jeeesã。所以,.gison。好多了。閩南語有一個詞——gingsong(快樂),也可以用。所以你可以用 .gingsong。選擇你的隊伍!無論如何,.json 不應該存在。我支援 .gison,但這無關緊要。
第二個問題是,有太多東西是必需的!同樣,只要想想您的程式中所有那些有趣的、動態的行為,比如反射、序列化、JDK 代理、資源載入和 JNI!這無窮無盡。您的 Web 框架。您的測試庫。您的資料訪問技術。我沒有時間為每個程式編寫手工製作的配置。我甚至沒有足夠的時間來完成這篇博文!
所以,取而代之的是,我們將使用 Spring Boot 中引入的 3.0 版本之後的 Spring Boot 提前(AOT)引擎。AOT 引擎會分析您 Spring 應用程式中的 bean,併為您生成所需的配置檔案。太棒了!甚至還有一個完整的元件模型,您可以利用它來擴充套件 Spring 到編譯時。我不會在這裡詳細介紹,但您可以閱讀我的 免費電子書,或者觀看我介紹所有關於 Spring 和 AOT 的免費 YouTube 影片。它們的內容基本相同,只是消費方式不同。
所以讓我們用 Gradle 啟動構建,./gradlew nativeCompile,或者如果您使用 Apache Maven,則使用 ./mvnw -Pnative native:compile。您可能想在此跳過測試……此構建將花費一些時間。請記住,它正在分析您程式碼庫中的所有內容——無論是類路徑上的庫、JRE,還是您程式碼本身中的類——以確定應該保留哪些型別,以及應該丟棄哪些型別。結果是一個精益、高效、閃電般快速的執行時機器,但代價是編譯時間非常、*非常*慢。
它耗時太長了,以至於會卡住我的流程。它讓我停滯不前,等待著。我就像這篇博文前面提到的平臺執行緒一樣:阻塞了!我感到無聊。等待。等待。我現在終於明白了 著名的 XKCD 漫畫。
有時我開始哼歌。或者主題曲。或者電梯音樂。您知道 電梯音樂聽起來是什麼樣的,對吧?持續不斷,永無止境。所以,我想,如果每個人都能聽到電梯音樂,那不是很好嗎?所以我問了。我得到了一些很棒的回應。
其中一個建議是,我們應該播放任天堂 64 電子遊戲《007:黃金眼》——第一部皮爾斯·布魯斯南飾演詹姆斯·邦德的電影——的原聲帶中的電梯音樂。我喜歡這個建議。
一個回覆建議新增一個嗶嗶聲會很有用。我完全同意。我那該死的微波爐在完成時會發出叮!的聲音。為什麼我那價值數百萬行的編譯器不行呢?
然後我們得到了這個回覆,來自我最喜歡的醫生之一,在 GraalVM 團隊工作的 Niephaus 博士。他說新增電梯音樂只能治標,不能治本,根本問題在於如何讓 GraalVM 在時間和記憶體方面更有效率。

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

我相信它很快就會被合併……
總之!如果您檢查編譯,它現在應該完成了。它位於 ./build/native/nativeCompile/ 資料夾中,名為 service。在我的機器上,編譯花了 52 秒。我的天!
執行它。它會失敗,因為,再說一遍,我們過著 git clone & 執行的生活!我們沒有指定任何連線憑證!所以,用指定 SQL 資料庫連線詳細資訊的環境變數來執行它。這是我在我的機器上使用的指令碼。這隻在類 Unix 系統上執行,並且適用於 Maven 或 Gradle。
#!/usr/bin/env bash
export SPRING_DATASOURCE_URL=jdbc:postgresql:///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 Lambda 風格的函式即服務,它會更快,因為我不需要打包 HTTP 伺服器。事實上,如果純粹的啟動速度是*我*真正想要的全部,那麼我甚至可能會使用 Spring 對 Project CRaC 的出色支援。這與本文無關。我並不特別在意,因為這是一個獨立的、長期的服務。我關心的是資源使用情況,由常駐集大小 (RSS) 表示。請注意程序識別符號 (PID)——它會在日誌中。如果 PID 是,比如說,55,那麼可以使用 ps 實用程式獲取 RSS,該實用程式幾乎在所有類 Unix 系統上都可用。
ps -o rss 55
它會輸出一個以千位元組為單位的數字;將其除以一千,您將得到以兆位元組為單位的數字。在我的機器上,它執行只需要 100MB 多一點。您無法在那麼小的記憶體中執行 Slack!我敢打賭,Chrome 中單個瀏覽器標籤頁佔用的記憶體就這麼多,或者更多!
因此,我們得到了一個程式,它儘可能簡潔,同時又易於開發和迭代。它使用虛擬執行緒為我們提供無與倫比的可擴充套件性。它作為一個獨立的、自包含的、特定於作業系統和體系結構的本機映像執行。哦!而且,它支援自由放飛的奇點!
我們生活在一個美好的時代。現在是成為一名 Java 和 Spring 開發者的最佳時機。我希望我已經成功地說服了您。
我以及 Spring 團隊的其他成員將在微軟的 JDConf 2024 上發表演講!
註冊並參加我的會議:JDConf:Bootiful Spring Boot 3
JDConf 上其他值得參加的 Spring 會議:
如果您喜歡這篇博文,我希望您訂閱我們的 YouTube 頻道,我每週都會在 Spring Tips 播放列表中釋出新影片。當然,您可以在 Biodrop 上找到我的 Twitter/X、網站、YouTube 頻道、書籍、播客等等。謝謝!