领先一步
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 开发者最好的时代!