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

flydean發表於2023-01-09

簡介

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

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

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

相關文章