Spring Boot 3.0橫空出世,快來看看是不是該升級了

語言: CN / TW / HK

簡介

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 = "\n" + " \n" + "

Hello, world

\n" + " \n" + "\n"; ~~~

上面的代碼看着特別彆扭,讓我們看看用文本塊方式怎麼做:

~~~ String html = """

Hello, world

"""; ~~~

是不是清爽很多,想要立即給文本塊點個贊。

別慌點贊,我們還有更多的東西要討論。

可能有人又有問題了,文本塊好用是好用,你這輸出結果中,字段前面的空格都去哪了了呀?

這裏就要介紹這個概念了:英文名字叫Indentation,中文我把它翻譯為編排。

再看一下上面的代碼,這一次我們把代碼前面的空格以點來表示:

~~~ String html = """ .............. .............. ..............

Hello, world

.............. .............. .............."""; ~~~

Indentation的規則就是以最下面的“”“為界,對每一行都移除相同數量的空格。

上面的代碼輸出:

~~~

Hello, world

~~~

上面的例子,最下面的”“”剛好在最左邊的位置,如果把“”“向右移動4個空格會發生什麼呢?

~~~ String html = """ .............. .............. ..............

Hello, world

.............. .............. .................."""; ~~~

輸出結果:

~~~java

Hello, world

~~~

我們看到輸出結果是不變的,這樣我們又得到一條結論:如果”“”向右移動,則以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

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程序那些事」,懂技術,更懂你!