領先一步
VMware 提供培訓和認證,為你的進步加速。
瞭解更多各位 Spring 愛好者,大家好!
在開始之前,請快速幫我一個忙。如果你還沒有安裝,請去安裝 SKDMAN。
然後執行
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 本機映像?嗯,因為它快而且超級資源高效。傳統上,這個論點總是有一個反駁:“好吧,老式 Java 的 JIT 還是更快,”對此我會反駁說,“好吧,你可以在佔用資源極少的情況下更容易地擴充套件新的例項,以彌補你可能損失的吞吐量,而且在資源消耗方面仍然具有優勢!” 這確實是真的。
但現在我們甚至不必進行那種微妙的討論了。根據 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
。instance of
檢查來更簡潔地處理每種可能的型別,但在這裡我們得不到編譯器的幫助。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
中是否存在 Emoji 的方法。
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
中的 Emoji。我同意,這些都是微小的生活質量改進,但 nonetheless 還是不錯的。
你需要一個有序集合來對這些 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
大約消耗兩兆位元組的 RAM。所以我們將它們放入執行緒池中重複使用。但即便如此,如果請求過多,我們最終會陷入執行緒池中沒有可用執行緒的情況。它們都卡在那裡,等待某個請求完成。嗯,差不多是這樣。許多執行緒只是坐在那裡,等待 InputStream
中的下一個位元組,但它們卻無法使用。
執行緒被阻塞了。它們可能正在等待來自客戶端的資料。不幸的是,伺服器等待資料時,別無選擇,只能坐在那裡,停駐在一個執行緒上,不允許其他人使用它。
直到現在。Java 21 引入了一種新的執行緒,即虛擬執行緒。現在,我們可以為堆建立數百萬個執行緒。這很容易。但從根本上說,實際情況是執行虛擬執行緒的物理執行緒仍然很昂貴。那麼,JRE 如何讓我們擁有數百萬個用於實際工作的執行緒呢?它擁有一個極大地改進的執行時,現在可以檢測到我們在何時阻塞並在執行緒上暫停執行,直到我們等待的東西到來。然後,它悄悄地將我們放回另一個執行緒上。實際執行緒充當虛擬執行緒的載體,允許我們啟動數百萬個執行緒。
Java 21 在所有過去會阻塞執行緒的地方都進行了改進,例如使用 InputStream
和 OutputStream
進行阻塞式 IO 以及 Thread.sleep
,因此現在它們可以正確地向執行時發出訊號,表示可以回收該執行緒並將其用於其他虛擬執行緒,從而即使虛擬執行緒“阻塞”也能讓工作繼續進行。你可以在這個例子中看到這一點,這個例子是我厚顏無恥地從 José Paumard 那裡“偷”來的,他是 Oracle 的一位 Java 開發者佈道師,我很喜歡他的工作。
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);
}
}
factory
方法。這個例子啟動了大量的執行緒,以至於造成競爭,需要共享作業系統載體執行緒。然後它讓執行緒進入 sleep
狀態。睡眠通常會阻塞,但在虛擬執行緒中不會。
我們將在每次睡眠前後對其中一個執行緒(啟動的第一個執行緒)進行取樣,以記錄我們的虛擬執行緒在每次睡眠前後執行的載體執行緒名稱。注意它們已經改變了!執行時在不同的載體執行緒之間移動了我們的虛擬執行緒,而我們的程式碼沒有做任何改變!這就是 Project Loom 的魔力。幾乎(請原諒這個雙關語)沒有程式碼更改,並且大大提高了可伸縮性(執行緒重用),與你否則只能透過響應式程式設計等方式獲得的可伸縮性相當。
我們的網路服務呢?我們確實需要進行一個改變。但這只是一個基礎的改變。像這樣替換掉執行緒池:
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
...
}
其他一切都保持不變,現在我們獲得了無與倫比的可伸縮性!Spring Boot 應用程式通常會使用大量的 Executor
例項來處理各種事情,比如整合、訊息傳遞、Web 服務等。如果你正在使用 2023 年 11 月即將釋出的 Spring Boot 3.2 和 Java 21,那麼你可以使用這個新的屬性,Spring Boot 將自動為你插入虛擬執行緒池!太棒了。
spring.threads.virtual.enabled=true
Java 21 是一個重大事件。它提供的語法可以與許多更現代的語言媲美,並且其可伸縮性與許多現代語言一樣好甚至更好,而無需使用 async/await、響應式程式設計等方式來複雜化程式碼。
如果你想要本機映像,還有一個 GraalVM 專案,它為 Java 21 提供了預先(AOT)編譯器。你可以使用 GraalVM 將你的高度可伸縮的 Boot 應用程式編譯成 GraalVM 本機映像,這些映像幾乎可以立即啟動,並且佔用的 RAM 是在 JVM 上執行時的一小部分。這些應用程式還受益於 Project Loom 的優勢,賦予它們無與倫比的可伸縮性。
./gradlew nativeCompile
太棒了!現在我們有了一個小的二進位制檔案,啟動速度非常快,佔用的 RAM 非常少,並且具有與最具可伸縮性的執行時相當的可伸縮性。恭喜你!你是一名 Java 開發者,現在是成為一名 Java 開發者最好的時代!