领先一步
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 开发者的好时机从未如此合适!