領先一步
VMware 提供培訓和認證,以加速您的進展。
了解更多Spring 粉絲們,大家好!
在我們開始之前,請快速為我做一件事。 如果您還沒有這樣做,請 安裝 SKDMAN。
然後執行
sdk install java 21-graalce && sdk default java 21-graalce
您成功了。 現在您的機器上已經安裝了 Java 21 和支援 Java 21 的 graalvm,可以開始使用了。 我認為 Java 21 是 Java 最關鍵的版本,或許是有史以來最重要的,因為它暗示了使用 Java 的人們會擁有一個全新的機會世界。 它帶來了許多不錯的 API 和新增功能,例如模式匹配,這是多年來緩慢而穩定地添加到平台的功能的高潮。 但到目前為止,最突出的功能是虛擬執行緒(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');
}
}
沒什麼太令人驚訝的。 容易理解。 三引號開始和停止多行字串。 您也可以去除前導、尾隨和縮排空間。
Records 是 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
語句,因為它很笨拙,而且通常還有其他模式,例如 Visitor Pattern,它可以讓我獲得大部分好處。 現在有一個新的 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 平台中模式比對的第一次真正嘗試。 模式比對背後的想法很簡單:比對類型並從這些類型中提取資料。
從技術上來說,密封型別(Sealed Types)也是 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 會是什麼感覺。當與 records 結合使用時,模式匹配具有更友好的互動,因為 records(如前所述)是其組件的簡歷,而編譯器知道這一點。因此,它也可以將這些組件提升到新的變數中。您也可以在 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 在這裡有一些不錯的改進,包括 BigIntegers 的並行乘法以及各種除法重載,如果發生溢位,會拋出例外。而不僅僅是如果發生除以零的錯誤。
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 以來就存在,現在已經是遙遠過去的整整 10 個版本!但是,現在它有了這個精美的新自動關閉 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 請求,則除非小心謹慎地僅在所有線程完成執行*後*才讓它到達範圍的末尾,否則您*不應*使用自動關閉。我在該範例中使用 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。小的生活品質改進,我同意,但仍然很好。
您需要一個排序的集合來對那些 String
實例進行排序。Java 提供了其中的一些,LinkedHashMap
、List
等,但它們沒有共同的祖先。現在他們有了;歡迎,SequencedCollection
!在此範例中,我們使用一個簡單的 ArrayList<String>
,並使用花哨的新 factory 方法來建立像 LinkedHashSet
這樣的東西。這個新的 factory 方法在內部進行了一些數學運算,以保證在您添加的元素數量達到構造函式中規定的數量之前,它不必重新平衡(從而緩慢地重新雜湊所有內容)。
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));
}
}
}
這是非常基礎的 Networking-101 內容。建立一個 ServerSocket
,並等待新客戶端 (由 Socket
的實例表示) 出現。每當有客戶端抵達時,將其移交給執行緒池中的一個執行緒。每個執行緒從客戶端 Socket
實例的 InputStream
參考讀取資料。客戶端可能會斷線、遇到延遲或有大量的資料要發送,所有這些都是問題,因為可用的執行緒數量有限,我們絕不能將寶貴的時間浪費在它們身上。
我們使用執行緒是為了避免無法快速處理的請求堆積。但再次地,我們失敗了,因為在 Java 21 之前,執行緒的成本很高!每個 Thread
大約需要兩兆位元組的 RAM。因此,我們將它們集中在一個執行緒池中並重複使用它們。但即使在那裡,如果我們有太多的請求,我們最終也會陷入一種情況,即執行緒池中沒有任何執行緒可用。它們都卡在等待某個請求完成。嗯,可以這麼說。許多執行緒只是坐在那裡,等待從 InputStream
傳來的下一個 byte
,但它們無法使用。
執行緒被阻塞了。它們可能正在等待來自客戶端的資料。不幸的是,伺服器正在等待該資料,別無選擇,只能坐在那裡,停在一個執行緒上,不允許其他人使用它。
直到現在,情況才有所改變。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 應用程式通常會為各種事情 (例如整合、消息傳遞、Web 服務等) 部署許多 Executor
實例。如果您使用的是將於 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 開發人員的最佳時機!