SpringBoot的外部化配置最全解析!

天喬巴夏丶發表於2020-11-10

SpringBoot中的配置解析【Externalized Configuration】

本篇要點

  • 介紹各種配置方式的優先順序。
  • 介紹各種外部化配置方式。
  • 介紹yaml的格式及原理。
  • 介紹如何繫結並測試型別安全的屬性配置。
  • 介紹@ConfigurationProperties與@Value的區別。

一、SpringBoot官方文件對於外部化配置的介紹及作用順序

SpringBoot支援多種外部化配置,以便於開發者能夠在不同的環境下,使用同一套應用程式程式碼。外部化配置的方式有多種:properties檔案,yaml檔案,Environment變數已經命令列引數等等。

外部化配置的屬性值可以通過@Value註解自動注入,亦可以通過Spring的Environment抽象訪問,也可以通過@ConfigurationProperties註解繫結到結構化物件上。

SpringBoot支援很多種的外部化配置,待會我們會介紹到。在這之前,我們必須要知道如果多種配置同時出現,一定是按照特定的順序生效的。規則如下:

  1. devtool處於active狀態時,$HOME/.config/spring-boot目錄中的Devtool全域性配置。
  2. 測試中的@TestPropertySource註解。
  3. 測試中的@SpringBootTest#properties註解特性。
  4. 命令列引數。
  5. SPRING_APPLICATION_JSON中的屬性(環境變數或系統屬性中的內聯JSON嵌入)。
  6. ServletConfig初始化引數。
  7. ServletContext初始化引數。
  8. java:comp/env裡的JNDI屬性
  9. JVM系統屬性System.getProperties()
  10. 作業系統環境變數
  11. 僅具有random.*屬性的RandomValuePropertySource
  12. 應用程式以外的application-{profile}.properties或者application-{profile}.yml檔案
  13. 打包在應用程式內的application-{profile}.properties或者application-{profile}.yml檔案
  14. 應用程式以外的application.properties或者appliaction.yml檔案
  15. 打包在應用程式內的application.properties或者appliaction.yml檔案
  16. @Configuration類上的@PropertySource註解,需要注意,在ApplicationContext重新整理之前,是不會將這個類中的屬性加到環境中的,像logging.*,spring.main.*之類的屬性,在這裡配置為時已晚。
  17. 預設屬性(通過SpringApplication.setDefaultProperties指定).

這裡列表按組優先順序排序,也就是說,任何在高優先順序屬性源裡設定的屬性都會覆蓋低優先順序的相同屬性,列如我們上面提到的命令列屬性就覆蓋了application.properties的屬性。

舉個例子吧:

如果在application.properties中設定name=天喬巴夏,此時我用命令列設定java -jar hyh.jar --author.name=summerday,最終的name值將會是summerday,因為命令列屬性優先順序更高。

二、各種外部化配置舉例

1、隨機值配置

配置檔案中${random} 可以用來生成各種不同型別的隨機值,從而簡化了程式碼生成的麻煩,例如 生成 int 值、long 值或者 string 字串。原理在於,RandomValuePropertySource類重寫了getProperty方法,判斷以random.為字首之後,進行了適當的處理。

my.secret=${random.value}
my.number=${random.int}
my.bignumber=${random.long}
my.uuid=${random.uuid}
my.lessThanTen=${random.int(10)}
my.inRange=${random.int[1024,65536]}

2、命令列引數配置

預設情況下,SpringApplication將所有的命令列選項引數【以--開頭的引數,如--server.port=9000】轉換為屬性,並將它們加入SpringEnvironment中,命令列屬性的配置始終優先於其他的屬性配置。

如果你不希望將命令列屬性新增到Environment中,可以使用SpringApplication.setAddCommandLineProperties(false)禁用它。

$ java -jar app.jar --debug=true #開啟debug模式,這個在application.properties檔案中定義debug=true是一樣的

3、屬性檔案配置

屬性檔案配置這一部分是我們比較熟悉的了,我們在快速建立SpringBoot專案的時候,預設會在resources目錄下生成一個application.properties檔案。SpringApplication都會從配置檔案載入配置的屬性,並最終加入到Spring的Environment中。除了resources目錄下,還有其他路徑,SpringBoot預設是支援存放配置檔案的。

  1. 當前專案根目錄下的 /config 目錄下
  2. 當前專案的根目錄下
  3. resources 目錄下的 /config 目錄下
  4. resources 目錄下

以上四個,優先順序從上往下依次降低,也就是說,如果同時出現,上面配置的屬性將會覆蓋下面的。

關於配置檔案,properties和yaml檔案都能夠滿足配置的需求。

當然,這些配置都是靈活的,如果你不喜歡預設的配置檔案命名或者預設的路徑,你都可以進行配置:

$ java -jar myproject.jar --spring.config.name=myproject
$ java -jar myproject.jar --spring.config.location=classpath:/default.properties,classpath:/override.properties

4、指定profile屬性

通常情況下,我們開發的應用程式需要部署到不同的環境下,屬性的配置自然也需要不同。如果每次在釋出的時候替換配置檔案,過於麻煩。SpringBoot的多環境配置為此提供了便利。具體做法如下:

我們之前在介紹各種配置的優先順序的時候說過,application-{profile}.properties或者application-{profile}.yml檔案的優先順序高於application.properties或application.yml配置,這裡的profile就是我們定義的環境標識:

我們在resource目錄下建立三個檔案:

  • application.properties:預設的配置,default。
  • application-dev.properties:開發環境,dev。
  • application-prod.properties:生產環境,prod。

我們可以通過指定spring.profiles.active屬性來啟用對應的配置環境:

spring.profiles.active=dev

或使用命令列引數的配置形式:

$ java -jar hyh.jar --spring.profiles.active=dev

如果沒有profile指定的檔案於profile指定的檔案的配置屬性同時定義,那麼指定profile的配置優先。

5、使用佔位符

在使用application.properties中的值的時候,他們會從Environment中獲取值,那就意味著,可以引用之前定義過的值,比如引用系統屬性。具體做法如下:

name=天喬巴夏
description=${name} is my name

6、加密屬性

Spring Boot不提供對加密屬性值的任何內建支援,但是,它提供了修改Spring環境中的值所必需的掛鉤點。我們可以通過實現EnvironmentPostProcessor介面在應用程式啟動之前操縱Environment。

可以參考howto.html,檢視具體使用方法。

7、使用YAML代替properties

YAML是JSON的超集,是一種指定層次結構配置資料的便捷格式,我們以properties檔案對比一下就知道了:

#properties
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456

my.servers[0]=www.hyh.com
my.servers[1]=www.yhy.com
# yml
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8
    username: root
    password: 123456
my:
  server:
    - www.hyh.com
    - www.yhy.com

只要在類路徑上具有SnakeYAML庫,SpringApplication類就會自動支援YAML作為屬性配置的方式。SpringBoot專案中的spring-boot-starter已經提供了相關類庫:org.yaml.snakeyaml,因此SpringBoot天然支援這種方式配置。

關於yaml檔案的格式,可以參考官方文件:Using YAML Instead of Properties

8、型別安全的屬性配置

上面說到通過@Value("${property}") 註解來注入配置有時會比較麻煩,特別是當多個屬性本質上具有層次結構的時候。SpringBoot提供了一種解決方案:讓強型別的bean管理和驗證你的配置

直接來看具體的使用叭:

@ConfigurationPropertie定義一個繫結配置的JavaBean

  1. 使用預設構造器+getter和setter注入
@ConfigurationProperties("acme")
public class AcmeProperties {
    private boolean enabled; //acme.enabled  預設為false
    private InetAddress remoteAddress;// acme.remote-address  可以從String轉換而來的型別
    private final Security security = new Security();
	//.. 省略getter和setter方法

    public static class Security {
        private String username; // acme.security.username
        private String password; // acme.security.password
        private List<String> roles = new ArrayList<>(Collections.singleton("USER"));// acme.security.roles
		//.. 省略getter setter方法
    }
}

這種方式依賴於預設的空建構函式,通過getter和setter方法賦值,因此getter和setter方法是必要的,且不支援靜態屬性的繫結。

如果巢狀pojo屬性已經被初始化值: private final Security security = new Security();可以不需要setter方法。如果希望繫結器使用其預設建構函式動態建立例項,則需要setter。

  1. 通過@ContructorBinding註解使用構造器繫結的方式:
@ConstructorBinding //標註使用構造器繫結
@ConfigurationProperties("acme")
public class AcmeProperties {
    private final Security security;
    private final boolean enabled;
    private final InetAddress remoteAddress;
    public AcmeProperties(boolean enabled, InetAddress remoteAddress, Security security) {
        this.enabled = enabled;
        this.remoteAddress = remoteAddress;
        this.security = security;
    }
    //..省略getter方法
    @ToString
    public static class Security {
        private final String username;
        private final String password;
        private final List<String> roles;
        public Security(String username, String password,
                        @DefaultValue("USER") List<String> roles) {
            this.username = username;
            this.password = password;
            this.roles = roles;
        }
    }
    //..省略getter方法
}

如果沒有配置Security例項屬性,那麼最後結果:Security=null。如果我們想讓Security={username=null,password=null,roles=[USER]},可以在Security上加上@DefaultValue。public AcmeProperties(boolean enabled, InetAddress remoteAddress, @DefaultValue Security security)

通過@EnableConfigurationProperties註冊

已經定義好了JavaBean,並與配置屬性繫結完成,接著需要註冊這些bean。我們通常用的@Component或@Bean,@Import載入bean的方式在這裡是不可取的,SpringBoot提供瞭解決方案:使用@EnableConfigurationProperties,我們既可以一一指定配置的類,也可以按照元件掃描的方式進行配置。

@SpringBootApplication
@EnableConfigurationProperties({HyhConfigurationProperties.class, MyProperties.class,AcmeProperties.class})
public class SpringBootProfileApplication {
}
@SpringBootApplication
@ConfigurationPropertiesScan({"com.hyh.config"})
public class SpringBootProfileApplication {
}

配置yaml檔案

acme:
  remote-address: 192.168.1.1
  security:
    username: admin
    roles:
      - USER
      - ADMIN

注入properties,測試

@Configuration
public class Application implements CommandLineRunner {
    @Autowired
    private AcmeProperties acmeProperties;

    @Override
    public void run(String... args) throws Exception {
        System.out.println(acmeProperties);
    }
}
//輸出: 
AcmeProperties(security=AcmeProperties.Security(username=admin, password=null, roles=[USER, ADMIN]), enabled=false, remoteAddress=/192.168.1.1)

寬鬆繫結

SpringBoot採用寬鬆的規則進行Environment和@ConfigurationProperties標註bean的匹配。如:

@ConfigurationProperties(prefix="acme.my-project.person")
public class OwnerProperties {

    private String firstName;

    public String getFirstName() {
        return this.firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

}

下面表格中的屬性名都可以匹配:

Property Note
acme.my-project.person.first-name Kebab case, which is recommended for use in .properties and .yml files.
acme.myProject.person.firstName Standard camel case syntax.
acme.my_project.person.first_name Underscore notation, which is an alternative format for use in .properties and .yml files.
ACME_MYPROJECT_PERSON_FIRSTNAME Upper case format, which is recommended when using system environment variables.

@ConfigurationProperties註解中的prefix值必須是kebab case形式的,以-為分割符。

Spring官方建議,屬性儘可能以lower-case kebab的形式:my.property-name=acme

Map如何繫結

繫結到Map屬性時,如果key包含小寫字母數字字元或-以外的任何其他字元,則需要使用方括號包圍key,以便保留原始值。 如果鍵沒有被[]包圍,則所有非字母數字或-的字元都將被刪除。如下:

hyh:
  username: 天喬巴夏
  password: 123456
  map:
    "[/key1]": value1 #用引號包圍[],用[]包圍key
    /key3: value3
    key-4: value4
    key/5: value5
# 結果:"map":{/key1=value1,key5=value5, key-4=value4, key3=value3}

環境變數如何繫結

遵循三條原則:

  1. .換成下劃線_
  2. 移除-
  3. 小寫轉大寫。

如:spring.main.log-startup-info轉為:SPRING_MAIN_LOGSTARTUPINFOmy.acme[0].other轉為MY_ACME_0_OTHER

9、複雜型別

之前介紹yml檔案,介紹了單純的陣列形式或值的繫結,SpringBoot還支援複雜型別的繫結。

merge:
  list:
    - name: 天喬巴夏
      desc: 帥啊
    - name: tqbx
      desc: 很帥啊
  map:
    key1:
      name: summerday
      desc: handsome!
    key2:
      name: summer
@ToString
@ConfigurationProperties(prefix = "merge")
public class MergeProperties {
    private final List<User> list = new ArrayList<>();
    private final Map<String,User> map = new HashMap<>();
    public List<User> getList() {
        return list;
    }
    public Map<String, User> getMap() {
        return map;
    }
}

最後輸出:

MergeProperties(
    list=[User(name=天喬巴夏, desc=帥啊), 
          User(name=tqbx, desc=很帥啊)], 
    map={key1=User(name=summerday, desc=handsome!), 
         key2=User(name=summer, desc=null)}a
)

10、引數校驗

對@ConfigurationProperties類使用Spring的@Valid註解時,Spring Boot就會嘗試對其進行驗證。

你可以直接在配置類上使用JSR-303 javax.validation約束註解。這個做法的前提是,你的類路徑上有相容的JSR-303實現:

        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-validator</artifactId>
            <version>6.0.18.Final</version>
        </dependency>

然後將約束註解加到欄位上,如下:

@Data
@Validated
@ConfigurationProperties(prefix = "validate")
public class ValidateProperties {
    @NotNull
    private String name;
    @Valid
    private final SubProperties subProperties = new SubProperties();
    @Data
    public static class SubProperties {
        @Min(value = 10,message = "年齡最小為10")
        public Integer age;
    }
}

配置如下:

validate:
  name: hyh
  sub-properties:
    age: 5

結果如下:

Description:

Binding to target org.springframework.boot.context.properties.bind.BindException: 
Failed to bind properties under 'validate' to com.hyh.config.ValidateProperties failed:
    Property: validate.sub-properties.age
    Value: 5
    Origin: class path resource [application.yml]:47:10
    Reason: 年齡最小為10
        
Action:
Update your application's configuration

三、@ConfigurationProperties與@Value的區別

@Value註解是一個核心容器功能,它沒有提供和type-safe配置屬性相關的功能,下面這個表格總結了兩者分別支援的功能:

Feature @ConfigurationProperties @Value
寬鬆繫結 Yes Limited (see note below)
後設資料支援 Yes No
SpEL 表示式 No Yes

官方建議:

  • 如果你為自己的元件定義了一套配置,建議使用@ConfigurationProperties和POJO繫結,這樣做能夠提供結構化且型別安全的物件。
  • 如果硬要使用@Value,建議使用kebab-case形式,如@Value(" ${demo.item-price}")

原始碼下載

本文內容均為對優秀部落格及官方文件總結而得,原文地址均已在文中參考閱讀處標註。最後,文中的程式碼樣例已經全部上傳至Gitee:https://gitee.com/tqbx/springboot-samples-learn,另有其他SpringBoot的整合哦。

參考閱讀

相關文章