Spring Boot 3.0橫空出世,快來看看是不是該升級(jí)了
簡(jiǎn)介
Spring boot 3.0于2022年11月正式發(fā)布了,這次的發(fā)布對(duì)于我們普通程序員的影響有多少呢?我們是不是需要考慮立馬升級(jí)到Spring Boot3.0呢?
別急,看完這篇文章再來做決定也不遲。
對(duì)JAVA17和JAVA19的支持
相信很多小伙伴到現(xiàn)在還是使用得是JDK8,但是JDK8已經(jīng)發(fā)布很多年了,隨著oracle加速JDK版本的發(fā)布,現(xiàn)在每半年發(fā)布一次,目前最新的JDK版本已經(jīng)到了19了。其中JDK11和JDK17是LTS版本,也就是說我們常說的穩(wěn)定版本。
鑒于JDK17帶來的很多新特性,Spring boot的最低JDK版本支持已經(jīng)提升到了JDK17,如果你還在使用JDK8或者JDK11的話,那么首先需要把JDK版本升級(jí)到17才能夠使用Spring Boot 3.0。
很多小伙伴可能不是很清楚JDK17到底有些什么新的特性或者功能,這里再給大家詳細(xì)介紹一下。
record
首先是在JDK14的時(shí)候引入了record這個(gè)關(guān)鍵詞,Record是一種輕量級(jí)的class,可以看做是數(shù)據(jù)結(jié)構(gòu)體。和scala中的case有點(diǎn)相似。
舉個(gè)自定義User的例子看一下Record是怎么用的:
public record Address(
String addressName,
String city
) {
}
public record CustUser(
String firstName,
String lastName,
Address address,
int age
) {}
上面我們定義了兩個(gè)類,CustUser和Address。CustUser中引用了Address。
Record和普通的類的區(qū)別就在于Record多了一個(gè)括號(hào)括起來的定義的字段。
Record類默認(rèn)是final的,里面的字段默認(rèn)是private final的。
要想知道Record到底是怎么工作的,我們可以使用javap來對(duì)編譯好的class文件反編譯,運(yùn)行javap CustUser,可以得到下面的結(jié)果:
警告: 二進(jìn)制文件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。
并且自動(dòng)添加了默認(rèn)帶有所有字段的構(gòu)造函數(shù)。各個(gè)自動(dòng)的獲取方法,并實(shí)現(xiàn)了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的,只能在初始化的時(shí)候設(shè)置。并且方法里面也沒有提供其他可以改變字段內(nèi)容的方法。
Text Blocks
Text Blocks是在JDK13中以第一次預(yù)覽版本引入的。現(xiàn)在在JDK14中是第二次預(yù)覽版本 JEP 368: Text Blocks。
在我們?nèi)粘5墓ぷ髦?,有時(shí)候需要用到一大段的字符串,這些字符串需要換行,需要排版,需要轉(zhuǎn)義。在一個(gè)文本編輯器中,這當(dāng)然是非常容易的事情。但是在java代碼中,就是一個(gè)噩夢(mèng)了。
雖然IDE可以自動(dòng)幫我們加上換行甚至可以對(duì)字符串進(jìn)行拼接。但在java程序眼中,添加的諸多額外的代碼破壞了代碼的美感。是任何一個(gè)有潔癖的程序員都無法忍受的。
怎么辦? Text Blocks就是來解救大家的。
我們先來個(gè)直觀的例子,然后再分析Text Blocks的特點(diǎn)。
還是舉HTML的例子,如果我們想要打印出帶縮減,有格式的html,傳統(tǒng)方法可以這樣做:
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>
""";
是不是清爽很多,想要立即給文本塊點(diǎn)個(gè)贊。
別慌點(diǎn)贊,我們還有更多的東西要討論。
可能有人又有問題了,文本塊好用是好用,你這輸出結(jié)果中,字段前面的空格都去哪了了呀?
這里就要介紹這個(gè)概念了:英文名字叫Indentation,中文我把它翻譯為編排。
再看一下上面的代碼,這一次我們把代碼前面的空格以點(diǎn)來表示:
String html = """
..............<html>
.............. <body>
.............. <p>Hello, world</p>
.............. </body>
..............</html>
..............""";
Indentation的規(guī)則就是以最下面的“”“為界,對(duì)每一行都移除相同數(shù)量的空格。
上面的代碼輸出:
<html>
<body>
<p>Hello, world</p>
</body>
</html>
上面的例子,最下面的”“”剛好在最左邊的位置,如果把“”“向右移動(dòng)4個(gè)空格會(huì)發(fā)生什么呢?
String html = """
..............<html>
.............. <body>
.............. <p>Hello, world</p>
.............. </body>
..............</html>
..................""";
輸出結(jié)果:
<html>
<body>
<p>Hello, world</p>
</body>
</html>
我們看到輸出結(jié)果是不變的,這樣我們又得到一條結(jié)論:如果”“”向右移動(dòng),則以text block中最左的那一行記錄為準(zhǔn)。
如果我們把“”“向左移動(dòng)四位,就會(huì)發(fā)現(xiàn)最終的輸出結(jié)果每行前面都有四個(gè)空格。
這個(gè)功能是和String添加的新的String::stripIndent()對(duì)應(yīng)的。
Switch Expressions
switch的新特性可是源遠(yuǎn)流長(zhǎng),早在JDK 12就以預(yù)覽功能被引入了,最終在JDK 14成為了正式版本的功能:JEP 361: Switch Expressions (Standard)。
其實(shí)Switch新增的功能有兩個(gè),一個(gè)就是可以連寫case,一個(gè)就是switch可以帶返回值了。
先看一個(gè)老版本的例子:
@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;
}
}
上面的例子中,我們想要匹配所有的星期,然后打印出相應(yīng)的結(jié)果。寫了很多個(gè)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);
}
}
一個(gè)漂亮的連寫,將一切都帶走。
注意這里switch語句沒有返回值,所以并不需要default語句。
考慮一個(gè)在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("這天沒發(fā)見人!");
}
}
傳統(tǒng)方式我們需要定義一個(gè)局部變量,并在case中給這個(gè)局部變量賦值。
我們看下怎么使用新版的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("這天沒發(fā)見人!");
};
}
是不是非常簡(jiǎn)單。
注意,這里需要一個(gè)default操作,否則會(huì)報(bào)編譯錯(cuò)誤。因?yàn)榭赡艽嬖谖幢闅v的值。
上面的switch返回值的情況,如果case后面的表達(dá)式比較復(fù)雜,那么就需要使用大括號(hào)來圍起來。這種情況我們需要使用到y(tǒng)ield來返回要返回的值。
@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模式匹配
怎么理解呢?
我們先舉個(gè)歷史版本中使用instanceof的例子。
假如我們是動(dòng)物園的管理員,動(dòng)物園里面有Girraffe和Hippo兩種動(dòng)物。
@Data
public class Girraffe {
private String name;
}
@Data
public class Hippo {
private String name;
}
為了簡(jiǎn)單起見,上面兩種動(dòng)物我們都之定義一個(gè)name屬性。
接下來我們要對(duì)兩種動(dòng)物進(jìn)行管理,傳入一個(gè)動(dòng)物,判斷一下這個(gè)動(dòng)物是不是上面兩種動(dòng)物之一,按照傳統(tǒng)的辦法,我們應(yīng)該這樣做:
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("對(duì)不起,該動(dòng)物不是地球上的生物!");
}
上面的代碼中, 如果instanceof確認(rèn)成功,我們還需要將對(duì)象進(jìn)行轉(zhuǎn)換,才能調(diào)用相應(yīng)對(duì)象中的方法。
有了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("對(duì)不起,該動(dòng)物不是地球上的生物!");
}
注意instanceof的用法,通過instanceof的模式匹配,就不需要二次轉(zhuǎn)換了。直接使用就可以了。并且模式匹配的對(duì)象還被限定了作用域范圍,會(huì)更加安全。
Sealed Classes and Interfaces
在Java中,類層次結(jié)構(gòu)通過繼承實(shí)現(xiàn)代碼的重用,父類的方法可以被許多子類繼承。
但是,類層次結(jié)構(gòu)的目的并不總是重用代碼。有時(shí),其目的是對(duì)域中存在的各種可能性進(jìn)行建模,例如圖形庫支持的形狀類型或金融應(yīng)用程序支持的貸款類型。
當(dāng)以這種方式使用類層次結(jié)構(gòu)時(shí),我們可能需要限制子類集從而來簡(jiǎn)化建模。
因?yàn)槲覀円肓藄ealed class或interfaces,這些class或者interfaces只允許被指定的類或者interface進(jìn)行擴(kuò)展和實(shí)現(xiàn)。
舉個(gè)例子:
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.*。
舉個(gè)例子HttpServletRequest需要從:
import javax.servlet.http.HttpServletRequest;
替換成為:
import jakarta.servlet.http.HttpServletRequest;
GraalVM Native Image Support
Spring Boot3的一個(gè)非常大的功能點(diǎn)就是可以利用Spring的AOT技術(shù),將spring boot的應(yīng)用編譯成為native的image,從而大大提升系統(tǒng)的運(yùn)行效率。
比如,我們可以這樣添加一個(gè)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>
然后運(yùn)行下面的命令就可以把spring boot項(xiàng)目打包成native項(xiàng)目了:
mvn clean package -Pnative
對(duì)Micrometer的支持
在spring boot3中默認(rèn)提供了對(duì)Micrometer 1.10的支持,spring boot會(huì)自動(dòng)幫你配置一個(gè)ObservationRegistry的實(shí)例。
Micrometer可以用來收集應(yīng)用程序各項(xiàng)指標(biāo)數(shù)據(jù),從而實(shí)現(xiàn)對(duì)應(yīng)用程序的各種監(jiān)控。
其他的一些改動(dòng)
當(dāng)然,除了上面的主要的變化之外,Spring boot3還提供了其他的一些小的調(diào)整,大家感興趣的話可以親自升級(jí)到spring boot3嘗試一下。
作者:程序那些事
歡迎關(guān)注微信公眾號(hào) :程序那些事