简介
Spring boot 3.0 于 2022 年 11 月正式发布了,这次的发布对于我们普通程序员的影响有多少呢?我们是不是需要考虑立马升级到 Spring Boot3.0 呢?
别急,看完这篇文章再来做决定也不迟。
对 JAVA17 和 JAVA19 的支持
相信很多小伙伴到现在还是使用得是 JDK8,但是 JDK8 已经发布很多年了,随着 oracle 加速 JDK 版本的发布,现在每半年发布一次,目前最新的 JDK 版本已经到了 19 了。其中 JDK11 和 JDK17 是 LTS 版本,也就是说我们常说的稳定版本。
鉴于 JDK17 带来的很多新特性,Spring boot 的最低 JDK 版本支持已经提升到了 JDK17,如果你还在使用 JDK8 或者 JDK11 的话,那么首先需要把 JDK 版本升级到 17 才能够使用 Spring Boot 3.0。
很多小伙伴可能不是很清楚 JDK17 到底有些什么新的特性或者功能,这里再给大家详细介绍一下。
record
首先是在 JDK14 的时候引入了 record 这个关键词,Record 是一种轻量级的 class,可以看做是数据结构体。和 scala 中的 case 有点相似。
举个自定义 User 的例子看一下 Record 是怎么用的:
public record Address(
String addressName,
String city
) {
}
复制代码
public record CustUser(
String firstName,
String lastName,
Address address,
int age
) {}
复制代码
上面我们定义了两个类,CustUser 和 Address。CustUser 中引用了 Address。
Record 和普通的类的区别就在于 Record 多了一个括号括起来的定义的字段。
Record 类默认是 final 的,里面的字段默认是 private final 的。
要想知道 Record 到底是怎么工作的,我们可以使用 javap 来对编译好的 class 文件反编译,运行 javap CustUser,可以得到下面的结果:
警告: 二进制文件CustUser包含com.flydean.records.CustUser
Compiled from "CustUser.java"
public final class com.flydean.records.CustUser extends java.lang.Record {
public com.flydean.records.CustUser(java.lang.String, java.lang.String, com.flydean.records.Address, int);
public java.lang.String toString();
public final int hashCode();
public final boolean equals(java.lang.Object);
public java.lang.String firstName();
public java.lang.String lastName();
public com.flydean.records.Address address();
public int age();
}
复制代码
上面可以看到 final class CustUser 继承自 java.lang.Record。
并且自动添加了默认带有所有字段的构造函数。各个自动的获取方法,并实现了 toString,hashCode 和 equals 方法。
天啦,太完美了,我们想要的它居然都有。
如果上面的 javap 还不是很清楚的话,大家可以借助 IDE 的反编译功能,打开 CustUser.class 文件看一看:
public final class CustUser extends java.lang.Record {
private final java.lang.String firstName;
private final java.lang.String lastName;
private final com.flydean.records.Address address;
private final int age;
public CustUser(java.lang.String firstName, java.lang.String lastName, com.flydean.records.Address address, int age) { /* compiled code */ }
public java.lang.String toString() { /* compiled code */ }
public final int hashCode() { /* compiled code */ }
public final boolean equals(java.lang.Object o) { /* compiled code */ }
public java.lang.String firstName() { /* compiled code */ }
public java.lang.String lastName() { /* compiled code */ }
public com.flydean.records.Address address() { /* compiled code */ }
public int age() { /* compiled code */ }
}
复制代码
注意,上面的反编译我们可以看到,record 中的所有字段都是 final 的,只能在初始化的时候设置。并且方法里面也没有提供其他可以改变字段内容的方法。
Text Blocks
Text Blocks 是在 JDK13 中以第一次预览版本引入的。现在在 JDK14 中是第二次预览版本 JEP 368: Text Blocks。
在我们日常的工作中,有时候需要用到一大段的字符串,这些字符串需要换行,需要排版,需要转义。在一个文本编辑器中,这当然是非常容易的事情。但是在 java 代码中,就是一个噩梦了。
虽然 IDE 可以自动帮我们加上换行甚至可以对字符串进行拼接。但在 java 程序眼中,添加的诸多额外的代码破坏了代码的美感。是任何一个有洁癖的程序员都无法忍受的。
怎么办? Text Blocks 就是来解救大家的。
我们先来个直观的例子,然后再分析 Text Blocks 的特点。
还是举 HTML 的例子,如果我们想要打印出带缩减,有格式的 html,传统方法可以这样做:
String html = "<html>\n" +
" <body>\n" +
" <p>Hello, world</p>\n" +
" </body>\n" +
"</html>\n";
复制代码
上面的代码看着特别别扭,让我们看看用文本块方式怎么做:
String html = """
<html>
<body>
<p>Hello, world</p>
</body>
</html>
""";
复制代码
是不是清爽很多,想要立即给文本块点个赞。
别慌点赞,我们还有更多的东西要讨论。
可能有人又有问题了,文本块好用是好用,你这输出结果中,字段前面的空格都去哪了了呀?
这里就要介绍这个概念了:英文名字叫 Indentation,中文我把它翻译为编排。
再看一下上面的代码,这一次我们把代码前面的空格以点来表示:
String html = """
..............<html>
.............. <body>
.............. <p>Hello, world</p>
.............. </body>
..............</html>
..............""";
复制代码
Indentation 的规则就是以最下面的“”“为界,对每一行都移除相同数量的空格。
上面的代码输出:
<html>
<body>
<p>Hello, world</p>
</body>
</html>
复制代码
上面的例子,最下面的”“”刚好在最左边的位置,如果把“”“向右移动 4 个空格会发生什么呢?
String html = """
..............<html>
.............. <body>
.............. <p>Hello, world</p>
.............. </body>
..............</html>
..................""";
复制代码
输出结果:
<html>
<body>
<p>Hello, world</p>
</body>
</html>
复制代码
我们看到输出结果是不变的,这样我们又得到一条结论:如果”“”向右移动,则以 text block 中最左的那一行记录为准。
如果我们把“”“向左移动四位,就会发现最终的输出结果每行前面都有四个空格。
这个功能是和 String 添加的新的 String::stripIndent()对应的。
Switch Expressions
switch 的新特性可是源远流长,早在 JDK 12 就以预览功能被引入了,最终在 JDK 14 成为了正式版本的功能:JEP 361: Switch Expressions (Standard)。
其实 Switch 新增的功能有两个,一个就是可以连写 case,一个就是 switch 可以带返回值了。
先看一个老版本的例子:
@Test
public void useOldSwitch(){
switch (MONDAY) {
case MONDAY:
case FRIDAY:
case SUNDAY:
System.out.println(6);
break;
case TUESDAY:
System.out.println(7);
break;
case THURSDAY:
case SATURDAY:
System.out.println(8);
break;
case WEDNESDAY:
System.out.println(9);
break;
}
}
复制代码
上面的例子中,我们想要匹配所有的星期,然后打印出相应的结果。写了很多个 case 语句,不美观。
再看一下新版本的例子:
@Test
public void useNewSwitch(){
switch (MONDAY) {
case MONDAY, FRIDAY, SUNDAY -> System.out.println(6);
case TUESDAY -> System.out.println(7);
case THURSDAY, SATURDAY -> System.out.println(8);
case WEDNESDAY -> System.out.println(9);
}
}
复制代码
一个漂亮的连写,将一切都带走。
注意这里 switch 语句没有返回值,所以并不需要 default 语句。
考虑一个在 switch 中赋值的情况:
@Test
public void oldSwitchWithReturnValue(){
int numLetters;
switch (MONDAY) {
case MONDAY:
case FRIDAY:
case SUNDAY:
numLetters = 6;
break;
case TUESDAY:
numLetters = 7;
break;
case THURSDAY:
case SATURDAY:
numLetters = 8;
break;
case WEDNESDAY:
numLetters = 9;
break;
default:
throw new IllegalStateException("这天没发见人!");
}
}
复制代码
传统方式我们需要定义一个局部变量,并在 case 中给这个局部变量赋值。
我们看下怎么使用新版的 switch 替换:
@Test
public void newSwitchWithReturnValue(){
int numLetters = switch (MONDAY) {
case MONDAY, FRIDAY, SUNDAY -> 6;
case TUESDAY -> 7;
case THURSDAY, SATURDAY -> 8;
case WEDNESDAY -> 9;
default -> throw new IllegalStateException("这天没发见人!");
};
}
复制代码
是不是非常简单。
注意,这里需要一个 default 操作,否则会报编译错误。因为可能存在未遍历的值。
上面的 switch 返回值的情况,如果 case 后面的表达式比较复杂,那么就需要使用大括号来围起来。这种情况我们需要使用到 yield 来返回要返回的值。
@Test
public void withYield(){
int result = switch (MONDAY) {
case MONDAY: {
yield 1;
}
case TUESDAY: {
yield 2;
}
default: {
System.out.println("不是MONDAY,也不是TUESDAY!");
yield 0;
}
};
}
复制代码
instanceof 模式匹配
怎么理解呢?
我们先举个历史版本中使用 instanceof 的例子。
假如我们是动物园的管理员,动物园里面有 Girraffe 和 Hippo 两种动物。
@Data
public class Girraffe {
private String name;
}
复制代码
@Data
public class Hippo {
private String name;
}
复制代码
为了简单起见,上面两种动物我们都之定义一个 name 属性。
接下来我们要对两种动物进行管理,传入一个动物,判断一下这个动物是不是上面两种动物之一,按照传统的办法,我们应该这样做:
public void testZooOld(Object animal){
if(animal instanceof Girraffe){
Girraffe girraffe = (Girraffe) animal;
log.info("girraffe name is {}",girraffe.getName());
}else if(animal instanceof Hippo){
Hippo hippo = (Hippo) animal;
log.info("hippo name is {}",hippo.getName());
}
throw new IllegalArgumentException("对不起,该动物不是地球上的生物!");
}
复制代码
上面的代码中, 如果 instanceof 确认成功,我们还需要将对象进行转换,才能调用相应对象中的方法。
有了 JDK 14,一切都变得容易了,我们看下最新的 JDK 14 的模式匹配怎么做:
public void testZooNew(Object animal){
if(animal instanceof Girraffe girraffe){
log.info("name is {}",girraffe.getName());
}else if(animal instanceof Hippo hippo){
log.info("name is {}",hippo.getName());
}
throw new IllegalArgumentException("对不起,该动物不是地球上的生物!");
}
复制代码
注意 instanceof 的用法,通过 instanceof 的模式匹配,就不需要二次转换了。直接使用就可以了。并且模式匹配的对象还被限定了作用域范围,会更加安全。
Sealed Classes and Interfaces
在 Java 中,类层次结构通过继承实现代码的重用,父类的方法可以被许多子类继承。
但是,类层次结构的目的并不总是重用代码。有时,其目的是对域中存在的各种可能性进行建模,例如图形库支持的形状类型或金融应用程序支持的贷款类型。
当以这种方式使用类层次结构时,我们可能需要限制子类集从而来简化建模。
因为我们引入了 sealed class 或 interfaces,这些 class 或者 interfaces 只允许被指定的类或者 interface 进行扩展和实现。
举个例子:
package com.example.geometry;
public abstract sealed class Shape
permits Circle, Rectangle, Square {...}
复制代码
上面的例子中,我们指定了 Shape 只允许被 Circle, Rectangle, Square 来继承。
上面的例子中并没有指定类的包名,我们可以这样写:
package com.example.geometry;
public abstract sealed class Shape
permits com.example.polar.Circle,
com.example.quad.Rectangle,
com.example.quad.simple.Square {...}
复制代码
迁移到 Jakarta EE
除了下面一些 spring 依赖包的更新之外:
Spring Framework 6.0.
Spring AMQP 3.0.
Spring Batch 5.0.
Spring Data 2022.0.
Spring GraphQL 1.1.
Spring HATEOAS 2.0.
Spring Integration 6.0.
Spring Kafka 3.0.
Spring LDAP 3.0.
Spring REST Docs 3.0.
Spring Retry 2.0.
Spring Security 6.0
Spring Session 3.0
Spring WS 4.0.
复制代码
spring boot3 最大的变化就是把 Java EE 迁移到了 Jakarta EE,也就是说我们需要把 javax.* 替换成为 jakarta.*。
举个例子 HttpServletRequest 需要从:
import javax.servlet.http.HttpServletRequest;
复制代码
替换成为:
import jakarta.servlet.http.HttpServletRequest;
复制代码
GraalVM Native Image Support
Spring Boot3 的一个非常大的功能点就是可以利用 Spring 的 AOT 技术,将 spring boot 的应用编译成为 native 的 image,从而大大提升系统的运行效率。
比如,我们可以这样添加一个 native 的 build profile:
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>compile-no-fork</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
复制代码
然后运行下面的命令就可以把 spring boot 项目打包成 native 项目了:
mvn clean package -Pnative
复制代码
对 Micrometer 的支持
在 spring boot3 中默认提供了对 Micrometer 1.10 的支持,spring boot 会自动帮你配置一个 ObservationRegistry 的实例。
Micrometer 可以用来收集应用程序各项指标数据,从而实现对应用程序的各种监控。
其他的一些改动
当然,除了上面的主要的变化之外,Spring boot3 还提供了其他的一些小的调整,大家感兴趣的话可以亲自升级到 spring boot3 尝试一下。
更多内容请参考 www.flydean.com
最通俗的解读,最深刻的干货,最简洁的教程,众多你不知道的小技巧等你来发现!
欢迎关注我的公众号:「程序那些事」,懂技术,更懂你!
评论