領先一步
VMware 提供培訓和認證,助您加速進步。
瞭解更多你好,Spring 愛好者!
在我們開始之前,請幫我做一件事。如果你還沒有安裝,請去 安裝 SDKMAN。
然後執行
sdk install java 21-graalce && sdk default java 21-graalce
看,你就做到了。現在你的電腦上已經安裝了支援 Java 21 的 Java 21 和 GraalVM,隨時可用。在我看來,Java 21 是 Java 中最關鍵的版本,也許是前所未有的,因為它為 Java 使用者帶來了全新的機遇。它帶來了大量優秀的 API 和新增功能,例如模式匹配,這是多年來不斷為平臺新增的功能的集大成者。但迄今為止最突出的功能是(Project Loom)對虛擬執行緒的新支援。虛擬執行緒和 GraalVM 原生映象意味著,今天你可以編寫出效能和可伸縮性堪比 C、Rust 或 Go 的程式碼,同時保留 JVM 強大而熟悉的生態系統。
成為 JVM 開發者的最好時機從未如此合適。
我剛釋出了一個影片,探討了 Java 21 和 GraalVM 中的新功能和機遇。
在這篇博文中,我希望回顧一下同樣的內容,並補充一些適合文字形式的額外資料。
首先,說清楚。如果從上面的安裝過程還不明顯的話,我建議先安裝 GraalVM。它是 OpenJDK,所以你可以獲得所有 OpenJDK 的內容,但它也能建立 GraalVM 原生映象。
為什麼需要 GraalVM 原生映象?因為它速度快且資源效率極高。傳統上,這種說法總是有個反駁:“是的,好吧,JIT 在純粹的 Java 中仍然更快”,對此我會反駁道:“是的,好吧,你可以更容易地以更小的記憶體佔用空間來擴充套件新例項,以彌補任何損失的吞吐量,並且仍然在資源消耗支出方面領先!” 這是事實。
但現在我們甚至不必進行這種細緻的討論了。根據 GraalVM 釋出部落格,Oracle 的 GraalVM 原生映象配合剖面導向最佳化,在基準測試中的效能現在持續領先於 JIT,而以前只是在某些方面領先。Oracle GraalVM 不一定等同於開源 GraalVM 發行版,但關鍵在於,最高級別的效能現在已超過 JRE JIT。

這篇來自 10MinuteMail 的精彩文章,講述了他們如何使用 GraalVM 和 Spring Boot 3 將啟動時間從約 30 秒縮短到約 3 毫秒,記憶體使用量從 6.6GB 減少到 1GB,同時保持相同的吞吐量和 CPU 利用率。太棒了。
Java 21 中的許多功能都建立在 Java 17 中首次引入的功能之上(在某些情況下,甚至更早!)。在探討它們在 Java 21 中的最終體現之前,讓我們回顧一下其中一些功能。
你知道 Java 支援多行字串嗎?這是我最喜歡的功能之一,它使使用 JSON、JDBC、JPA QL 等比以往任何時候都更加方便。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class MultilineStringTest {
@Test
void multiline() throws Exception {
var shakespeare = """
To be, or not to be, that is the question:
Whether 'tis nobler in the mind to suffer
The slings and arrows of outrageous fortune,
Or to take arms against a sea of troubles
And by opposing end them. To die—to sleep,
No more; and by a sleep to say we end
The heart-ache and the thousand natural shocks
That flesh is heir to: 'tis a consummation
Devoutly to be wish'd. To die, to sleep;
To sleep, perchance to dream—ay, there's the rub:
For in that sleep of death what dreams may come,
""";
Assertions.assertNotEquals(shakespeare.charAt(0), 'T');
shakespeare = shakespeare.stripLeading();
Assertions.assertEquals(shakespeare.charAt(0), 'T');
}
}
沒什麼太令人驚訝的。易於理解。三重引號開始和結束多行字串。你也可以刪除前導、尾隨和縮排空格。
Record 是我最喜歡的 Java 功能之一!它們太棒了!你是否有這樣一個類的身份等同於類中的欄位?當然有。想想你的基本實體、事件、DTO 等。每當你使用 Lombok 的 @Data 時,都可以同樣方便地使用 record。它們在 Kotlin(data class)和 Scala(case class)中有類似的類,因此很多人也知道它們。終於在 Java 中有了它們,這太好了。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class RecordTest {
record JdkReleasedEvent(String name) { }
@Test
void records() throws Exception {
var event = new JdkReleasedEvent("Java21");
Assertions.assertEquals( event.name() , "Java21");
System.out.println(event);
}
}
這種簡潔的語法會生成一個帶有建構函式、類中相關的儲存、getter(例如:event.name())、有效的 equals 和良好的 toString() 實現的類。
我很少使用現有的 switch 語句,因為它很笨拙,而且通常還有其他模式,例如 訪問者模式,這些模式能給我帶來大部分的好處。現在有一個新的 switch,它是一個表示式,而不是語句,因此我可以將 switch 的結果賦值給一個變數,或返回它。
這是一個將經典 switch 重寫為使用新的增強型 switch 的示例。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.DayOfWeek;
class EnhancedSwitchTest {
// ①
int calculateTimeOffClassic(DayOfWeek dayOfWeek) {
var timeoff = 0;
switch (dayOfWeek) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY:
timeoff = 16;
break;
case SATURDAY, SUNDAY:
timeoff = 24;
break;
}
return timeoff;
}
// ②
int calculateTimeOff(DayOfWeek dayOfWeek) {
return switch (dayOfWeek) {
case MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY -> 16;
case SATURDAY, SUNDAY -> 24;
};
}
@Test
void timeoff() {
Assertions.assertEquals(calculateTimeOffClassic(DayOfWeek.SATURDAY), calculateTimeOff (DayOfWeek.SATURDAY));
Assertions.assertEquals(calculateTimeOff(DayOfWeek.FRIDAY), 16);
Assertions.assertEquals(calculateTimeOff(DayOfWeek.FRIDAY), 16);
}
}
instanceof 檢查新的 instanceof 測試使我們能夠避免過去笨拙的檢查和轉換,過去的程式碼看起來像這樣:
var animal = (Object) new Dog ();
if (animal instanceof Dog ){
var fido = (Dog) animal;
fido.bark();
}
並用此替換:
var animal = (Object) new Dog ();
if (animal instanceof Dog fido ){
fido.bark();
}
智慧 instanceof 會自動為測試範圍內的變數分配一個向下轉換的變數。無需在同一個塊中兩次指定類 Dog。智慧 instanceof 運算子的使用是 Java 平臺中模式匹配的第一個真正體驗。模式匹配背後的思想很簡單:匹配型別並從中提取資料。
嚴格來說,密封型別也屬於 Java 17,但它們目前的作用不大。基本思想是,在過去,限制類型可擴充套件性的唯一方法是透過可見性修飾符(public、private 等)。使用 sealed 關鍵字,你可以明確允許哪些類可以繼承另一個類。這是一個巨大的飛躍,因為它讓編譯器能夠看到哪些型別可能擴充套件給定的型別,從而能夠進行最佳化,並在編譯時幫助我們理解是否已涵蓋所有可能的案例,例如在增強型 switch 表示式中。讓我們看看它的實際應用。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class SealedTypesTest {
// ①
sealed interface Animal permits Bird, Cat, Dog {
}
// ②
final class Cat implements Animal {
String meow() {
return "meow";
}
}
final class Dog implements Animal {
String bark() {
return "woof";
}
}
final class Bird implements Animal {
String chirp() {
return "chirp";
}
}
@Test
void doLittleTest() {
Assertions.assertEquals(communicate(new Dog()), "woof");
Assertions.assertEquals(communicate(new Cat()), "meow");
}
// ③
String classicCommunicate(Animal animal) {
var message = (String) null;
if (animal instanceof Dog dog) {
message = dog.bark();
}
if (animal instanceof Cat cat) {
message = cat.meow();
}
if (animal instanceof Bird bird) {
message = bird.chirp();
}
return message;
}
// ④
String communicate(Animal animal) {
return switch (animal) {
case Cat cat -> cat.meow();
case Dog dog -> dog.bark();
case Bird bird -> bird.chirp();
};
}
}
switch 表示式將失敗。sealed,從而宣告它允許哪些類作為子類,要麼必須宣告為 final。instanceof 檢查來更輕鬆地處理每種可能的型別,但在這裡我們得不到編譯器的幫助。switch *結合*模式匹配,就像我們在這裡做的那樣。請注意經典版本的笨拙。真討厭。我很高興擺脫了它。另一件好事是,switch 表示式現在會告訴我們是否涵蓋了所有可能的案例,就像 enum 一樣。感謝編譯器!
結合所有這些,我們開始順利進入 Java 21 的世界。從這裡開始,我們將審視自 Java 17 以來出現的新功能。
Records、Switch 和 If 實現更高級別的模式匹配增強型 switch 表示式和模式匹配非常出色,這讓我想知道多年前使用 Akka 的感覺會是怎樣的,如果使用 Java 並且有這種優秀的全新語法。模式匹配與 Record 結合使用時,互動效果更佳,因為 Record — 如前所述 — 是其元件的摘要,並且編譯器也知道這一點。因此,它也可以將這些元件提升為新的變數。你也可以在 if 檢查中使用這種模式匹配語法。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.time.Instant;
class RecordsTest {
record User(String name, long accountNumber) {
}
record UserDeletedEvent(User user) {
}
record UserCreatedEvent(String name) {
}
record ShutdownEvent(Instant instant) {
}
@Test
void respondToEvents() throws Exception {
Assertions.assertEquals(
respond(new UserCreatedEvent("jlong")), "the new user with name jlong has been created"
);
Assertions.assertEquals(
respond(new UserDeletedEvent(new User("jlong", 1))),
"the user jlong has been deleted"
);
}
String respond(Object o) {
// ①
if (o instanceof ShutdownEvent(Instant instant)) {
System.out.println(
"going to to shutdown the system at " + instant.toEpochMilli());
}
return switch (o) {
// ②
case UserDeletedEvent(var user) -> "the user " + user.name() + " has been deleted";
// ③
case UserCreatedEvent(var name) -> "the new user with name " + name + " has been created";
default -> null;
};
}
}
String,所以我們將使用新的模式匹配支援,並配合 if 語句。UserDeletedEvent 中的 User user。UserCreatedEvent 中的 String name。所有這些功能都在早期版本的 Java 中開始生根發芽,但在 Java 21 中匯聚在一起,形成了你可能稱之為面向資料程式設計的東西。它不是面向物件程式設計的替代品,而是對其的補充。你可以使用模式匹配、增強型 switch 和 instanceof 運算子等功能,在不暴露公共 API 中分派點的情況下,為你的程式碼帶來新的多型性。
Java 21 中還有許多其他新功能。有一些小但不錯的功能,當然還有 Project Loom 或虛擬執行緒。(僅虛擬執行緒就值回票價!)讓我們深入瞭解其中一些出色的功能。
在人工智慧和演算法領域,高效的數學運算比以往任何時候都更重要。新的 JDK 在這方面有一些不錯的改進,包括 BigInteger 的並行乘法以及各種除法過載,這些過載會在溢位時丟擲異常。而不僅僅是除以零錯誤。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.math.BigInteger;
class MathematicsTest {
@Test
void divisions() throws Exception {
//<1>
var five = Math.divideExact( 10, 2) ;
Assertions.assertEquals( five , 5);
}
@Test
void multiplication() throws Exception {
var start = BigInteger.valueOf(10);
// ②
var result = start.parallelMultiply(BigInteger.TWO);
Assertions.assertEquals(BigInteger.valueOf(10 * 2), result);
}
}
BigInteger 例項進行並行乘法。請記住,只有當 BigInteger 包含數千位時,它才真正有用……Future#state如果你正在進行非同步程式設計(是的,即使有了 Project Loom,它仍然是一種事物),那麼你會很高興知道我們老朋友 Future<T> 現在提供了一個 state 例項,你可以對其進行 switch 來檢視正在進行的非同步操作的狀態。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.concurrent.Executors;
class FutureTest {
@Test
void futureTest() throws Exception {
try (var executor = Executors
.newFixedThreadPool(Runtime.getRuntime().availableProcessors())) {
var future = executor.submit(() -> "hello, world!");
Thread.sleep(100);
// ①
var result = switch (future.state()) {
case CANCELLED, FAILED -> throw new IllegalStateException("couldn't finish the work!");
case SUCCESS -> future.resultNow();
default -> null;
};
Assertions.assertEquals(result, "hello, world!");
}
}
}
state 物件,讓我們列舉已提交的 Thread 狀態。它與增強型 switch 功能配合得很好。HTTP 客戶端 API 是你將來可能希望將非同步操作包裝起來並使用 Project Loom 的地方。HTTP 客戶端 API 自 Java 11 以來就存在了,現在已經是十個版本以前了!但是,現在它有了這個時髦的新 AutoCloseable API。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
class HttpTest {
@Test
void http () throws Exception {
// ①
try (var http = HttpClient
.newHttpClient()){
var request = HttpRequest.newBuilder(URI.create("https://httpbin.org"))
.GET()
.build() ;
var response = http.send( request, HttpResponse.BodyHandlers.ofString());
Assertions.assertEquals( response.statusCode() , 200);
System.out.println(response.body());
}
}
}
HttpClient。請注意,如果你啟動了任何執行緒並在其中傳送 HTTP 請求,則不應使用 AutoCloseable,除非小心確保它僅在所有執行緒執行完畢後才能到達作用域的末尾。我在那個示例中使用了 HttpResponse.BodyHandlers.ofString 來獲取 String 響應。你可以獲得各種各樣的物件,而不僅僅是 String。但 String 結果很好,因為它們是另一個出色的 Java 21 功能的絕佳過渡:對處理 String 例項的新支援。這個類展示了我最喜歡的兩個功能:用於 StringBuilder 的 repeat 操作以及檢測 String 中表情符號存在的方法。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
class StringsTest {
@Test
void repeat() throws Exception {
// ①
var line = new StringBuilder()
.repeat("-", 10)
.toString();
Assertions.assertEquals("----------", line);
}
@Test
void emojis() throws Exception {
// ②
var shockedFaceEmoji = "\uD83E\uDD2F";
var cp = Character.codePointAt(shockedFaceEmoji.toCharArray(), 0);
Assertions.assertTrue(Character.isEmoji(cp));
System.out.println(shockedFaceEmoji);
}
}
StringBuilder 重複 String(我們能否集體淘汰各種 StringUtils 了?)。String 中的表情符號。雖然是小的生活質量改進,但我仍然很高興。
你需要一個有序集合來排序那些 String 例項。Java 提供了幾個這樣的集合,例如 LinkedHashMap、List 等,但它們沒有共同的祖先。現在有了;歡迎 SequencedCollection!在這個示例中,我們使用了一個簡單的 ArrayList<String>,並使用了新的工廠方法來建立像 LinkedHashSet 這樣的集合。這個新的工廠方法在內部進行一些計算,以確保在新增的元素數量達到你在建構函式中指定的數量之前,它不會發生重新平衡(從而緩慢地重新雜湊所有內容)。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.LinkedHashSet;
import java.util.SequencedCollection;
class SequencedCollectionTest {
@Test
void ordering() throws Exception {
var list = LinkedHashSet.<String>newLinkedHashSet(100);
if (list instanceof SequencedCollection<String> sequencedCollection) {
sequencedCollection.add("ciao");
sequencedCollection.add("hola");
sequencedCollection.add("ni hao");
sequencedCollection.add("salut");
sequencedCollection.add("hello");
sequencedCollection.addFirst("ola"); //<1>
Assertions.assertEquals(sequencedCollection.getFirst(), "ola"); // ②
}
}
}
還有類似的 getLast 和 addLast 方法,甚至還有透過 reverse 方法支援反轉集合。
最後,我們來到了 Loom。你肯定聽說過 Loom。基本思想是讓你能夠擴充套件你在大學時編寫的程式碼!這是什麼意思?讓我們編寫一個簡單的網路服務,它會打印出接收到的任何內容。我們必須從一個 InputStream 讀取並將所有內容累積到一個新的緩衝區(一個 ByteArrayOutputStream)中。然後,當請求完成時,我們將列印 ByteArrayOutputStream 的內容。問題是我們可能同時接收到大量資料。因此,我們將使用執行緒來同時處理多個請求。
這是程式碼:
package bootiful.java21;
import java.io.ByteArrayOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.Executors;
class NetworkServiceApplication {
public static void main(String[] args) throws Exception {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
try (var serverSocket = new ServerSocket(9090)) {
while (true) {
var clientSocket = serverSocket.accept();
executor.submit(() -> {
try {
handleRequest(clientSocket);
} catch (Exception e) {
throw new RuntimeException(e);
}
});
}
}
}
}
static void handleRequest(Socket socket) throws Exception {
var next = -1;
try (var baos = new ByteArrayOutputStream()) {
try (var in = socket.getInputStream()) {
while ((next = in.read()) != -1) {
baos.write(next);
}
}
var inputMessage = baos.toString();
System.out.println("request: %s".formatted(inputMessage));
}
}
}
這相當瑣碎,是網路基礎知識。建立一個 ServerSocket,並等待新的客戶端(由 Socket 例項表示)出現。每當一個客戶端到達時,將其交給執行緒池中的一個執行緒。每個執行緒從客戶端 Socket 例項的 InputStream 引用讀取資料。客戶端可能會斷開連線、遇到延遲或傳送大量資料,所有這些都是一個問題,因為執行緒的數量是有限的,而且我們不能浪費我們寶貴的時間在它們身上。
我們使用執行緒來避免請求堆積而我們無法足夠快地處理。但在這裡,我們再次受挫,因為在 Java 21 之前,執行緒是昂貴的!每個 Thread 大約需要兩兆位元組的記憶體。所以我們把它們池化線上程池中並重復使用。但即使在那裡,如果我們有太多請求,我們也會遇到一種情況,即執行緒池中的所有執行緒都不可用。它們都卡在等待某個請求完成。嗯,可以說是這樣。許多執行緒只是在那裡,等待下一個 byte 從 InputStream 中讀取,但它們卻不可用。
執行緒被阻塞了。它們可能正在等待來自客戶端的資料。不幸的現狀是,伺服器等待這些資料,別無選擇,只能坐在那裡,被一個執行緒佔用,不允許任何人使用它。
直到現在。Java 21 引入了一種新的執行緒型別,即虛擬執行緒。現在,我們可以為堆建立數百萬個執行緒。這很容易。但根本上,事實是實際的執行緒(虛擬執行緒在其上執行)是昂貴的。那麼,JRE 如何讓我們擁有數百萬個執行緒來處理實際工作呢?它擁有一個經過大幅改進的執行時,該執行時現在注意到我們在何時阻塞,並暫停執行緒的執行,直到我們等待的東西到達。然後,它會悄悄地將我們放回另一個執行緒。實際執行緒充當虛擬執行緒的載體,允許我們啟動數百萬個執行緒。
Java 21 在所有歷史上會阻塞執行緒的地方都進行了改進,例如阻塞 I/O(使用 InputStream 和 OutputStream)以及 Thread.sleep,因此現在它們可以正確地向執行時發出訊號,表明可以回收執行緒並將其重新用於其他虛擬執行緒,即使虛擬執行緒“阻塞”也能讓工作繼續進行。你可以在這個例子中看到,我厚顏無恥地剽竊了 Oracle 的 Java 開發倡導者之一 José Paumard 的作品,他的工作我很喜歡。
package bootiful.java21;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.io.ByteArrayOutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;
class LoomTest {
@Test
void loom() throws Exception {
var observed = new ConcurrentSkipListSet<String>();
var threads = IntStream
.range(0, 100)
.mapToObj(index -> Thread.ofVirtual() // ①
.unstarted(() -> {
var first = index == 0;
if (first) {
observed.add(Thread.currentThread().toString());
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (first) {
observed.add(Thread.currentThread().toString());
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (first) {
observed.add(Thread.currentThread().toString());
}
try {
Thread.sleep(20);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
if (first) {
observed.add(Thread.currentThread().toString());
}
}))
.toList();
for (var t : threads)
t.start();
for (var t : threads)
t.join();
System.out.println(observed);
Assertions.assertTrue(observed.size() > 1);
}
}
這個例子啟動了大量執行緒,以至於產生了爭用,並且需要共享作業系統載體執行緒。然後它會導致執行緒 sleep。通常,休眠會阻塞,但在虛擬執行緒中不會。
我們將取樣其中一個執行緒(第一個啟動的執行緒),在每次休眠之前和之後,以記錄我們的虛擬執行緒在每次休眠之前和之後執行的載體執行緒的名稱。請注意,它們已經改變了!執行時已將我們的虛擬執行緒移動到不同的載體執行緒上,而我們的程式碼沒有任何改變!這就是 Project Loom 的魔力。幾乎(原諒這個雙關語)無需更改程式碼,即可大大提高可伸縮性(執行緒重用),與你可能僅透過響應式程式設計才能獲得的效果相媲美。
我們的網路服務怎麼樣?確實需要一項更改。但這只是一個基本更改。像這樣替換執行緒池:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
...
}
其他一切都保持不變,現在我們獲得了無與倫比的規模!Spring Boot 應用程式通常有很多 Executor 例項用於各種事情,例如整合、訊息傳遞、Web 服務等。如果你使用的是 Spring Boot 3.2(將於 2023 年 11 月釋出)和 Java 21,那麼你可以使用這個新屬性,Spring Boot 將自動為你配置虛擬執行緒池!很棒。
spring.threads.virtual.enabled=true
Java 21 是一個巨大的進步。它提供了與許多現代語言相媲美的語法,並且可伸縮性與許多現代語言相當甚至更好,而無需透過非同步/等待、響應式程式設計等複雜方式來使程式碼複雜化。
如果你需要原生映象,還有一個 GraalVM 專案,它為 Java 21 提供了一個預編譯器 (AOT)。你可以使用 GraalVM 將高度可伸縮的 Boot 應用程式編譯為 GraalVM 原生映象,這些映象幾乎可以立即啟動,並且佔用的記憶體比在 JVM 上執行時少得多。這些應用程式還受益於 Project Loom 的優美之處,使其獲得無與倫比的可伸縮性。
./gradlew nativeCompile
太棒了!現在我們有了一個小的二進位制檔案,它啟動速度極快,佔用的記憶體極少,並且可伸縮性與最可伸縮的執行時相當。恭喜!你是一名 Java 開發者,而且成為 Java 開發者的好時機從未如此合適!