聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

xiaoxi666發表於2021-12-11

前言

在 SpringBoot 專案中,我們經常會使用兩種佔位符(有時候還會混用),它們分別是:

  • @*@

  • ${*}

如果我們上網搜尋「SpringBoot 的佔位符 @」,大部分答案會告訴你,SpringBoot 的預設佔位符由 ${*}變成 @*@了,更好一點的答案會引用 SpringBoot官網 中的描述:

On the last point: since the default config files accept Spring style placeholders (${…​}) the Maven filtering is changed to use @..@ placeholders (you can override that with a Maven property resource.delimiter).

於是我們得到了答案,並心安理得地開始使用 @*@佔位符。但如果有探索欲比較強的同學問起:Spring 中的佔位符本來是 ${*},為啥 SpringBoot 中的佔位符就變成 @*@了呢?有時候這兩種佔位符還能混用,這又是為什麼呢?

今天,我們就來一探究竟,這兩種佔位符到底是如何實現的。

場景

首先要說明兩種場景:

  1. 使用 @Value 註解注入屬性時,只能使用 ${*} 佔位符解析。

  2. 處理資原始檔中的屬性時,這兩種佔位符就有點意思了:它們既有可能都有效,還有可能都不生效,甚至你可以擴充套件自己的佔位符!當然這一切都要看你是怎麼配置的。下文會進行詳細描述。

我們先簡單看下第一種場景,@Value 註解的處理屬於 Spring 核心框架邏輯,可以參見 PropertySourcesPlaceholderConfigurer 這個類,最終會執行 ${*} 佔位符的解析。其中的冒號後面可以寫預設值。

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}
聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

由於這種場景不是本文重點,因此不再展開。有興趣的同學可自行探索詳細解析流程。可以參考文章SpringBoot 中 @Value 原始碼解析

下面我們重點看看第二種場景:處理資原始檔中的屬性佔位符。為方便說明,我們搭建一個 Demo 專案。

前置知識

用過 Maven 的同學應該都知道,外掛 maven-resources-plugin 就是用來處理資原始檔的。結合前文中提到的 resource.delimite,我們在 spring-boot-starter-parent 中可以找到對應的配置:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

可以看到 delimiter 是 maven-resources-plugin 外掛中的一個配置項,用於控制佔位符的型別。稍後我們會更改其中的一些配置項進行實驗。 

專案搭建

我們建立一個 SpringBoot Demo 專案,環境資訊如下:

  • spring-boot 2.6.1

  • maven-resources-plugin 3.2.0

我們需要準備一些配置資料,如下所示:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

它們會被 application.properties 引用:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

為進行對比,這裡我們使用了三種佔位符,分別是 Spring 的預設佔位符 ${*}、SpringBoot 的預設佔位符 @*@,以及我隨便寫的一種佔位符 #*#。可以預知的是,預設情況下 #*# 這種佔位符一定不會被解析。

然後我們還需要在 pom.xml 進行配置,確保資源被正確解析:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

此時 pom.xml 的完整內容如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.1</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>resource.placeholder.demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>resource.placeholder.demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
    </dependencies>

    <profiles>
        <profile>
            <id>product</id>
            <properties>
                <env>product</env>
            </properties>
        </profile>
    </profiles>

    <build>
        <filters>
            <!-- 指定配置讀取路徑 -->
            <filter>src/main/filters/${env}.properties</filter>
        </filters>
        <resources>
            <!-- 把資原始檔中的佔位符替換為配置資料 -->
            <resource>
                <directory>src/main/resources</directory>
                <filtering>true</filtering>
                <excludes>
                    <exclude>static/**</exclude>
                </excludes>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

注:上面我們準備了一個非常簡單的配置檔案 product.properties 用於演示。在實際專案中,一般會為不同的 Profile 配置不同的資料,比如除了 product.properties 配置檔案外,還可能會有 dev.properties 等等配置檔案。

現在,我們 build 一下專案,看看 class 中的資原始檔內容:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

很明顯,只有 @*@ 這種佔位符被解析了,而 ${*} 和 #*# 都沒有被解析。

那我們修改一下配置(手動引入 maven-resources-plugin,覆蓋 parent 中的配置),看看會發生什麼:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

Reimport Maven 後,再次 build,看看效果:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

可以發現把 useDefaultDelimiters 改為 true 後, ${*} 佔位符也可以解析了。

那我們繼續改,把 delimite 改成 #,看看 #*# 這種佔位符能否被解析:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

Reimport Maven 後,再次 build,看看效果:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

可以看到,我們自定義的佔位符也可以解析了。

繼續實驗,把 useDefaultDelimiters 改回 false:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

Reimport Maven 後,再次 build,看看效果:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

我們發現,現在只能解析自定義佔位符 #*# 了,而 ${*} 和 @*@ 沒有被解析。

基於上面幾項實驗的結果,我們可以大膽推測,maven-resources-plugin 外掛的:

  • 預設佔位符有兩種,分別是 ${*} 和 @*@

  • 配置項 useDefaultDelimiters,可以控制是否使用預設佔位符

  • 配置項 delimiter,既可以寫預設佔位符,也可以自定義佔位符

好了,現在我們需要到 maven-resources-plugin 外掛中找一下對應的原始碼,驗證上述猜測是否正確。

原始碼解析

首先我們要下載 maven-resources-plugin 的原始碼。URL 為https://archive.apache.org/dist/maven/plugins/

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

在不熟悉原始碼的情況下,我們直接通過關鍵詞 useDefaultDelimiters,定位到關鍵程式碼 org.apache.maven.shared.filtering.AbstractMavenFilteringRequest#setDelimiters,打上斷點進行除錯。

PS:可以參考文章 如何除錯 Maven 原始碼和外掛原始碼 學習 Maven 外掛的除錯方法。具體到本專案,我們可以執行命令 mvnDebug -Pproduct resources:resources 以啟動除錯。其中的 -P 是為了指定 profile,從而能夠找到 ${env}.properties 檔案進行配置資料的讀取。

我們的第一個斷點位於解析 delimiter 的地方:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

進到方法內部看看:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

可以看到邏輯非常簡單:

檢查是否傳入了自定義 delimiters:

  • 如果沒有,setDelimiters 執行將沒有任何效果;也就是說,一定還有預設的值,稍後我們去驗證。

  • 如果有,那麼進行解析(如果為 null,預設使用 ${*} )。同時會判斷 useDefaultDelimiters 是否為 true,若為 true,就把預設 delimiters 加到結果集中。

那麼我們順著找一下預設 delimiters:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

發現是在初始化時設定的。

繼續追蹤,可以看到 delimiters 被解析為佔位符:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

PS:maven-resources-plugin 外掛註釋中有相關說明:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

然後開始逐字元讀取檔案 application.properties,只有發現字元匹配佔位符時才處理:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

由於我們自定義了 delimiter 為 #,並且把 useDefaultDelimiters 置為 false,因此 delimiters 中只有 #*# 這一種佔位符,因此只有 # 這個字元才會被解析。而 ${ 、} 和 @ 都會被無視。

接下來進入 org.codehaus.plexus.interpolation.multi.MultiDelimiterStringSearchInterpolator#interpolate 中,將佔位符替換為配置資料:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

首先獲取即將被解析的佔位符表示式:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

接著獲取可用的佔位符:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

進入方法內部:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

最後解析出配置資料:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

然後回到上層,將佔位符替換為配置資料:

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

到這裡,佔位符的解析過程就結束了。

至此,我們知道:maven-resources-plugin 外掛根據我們傳入的配置資料,首先解析出可用的 delimiters,並將其轉換為佔位符,最終用真實的配置資料進行替換。

 

總結

本文討論了 SpringBoot 專案中的佔位符機制,結合實驗和原始碼進行了驗證。可以得出結論,對於 SpringBoot 使用的 maven-resources-plugin 3.2.0 (更低的版本可自行探索)來說:

  • 預設佔位符有兩種,分別是 ${*} 和 @*@

  • 配置項 useDefaultDelimiters,可以控制是否使用預設佔位符。如果為 true,則 ${*} 和 @*@ 這兩種佔位符始終有效,可以同時使用

  • 配置項 delimiter,既可以寫預設佔位符,也可以自定義佔位符,比如上文中的 #

注意事項:

  • 佔位符必須成對使用,如果忘記寫右邊的,則不會被解析。

  • 本文搭建的 Demo 專案,使用了 spring-boot-starter-parent 作為 parent,但有時我們可能不會使用它。此時,maven-resources-plugin 外掛需要我們手動引入,道理是一樣的。

常見情況:

  • 如果專案直接或間接引入 spring-boot-starter-parent 作為 parent,且沒有手動配置 maven-resources-plugin 外掛。則只能使用 @*@ 這一種佔位符,這是在 spring-boot-starter-parent 指定的。

  • 如果專案沒有引入 spring-boot-starter-parent 作為 parent,手動引入 maven-resources-plugin 外掛,但沒有指定任何 delimiter,也沒有顯式配置 useDefaultDelimiters 為 false,那麼可以使用預設佔位符 @*@ 或 ${*},因為不配置 useDefaultDelimiters 的話,預設為 true。

聊聊 SpringBoot 中的兩種佔位符:@*@ 和 ${*}

 

 

相關文章