抢先一步
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 原生镜像?好吧,它 *很快* 并且超级资源高效。传统上,这个真理总是会遭到反驳:“是的,好吧,在普通的 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 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');
}
}
没什么太令人惊讶的。易于理解。三个引号开始和结束多行字符串。您还可以去除前导、尾随和缩进空格。
记录是我最喜欢的 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
*与* 模式匹配一起使用。注意经典版本有多么笨拙。Ugh。我很高兴摆脱了它。另一件好事是,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中还有许多其他新特性。有很多小而实用的功能,当然还有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
如果您正在进行异步编程(是的,即使在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是您可能希望在将来将异步操作包装起来并使用Loom项目的地方。HTTP客户端API自Java 11以来就已存在,现在已经发布了十个版本!但是,现在它有了这个漂亮的新自动关闭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
中是否存在表情符号的方法。
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));
}
}
}
这是非常简单的网络101知识。创建一个ServerSocket
,并等待新的客户端(由Socket
实例表示)出现。当每个客户端到达时,将其传递给线程池中的一个线程。每个线程从客户端Socket
实例的InputStream
引用中读取数据。客户端可能会断开连接、遇到延迟或有大量数据要发送,所有这些都是问题,因为可用的线程数量有限,我们不能浪费宝贵的时间在它们上面。
我们使用线程来避免请求积压,我们无法足够快地处理这些请求。但在这里,我们再次失败了,因为在Java 21之前,线程是昂贵的!每个Thread
大约需要2MB的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 应用程序通常会为各种事物使用许多Executor
实例,例如集成、消息传递、Web 服务等。如果您使用的是将于 2023 年 11 月发布的 Spring Boot 3.2 和 Java 21,那么您可以使用此新属性,Spring Boot 会自动为您插入虚拟线程池!很不错。
spring.threads.virtual.enabled=true
Java 21 意义重大。它提供的语法与许多更现代的语言相当,并且可扩展性与许多现代语言一样好或更好,而无需使用异步/等待、反应式编程等复杂代码。
如果您想要一个原生镜像,还可以使用 GraalVM 项目,该项目为 Java 21 提供了一个提前 (AOT) 编译器。您可以使用 GraalVM 将高度可扩展的 Boot 应用程序编译成 GraalVM 原生镜像,这些镜像启动速度非常快,并且仅占用 JVM 上一小部分 RAM。这些应用程序还可以从 Project Loom 的优势中受益,从而获得无与伦比的扩展性。
./gradlew nativeCompile
不错!我们现在获得了一个小型二进制文件,它可以在很短的时间内启动,占用很少的 RAM,并且扩展性与最具扩展性的运行时一样好。恭喜!您是一名 Java 开发人员,现在是成为 Java 开发人员的最佳时机!