前言 "《【源碼解析】憑什麼?spring boot 一個 jar 就能開發 web 項目》 " 中有讀者反應: 部署後運維很不方便,比較修改一個 IP 配置,需要重新打包。 這一點我是深有體會,17 年自學,並很大膽的直接在生產環境用的時候,我都是讓產品經理(此時他充當我們的運維,嘿嘿)用壓縮軟體 ...
前言
《【源碼解析】憑什麼?spring boot 一個 jar 就能開發 web 項目》 中有讀者反應:
部署後運維很不方便,比較修改一個 IP 配置,需要重新打包。
這一點我是深有體會,17 年自學,並很大膽的直接在生產環境用的時候,我都是讓產品經理(此時他充當我們的運維,嘿嘿)用壓縮軟體打開 jar,然後複製出配置,修改完之後再替換回去。為什麼我這麼大膽,因為當時才入行一年,而且覺得有架構師兜底,我就奔放了。你是不知道,當時負責這個項目的開發(c#開發)一開始不想用 SpringBoot 的。
不過如今看到這個問題,我有點震驚,都 9102 年了,竟然還擔心這樣的問題。我想說,哥們,這真的不是事兒。SpringBoot 早就提供了方法來解決這個問題。
SpringBoot 生產特性
SpringBoot 有很多生產特性,可以在生產環境中使用時更加方便。其中外部化配置基本都會用到。
Spring Boot 允許外部化配置,以便相同的應用在不同的環境中工作。
屬性值可以在 Spring 環境中使用 @Value 或 @ConfigurationProperties 使用。
此次參考的版本是 SpringBoot-2.2.0.RELEASE
優先順序
外部化配置的優先順序順序如下:
- Devtools 全局配置:當 devtools 啟用時,
$HOME/.config/spring-boot
- 測試類中的
@TestPropertySource
- 測試中的
properties
屬性:在 @SpringBootTest 和 用來測試特定片段的測試註解 - 命令行參數
SPRING_APPLICATION_JSON
中的屬性:內嵌在環境變數或系統屬性中的 JSONServletConfig
初始化參數ServletContext
初始化參數java:comp/env
中的 JNDI 屬性- Java 系統屬性:
System.getProperties()
- 操作系統環境變數
- 隨機值(
RandomValuePropertySource
):random.*
屬性 - jar 包外的指定 profile 配置文件:
application-{profile}.properties
- jar 包內的指定 profile 配置文件:
application-{profile}.properties
- jar 包外的預設配置文件:
application.properties
- jar 包內的預設配置文件:
application.properties
- 代碼內的
@PropertySource
註解:用於@Configuration
類上 - 預設屬性:通過設置
SpringApplication.setDefaultProperties
指定
註意:以上用 properties
文件的地方也可用 yml
文件
配置隨機值
my.uuid=${random.uuid}
命令行屬性
java -jar -Ddemo=vm demo.jar --demo=arg
- -Dxxx 為 vm 參數,在代碼中通過
System#getProperty
獲取 - --xxx 為 spring 命令行參數,通過
Environment#getProperty
獲取,若通過此方法獲取不到,會獲取 vm 同名參數 - xxx.jar 之後的參數都是 arg 參數,都會在 main 方法中的 arg 數組中獲取到
示例
public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(ArgApplication.class, args);
LOGGER.info("----------------");
/* 列印 arg 參數 */
Arrays.stream(args)
.forEach(
arg -> {
LOGGER.info("arg:{}", arg);
});
/* 命令行傳參 demo */
LOGGER.info("System#getProperty:{}", System.getProperty("demo"));
LOGGER.info("Environment#getProperty:{}", context.getEnvironment().getProperty("demo"));
}
輸入命令
java -jar -Ddemo=vm arg-0.0.1-SNAPSHOT.jar aaa bbb ccc --demo=arg
效果如下:
----------------
arg:aaa
arg:bbb
arg:ccc
arg:--demo=arg
System#getProperty:vm
Environment#getProperty:arg
而如果執行命令是:
java -jar -Ddemo=vm arg-0.0.1-SNAPSHOT.jar aaa bbb ccc
結果如下:
arg:aaa
arg:bbb
arg:ccc
System#getProperty:vm
Environment#getProperty:vm
如果執行命令是:
java -jar arg-0.0.1-SNAPSHOT.jar aaa bbb ccc --demo=arg
結果如下:
arg:aaa
arg:bbb
arg:ccc
arg:--demo=arg
System#getProperty:null
Environment#getProperty:arg
屬性文件
優先順序:
- file:./config/
- file:./
- classpath:/config/
- classpath:/
如果定義了 spring.config.location
,如:classpath:/custom-config/,file:./customr-config/
,優先順序如下:
- file:./custom-config/
- classpath:custom-config/
如果指定了 spring.config.additional-location
,會先載入 additional 配置 如:spring.config.additional-location=classpath:/custom-config/,file:./customr-config/
,優先順序如下:
- file:./custom-config/
- classpath:/custom-config/
- file:./config/
- file:./
- classpath:/config/
- classpath:/
指定 profile 的屬性
預設的 profile 是 default
,當沒有指定spring.profiles.active
屬性時,預設會載入application-default.properties
文件。指定 profiles 文件的載入順序與上述不指定 profiles 文件的載入一致。指定 profile 文件的屬性始終覆蓋未指定文件的屬性
。如:spring.profiles.active=dev
,則 application-dev.properties
文件內的屬性會覆蓋 application.properties
內的同名屬性。
註意:如果在
spring.config.location
屬性中指定了文件
,則此文件對應的特定 profiles 類文件不起作用。如果想要起作用,在spring.config.location
中使用文件夾
。
占位符
配置文件中可以引用之前定義的值,如下:
app.name=MyApp
app.description=${app.name} is a Spring Boot application.
可以用此特性創建一些已存在的 Spring Boot 配置的較短、易於使用的變數。如下:
# nacos 配置示例
spring:
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
namespace: d9a39d78-xxxxxxxx-ea4f282e9d99
discovery:
server-addr: 127.0.0.1:8848
namespace: d9a39d78-xxxxxxxx-ea4f282e9d99
# Discovery 配置示例
nacos:
plugin:
namespace: d9a39d78-xxxxxxxx-ea4f282e9d99
可改為如下配置
spring:
cloud:
nacos:
config:
server-addr: ${app.server-addr}
namespace: ${app.namespace}
discovery:
server-addr: ${app.server-addr}
namespace: ${app.namespace}
# Discovery 配置示例
nacos:
plugin:
namespace: ${app.namespace}
app:
server-addr: 127.0.0.1:8848
namespace: d9a39d78-xxxxxxxx-ea4f282e9d99
然後在命令行可以直接通過 -Dapp.namespace
或 --app.namespace
來傳參,會方便很多。特別是在多個地方用到同一個屬性的時候。
屬性加密
Spring Boot 不支持屬性加密,但提供鉤子節點修改配置屬性。EnvironmentPostProcessor
介面允許在應用啟動前操作 Environment
。
yaml
yaml 文件使用的時候非常直觀、方便。而且在 Spring Boot 中做了處理,獲取 yaml 和 properties 文件中的屬性基本是一樣的操作。
一個文件指定多 pfofile
通過 spring.profiles
指示何時使用對應的配置,使用 ---
進行配置分隔
# application.yml
server:
address: 192.168.1.100
---
spring:
profiles: development
server:
address: 127.0.0.1
---
spring:
profiles: production & eu-central
server:
address: 192.168.1.120
yaml 缺點
用 @PropertySource
不能載入 yaml 文件,這種情況下只能使用 properties 文件。
在特定 profile 的 yaml 文件中使用多 profile 配置,會有意料之外的情況:
# application-dev.yml
server:
port: 8000
---
spring:
profiles: "!test"
security:
user:
password: "secret"
當運行時指定 --spring.profiles.active=dev
,啟用 dev profile,其它的 profile 會忽略。也就是此例中 spring.security.user.password
屬性會失效。
因此,不要在指定 profile 的 yaml 文件中使用多種 profile 配置。
類型安全的屬性配置
JavaBean 屬性綁定
通過 @ConfigurationProperties
註解將屬性(properties、yml 文件、環境變數等)綁定到類對象中。與自動配置類類似。
@ConfigurationProperties("acme")
public class AcmeProperties{
private boolean enabled;
private InetAddress remoteAddress;
private final Security security = new Security();
// getter and setter
public static class Security{
private String username;
private String password;
private List<String> roles = new ArrayList<>(Collections.singleton("USER"));
// getter and setter
}
}
這種安排依賴於預設的無參構造器,getter 和 setter 通常是必需的,因為綁定就像 Spring MVC 一樣是通過標準的 Java Beans 屬性描述符進行的。在下列情況下,可省略 setter:
- Maps:只要被初始化後,getter 必須而 setter 不必須,binder 可以對它們進行修改
- Collections 和 數組:可以通過索引或逗號分隔的值來設定屬性。後者必須有 setter 方法。建議對於這種情況一直加上 setter。如果初始化了一個 Collection,確保它不是不可變類型。
- 如果初始化了嵌套的 POJO 屬性(如上例中的 Security),setter 不是必須的。如果需要 binder 通過其預設構造器動態創建實例,則需要 setter
註意:如果使用 Lombok 生成 getter 和 setter,確保不會生成任何特定的構造器,不然容器會自動使用它來實例化對象。
最後,只有標準 Java Bean 屬性可以這樣綁定屬性,靜態屬性不支持。
構造器綁定
上述示例可以改成如下:
@ConstructorBinding
@ConfigurationProperties("acme")
public class AcmeProperties{
private final boolean enabled;
private final InetAddress remoteAddress;
private final Security security;
public AcmeProperties(boolean enabled, InetAddress remoteAddress, Security security){
this.enabled = enabled;
this.remoteAddress = remoteAddress;
this.security = security;
}
// getter and setter
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 and setter
}
}
@ConstructorBinding
註解表示使用構造函數綁定屬性值。這意味著 binder
將期望找到一個包含待綁定參數的構造器。
@ConstructorBinding
類的嵌套成員也將通過構造函數綁定屬性值。
可以使用 @DefaultValue
指定預設值,轉換服務將字元串值強轉為缺少屬性的目標類型。
要使用構造綁定,類必須允許使用
@EnableConfigurationProperties
或 配置屬性掃描方式。不能對由常規 Spring 機制創建的 bean 使用構造函數綁定。如:@Component Bean、通過@Bean 方法創建的 Bean 或使用@Import 載入的 Bean
如果類中有多個構造器,可以直接將
@ConstructorBinding
註解使用在要綁定的構造器上。
啟用 @ConfigurationProperties
註解類型
Spring Boot 提供了一個基礎設施來綁定這些類型並將它們自動註冊為 bean。
如果應用程式中使用@SpringBootsApplication
,用@ConfigurationProperties
註解的類將被自動掃描並註冊為 bean。預設情況下,將從聲明此註解的類的包中進行掃描。如果要掃描特定的包,可以對 ·@SpringBootsApplication
註解的類顯式使用@ConfigurationPropertiescan
註解,如下例所示:
@SpringBootApplication
@ConfigurationPropertiesScan({ "com.example.app", "org.acme.another" })
public class MyApplication {
}
有時,用
@ConfigurationProperties
註釋的類可能不適合掃描,例如,如果正在開發自己的自動配置,在這些情況下,可以在任何@Configuration 類上指定要處理的類型列表,如下例所示:
@Configuration(proxyBeanMethods = false) @EnableConfigurationProperties(AcmeProperties.class)
public class MyConfiguration { }
註意:當使用配置屬性掃描或通過@EnableConfigurationProperties 註冊@ConfigurationProperties bean 時,bean 有一個常規名稱:
<prefix>-<fqn>
,其中<prefix>
是@ConfigurationProperties
註解中指定的環境 key 首碼,<fqn>
是 bean 的完全限定名。如果註解沒有提供任何首碼,則只使用 bean 的完全限定名。
上例中 bean name 是acme-com.example.AcmeProperties
。
使用@ConfigurationProperties
註解類型
這種類型的配置在 SpringApplication 外部 YAML 配置中特別適用,如下例所示:
# application.yml
acme:
remote-address: 192.168.1.1
security:
username: admin
roles:
- USER
- ADMIN
@ConfigurationProperties
bean 可以像其它 bean 一樣註入使用。如下:
@Service
public class MyService{
private final AcmeProperties properties;
@Autowired
public MyService(AcmeProperties properties){
this.properties = properties;
}
// ...
}
使用
@ConfigurationProperties
還可以生成元數據文件,IDE 可以使用這些文件提供代碼自動完成功能。
第三方配置
除了可以在 類
上使用 @ConfigurationProperties
註解,還可以在 public @Bean 方法
上使用它。如果要將屬性綁定到不在控制範圍內的第三方組件,那麼這樣做特別有用。
要從 Environment
屬性配置 bean,將 @ConfigurationProperties
添加到其 bean 註冊中,如下例所示:
@ConfigurationProperties(prefix = "another")
@Bean
public AnotherComponent anotherComponent() {
//...
}
用
another
首碼定義的任何 JavaBean 屬性都映射到AnotherComponent
bean 上,映射方式類似於前面的 AcmeProperties 示例。
鬆綁定
Spring Boot 使用一些寬鬆的規則將
Environment
屬性綁定到@ConfigurationProperties
bean,因此環境屬性名和 bean 屬性名之間不需要完全匹配。常見的包括短劃線分隔的環境屬性(例如,context-path
綁定到contextPath
)和大寫的環境屬性(例如,PORT
綁定到port
)。
@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;
}
}
對於以上 Java Bean,可以使用以下屬性
註意:註解的首碼值必須是短橫線 (小寫,用-分隔,如:acme.my-project.person)。
放寬每個屬性源的綁定規則
建議:如果可能的話,將屬性存儲為小寫的短橫線格式,例如:my.property-name=acme。
在綁定到 Map
屬性時,如果 key
包含除小寫字母-數字字元或 -
之外的任何內容,則需要使用括弧符號,以便保留原始值。如果 key
沒有被[]
包圍,則刪除任何不是字母數字或 -
的字元。
acme:
map:
"[/key1]": value1
"[/key2]": value2
/key3: value3
上面的屬性將綁定到 Map
的這些 key
中:/key1
、/key2
、key3
合併複雜類型
List
當在多個位置配置 list 時,通過替換(而非添加)整個 list 來覆蓋。
@ConfigurationProperties("acme")
public class AcmeProperties {
private final List<MyPojo> list = new ArrayList<>();
public List<MyPojo> getList() { return this.list;
}
}
acme:
list:
- name: my name
description: my description
---
spring:
profiles: dev
acme:
list:
- name: my another name
當啟用 dev
配置時,AcmeProperties.list
中值包含一個 MyPojo
對象(name 為my another name
),不是添加操作,而是覆蓋操作。
當一個
List
在多個 profiles 中定義時,最高優先順序的被使用。
Map
對於 Map
屬性,可以使用從多個屬性源獲取屬性值進行綁定。但是,對於多個源中的同一屬性,將使用優先順序最高的屬性。
@ConfigurationProperties("acme")
public class AcmeProperties {
private final Map<String, MyPojo> map = new HashMap<>();
public Map<String, MyPojo> getMap() {
return this.map;
}
}
acme:
map:
key1:
name: my name 1
description: my description 1
---
spring:
profiles: dev
acme:
map:
key1:
name: dev name 1
key2:
name: dev name 2
description: dev description 2
當 dev 配置啟用時,AcmeProperties.map
中包含兩個鍵值對。key1
中 pojo name 為 dev name 1,description 為 my description 1;key2
中 pojo name 為 dev name 2,description 為 dev description 2。
不同屬性源的配置進行了合併
以上合併規則適用於所有的屬性源
屬性轉換
Spring Boot 試圖在綁定到
@ConfigurationProperties
bean 時將外部應用程式屬性強轉為正確的類型。如果需要自定義類型轉換,可以提供ConversionService
bean(帶有名為ConversionService
的 bean)或自定義屬性編輯器(通過CustomEditorConfigurer
bean)或自定義Converters
(使用 bean 定義註解@ConfigurationPropertiesBinding
)。
註意:由於此 bean 在應用程式生命周期的早期被請求,請確保限制
ConversionService
正在使用的依賴項。通常,需要的任何依賴項在創建時都可能未完全初始化。如果自定義的ConversionService
不需要配置 keys 強轉,並且僅依賴於使用@ConfigurationPropertiesBinding
限定的自定義轉換器,則可能需要將它重命名。
時間區間轉換
SpringBoot 對錶示持續時間有專門的支持。如果暴露 java.time.Duration
屬性,則可以用以下格式:
- 常規的
long
表示(除非指定了@DurationUnit
,否則使用毫秒作為預設單位) java.time.Duration
使用的標準 ISO-8601 格式- 一種更可讀的格式,其中值和單位是耦合的(例如,10s 表示 10 秒)
@ConfigurationProperties("app.system")
public class AppSystemProperties {
@DurationUnit(ChronoUnit.SECONDS)
private Duration sessionTimeout = Duration.ofSeconds(30);
private Duration readTimeout = Duration.ofMillis(1000);
public Duration getSessionTimeout() {
return this.sessionTimeout;
}
public void setSessionTimeout(Duration sessionTimeout) {
this.sessionTimeout = sessionTimeout;
}
public Duration getReadTimeout() {
return this.readTimeout;
}
public void setReadTimeout(Duration readTimeout) {
this.readTimeout = readTimeout;
}
}
要指定 30 秒的 sessionTimeout,30、PT30S 和 30s 都是等效的。500ms 的 readTimeout 可以用以下任何形式指定:500、PT0.5S 和 500ms。
也可以使用以下任何支持的單位:
ns
:納秒us
:微妙ms
:毫秒s
:秒m
:分h
:時d
:天
預設的單位是毫秒,可以使用
@DurationUnit
指定
數據 size 轉換
Spring 框架有一個 DataSize
類型,以位元組表示大小。如果暴露一個 DataSize
屬性,則可以用以下格式:
- 常規的
long
表示(除非指定了@DataSizeUnit
,否則使用位元組作為預設單位) java.time.Duration
使用的標準 ISO-8601 格式- 一種更可讀的格式,其中值和單位是耦合的(例如,
10MB
表示 10 兆位元組)。
@ConfigurationProperties("app.io")
public class AppIoProperties {
@DataSizeUnit(DataUnit.MEGABYTES)
private DataSize bufferSize = DataSize.ofMegabytes(2);
private DataSize sizeThreshold = DataSize.ofBytes(512);
public DataSize getBufferSize() {
return this.bufferSize;
}
public void setBufferSize(DataSize bufferSize) {
this.bufferSize = bufferSize;
}
public DataSize getSizeThreshold() {
return this.sizeThreshold;
}
public void setSizeThreshold(DataSize sizeThreshold) {
this.sizeThreshold = sizeThreshold;
}
}
要指定 10 兆位元組的 bufferSize
,10
和 10MB
是等效的。256 位元組的 sizeThreshold
可以指定為 256
或 256B
。
也可以使用以下任何支持的單位:
B
:位元組
KB
:千位元組
MB
:兆位元組
GB
:千兆位元組
TB
:兆兆位元組
預設的單位是位元組,可以使用
@DataSizeUnit
指定
@ConfigurationProperties 校驗
每當對 @ConfigurationProperties
類使用 Spring 的@Validated
註解時,Spring Boot 就會驗證它們。可以直接在配置類上使用 JSR-303 javax.validation
約束註解。必須確保類路徑上有一個相容的 JSR-303 實現(如:hibernate-validator),然後將約束註解添加到欄位中。
@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {
@NotNull
private InetAddress remoteAddress;
// ... getters and setters
}
註意:還可以通過註解@Bean 方法來觸發驗證,該方法使用@Validated 創建配置屬性。
儘管嵌套屬性在綁定時也將被驗證,但最好對關聯欄位使用
@Valid
。這確保即使找不到嵌套屬性,也會觸發驗證。
@ConfigurationProperties(prefix="acme")
@Validated
public class AcmeProperties {
@NotNull
private InetAddress remoteAddress;
@Valid
private final Security security = new Security();
// ... getters and setters
public static class Security {
@NotEmpty
public String username;
// ... getters and setters
}
}
還可以通過創建
ConfigurationPropertiesValidator
bean 來添加自定義 SpringValidator
。@Bean
方法應該聲明為static
。配置屬性驗證器是在應用程式生命周期的早期創建的,將@Bean 方法聲明為 static 可以創建 Bean,而無需實例化@configuration 類。這樣做可以避免任何可能由早期實例化引起的問題。
註意:
spring-boot-actuator
模塊包含一個端點,該端點暴露所有@ConfigurationProperties
bean。訪問/actuator/configprops
可獲得相關信息。
@ConfigurationProperties vs. @Value
@Value
註解是一個核心容器特性,它不提供與 @ConfigurationProperties
相同的特性。
如果需要為組件定義了一組配置鍵,建議將它們配置到一個
@ConfigurationProperties
註解的 POJO 中。由於@Value
不支持鬆綁定,如果需要使用環境變數提供值,則它不是一個好的選項。
雖然可以在@Value
中編寫SpEL
表達式,但此類表達式不會從 properties 文件中處理。
使用配置中心
如果項目比較大的話,分成了好幾個 SpringBoot 工程,可以使用某些 SpringCloud 組件,比如:配置中心。配置中心支持一個地方管理所有的配置,有些還可以支持修改配置實時生效而不用重啟應用,真的是很棒棒呢。推薦使用 nacos
。如果項目比較小,你用 git
或者指定文件夾
來作為配置存放的地方也可以。
怎麼樣?有了這些用法的支持,你還會覺得 Springboot 打成一個 jar
會在部署的時候很不方便嗎?
參考資料
官方文檔
公眾號:逸飛兮(專註於 Java 領域知識的深入學習,從源碼到原理,系統有序的學習)