分離變化和不變是軟件設計的一個原則,將不變的部分形成模版,將變化的部分抽出為配置文件;不同的環(huán)境使用不同的配置文件,方便維護且不需要重新編譯代碼;Spring框架引入占位符為其提供了一個解決方案。
本文作為Spring系列文章的第六篇,內容包含占位符的使用和背后原理;其中,原理部分會伴隨著Spring源碼進行。
本文討論的占位符指${}
, 常見于SpringBoot的application.properties
(或application.yml
)配置文件、或自定義*.properties
配置文件中,也常見于@Value等注解、Feign相關接口上;在Spring項目中,常見于Spring的配置文件,可以用在bean的定義上。占位符中的變量在程序啟動過程中進行解析,developer需要引入配置文件使得解析過程正常執(zhí)行。
本章節(jié)以Spring系列-4 國際化中的國際化Bean的配置過程為例進行介紹。
引入配置文件在resources資源路徑下準備一個配置文件,文件內容如下:
# default.properties
basename=i18n/messages
defaultEncoding=UTF-8
在Bean定義的配置文件中,通過
等價于以下方式:
location屬性:
location屬性為PropertySourcesPlaceholderConfigurer對象指定了配置文件路徑;當需要指定多個配置文件時,應使用逗號分隔開。另外,也可以使用通配符形式,如下所示:
默認情況下,如果指定了文件而沒找到時——拋出異常;使用通配符時,即使匹配結果為空也不會拋出異常。
注意:當在一個
標簽中指定了多個配置文件時,處理占位符時會按照配置順序依次向配置文件中進行匹配,第一次完成匹配時即返回;否則一直向下匹配,直到拋出異常。
因此,配置順序決定了配置文件的優(yōu)先級,靠前的優(yōu)先級較高。
ignore-unresolvable和ignore-resource-not-found屬性:
除location外,有兩個屬性需要關注:ignore-unresolvable和ignore-resource-not-found.
ignore-unresolvable表示解析失敗時是否忽略(不拋出異常-返回原字符串);默認值false表示解析失敗時拋出異常。ignore-resource-not-found表示獲取不到配置文件時是否忽略(不拋出異常);默認值false表示獲取文件失敗時拋出異常。二者經常組合出現,因為存在邏輯上的優(yōu)先級順序:當ignore-unresolvable配置為true時,無論文件是否存在-解析是否成功,都不會拋出異常,即ignore-resource-not-found處于邏輯失效狀態(tài);當ignore-resource-not-found配置為false時,ignore-resource-not-found才會生效。
配置多個PropertySourcesPlaceholderConfigurer對象
當配置多個
需要注意:這與一個
標簽中配置多個配置文件不同;每個 標簽對應一個獨立的Bean對象。
以下通過簡單案例介紹,已知道原因的讀者,可跳過過該案例:
Bean定義的配置文件如下:
Spring會按照配置順序,先后向IOC容器注入 default.properties 對應的Bean對象(使用default解析器表示)和 local.properties 對應的Bean對象(使用local解析器表示);在解析 testPhc 的BeanDefinition時,會按照IOC順序依次調用兩個PropertySourcesPlaceholderConfigurer對象去處理${name}
和${age}
.
這兩個Bean對象是完全獨立的且解析過程在時間上先后進行,互補干擾;整個解析過程如下:
default解析器解析時,如果解析正常,即default.properties文件中配置了name
和age
變量,則將testPhc的BeanDefinition對象中的占位符替換為配置的value. 然后使用local解析器再次解析,發(fā)現沒有占位符號,直接退出解析過程,表現為整個解析過程正常。
default解析器解析失敗時,即default.properties文件中未配置name
或age
變量,會直接拋出異常,不再進入其他解析器。
因此,整個解析過程中只有default解析器生效,其他Bean對象都被邏輯失效了(等價于僅配置了default解析器)。
可以將最后一個PropertySourcesPlaceholderConfigurer對象前的所有PropertySourcesPlaceholderConfigurer對象的屬性設置為true,來解決上述問題,如下所示:
當然,最后一個PropertySourcesPlaceholderConfigurer對象的ignore-unresolvable屬性也可以設置為true;但作為最后一個解析器,需要保持當解析失敗時拋出異常的功能。
使用占位符提醒:盡量讓異常盡早拋出,能在編譯期的不要延遲到啟動時,能在啟動時拋出的不要延遲到運行過程中。
配置國際化bean對象:
使用配置文件實現等效配置:
實際上,在Bean定義的配置文件中, 所有值對象(包括bean的id、class等屬性)都可以使用占位符形式,甚至也包括引入配置文件的標簽:
另外,需要注意如果占位符解析失敗,會拋出異常;比如上面的location必須要求在default.properties進行了配置。
2.2 SpringBoot項目SpringBoot項目中的配置數據可以來自application.properties(或application.yml)或手動引入的自定義配置文件。
引入配置文件通過@PropertySource
注解可手動引入配置文件:
@Configuration
@PropertySource(value = {"classpath:default.properties", "classpath:location.properties"})
public class PropertiesConfiguration {}
或者在application.yml文件中進行配置:
# application.yml
placeholder:
serverName: PlaceHolderServer
url: http://127.0.0.1:8080/phs
使用占位符SpringBoot中占位符常見 @Value注解和 @ConfigurationProperties等注入場景, 其中 @ConfigurationProperties注解是SpringBoot引入的.
使用@Value注解
// PlaceHolderBean.java文件
@Data
@Component
public class PlaceHolderBean {@Value("${placeholder.serverName}")
private String serverName;
@Value("${placeholder.url}")
private String url;
}
// 測試用例
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class PlaceHolderTest {@Autowired
private PlaceHolderBean placeHolderBean;
@Test
public void testValueAnnotation() {LOGGER.info("placeHolderBean is {}.", placeHolderBean);
}
}
通過 @Value注解,可以將配置文件中的placeholder.serverName
和placeholder.url
變量分別賦值給PlaceHolderBean對象的serverName和url屬性,測試用例執(zhí)行結果如下:
使用@ConfigurationProperties注解
// PlaceHolderBean.java文件
@Data
@Component
@ConfigurationProperties(prefix = "placeholder")
public class PlaceHolderProperties {private String serverName;
private String url;
}
// 測試用例
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class PlaceHolderTest {@Autowired
private PlaceHolderProperties placeHolderProperties;
@Test
public void testPlaceHolder() {LOGGER.info("placeHolderProperties is {}.", placeHolderProperties);
}
}
Spring通過@ConfigurationProperties
注解將配置的placeholder.serverName
和placeholder.url
屬性值分別賦值給PlaceHolderProperties對象的serverName屬性和url屬性;得到如下結果:
對比@Value注解和@ConfigurationProperties注解
(1) 首先需要注意:@Value來自Spring, 而@ConfigurationProperties來自SpringBoot.
(2) 占位符要求變量名以及大小寫完全匹配,如@Value及前面涉及的Spring項目中使用的占位符;但@ConfigurationProperties忽略大小寫且會忽略中劃線,如下所示:
# application.yml
placeholder:
serverName: PlaceHolderServer
url: http://127.0.0.1:8080/phs
#等價于:
placeholder:
SerVer-NA-m-e: PlaceHolderServer
URl: http://127.0.0.1:8080/phs
(3) 此外,@Value不需要強行關聯(lián)變量名與屬性名(通過配置注解的value屬性關聯(lián)),而@ConfigurationProperties需要進行變量名與屬性名稱的關聯(lián);
(4) @Value除了可以使用占位符之外,還可以直接對屬性注入字符串;
最后,建議大家面向Java編程,而不面向Spring編程:一些特殊場景除外,提倡使用@ConfigurationProperties替代@Value。同理,提倡使用構造函數注入而非@Autowired注解注入方式。
前文提到占位符變量的數據來源有配置文件,除此之外還包括系統(tǒng)屬性、應用屬性、環(huán)境變量.
查看機器環(huán)境變量:
準備測試用例:
@Data
@Component
public class EnvProperties {@Value("${HOME}")
private String home;
@Value("${USER}")
private String user;
}
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public class PlaceHolderTest {@Autowired
private EnvProperties envProperties;
@Test
public void testEnvProperties() {LOGGER.info("envProperties is {}.", envProperties);
}
}
得到如下結果:
結果顯示EnvProperties的屬性值與機器對應的環(huán)境變量值保持一致。
Spring給占位符提供了默認配置,待解析字符串的第一個冒號為分隔符,分隔符后的值為默認值。
此時,若未配置servername
變量,則placeHolderServer這個bean的name屬性被設置為默認值placeholder:001
;若未配置url變量,則拋出異常。
Spring解析表達式時會遞歸調用,先解析最內層的變量——得到一個中間值(配置文件中變量配置的值),再解析外圍;這使得${}可以嵌套使用。這個過程中,解析器還會對得到的中間值進行遞歸解析操作,這使得配置文件中的變量也可以引用其他變量。
${}嵌套使用
程序解析時由外向內,但developer在閱讀和編寫時應按照由外到內的順序進行。
如配置文件:placeHolderServer對象的url屬性對應占位符字符串為${PHS_HOST:127.0.0.1:${PHS_PORT:${SERVER_PORT:8080}}}
;不妨假設變量的結果為駝峰形式,即:
# 配置變量
PHS_HOST=phsHost
PHS_PORT=phsPort
SERVER_PORT=serverPort
根據是否配置了PHS_HOST、PHS_PORT、SERVER_PORT變量可以得到4種不同的結果:
配置文件嵌套使用
為了維護方便,配置文件中的變量也可以抽取出公共部分,如下所示:
#application.yml
SERVER_IP: 127.0.0.1
SERVER_PORT: 8080
SERVER_NAME: phc
URL: http://${SERVER_IP}:${SERVER_PORT}/${SERVER_NAME}
API_URL: ${URL}/api
RPC_URL: ${URL}/rpc
3.原理
3.1 前置知識在介紹原理前,需要先熟悉PropertySource相關的幾個類:Properties、PropertySource(PropertiesPropertySource、ResourcePropertySource、MutablePropertySources)、PropertyResolver(PropertySourcesPropertyResolver).
3.1.1 Properties類型介紹public class Properties extends Hashtable
Properties繼承了Hashtable,因此可以看作一個特殊的Map類型(功能加強的Map), 因此Properties也基于鍵值對的存儲結構提供了很多接口,這里只介紹與本文有關的部分。
// 向內存中添加鍵值對
public synchronized Object setProperty(String key, String value) {//...}
// 根據key從內存中讀取數據
public String getProperty(String key) {//...}
// load方法會從InputStream流對象中讀取數據,寫入到Properties中:
public synchronized void load(InputStream inStream) throws IOException {//...}
For example:
在resources資源路徑下準備文件:
// default.properties文件
name=root
passwd=root
測試代碼如下:
@Slf4j
public class PropertiesTest {@Test
public void testProperties() throws IOException {Properties properties = new Properties();
properties.setProperty("key-1", "value-1");
properties.load(this.getClass().getClassLoader().getResourceAsStream("default.properties"));
properties.setProperty("key-2", "value-2");
LOGGER.info("properties is {}.", properties);
}
}
得到如下結果:
因此,Properties可以被用來從配置文件中加載資源入內存。
抽象類PropertySource被定義為資源對象,內部存在兩個屬性:name和泛型的source對象。通過equals
和hashCode
方法可知name屬性被作為判斷PropertySource對象是否相等的依據。
public abstract class PropertySource{// @Getter
protected final String name;
// @Getter
protected final T source;
public boolean containsProperty(String name) {return this.getProperty(name) != null;
}
@Nullable
public abstract Object getProperty(String propertyName);
public boolean equals(@Nullable Object other) {return this == other || other instanceof PropertySource && ObjectUtils.nullSafeEquals(this.getName(), ((PropertySource)other).getName());
}
public int hashCode() {return ObjectUtils.nullSafeHashCode(this.getName());
}
}
定義類一個抽象的getProperty(String propertyName)
方法給自類實現,接口的功能是根據propertyName從source屬性中取值;需要注意propertyName與PropertySource中的name屬性不是同一概念(name僅作為PropertySource對象對外的身份)。PropertySource有兩個比較重要的實現類:PropertiesPropertySource和ResourcePropertySource。
PropertySource的實現類PropertiesPropertySource:
public class PropertiesPropertySource extends MapPropertySource {public PropertiesPropertySource(String name, Properties source) {super(name, source);
}
protected PropertiesPropertySource(String name, Mapsource) {super(name, source);
}
//...
}
PropertiesPropertySource的source屬性為Properties類型;注意Properties是Hashtable的子類,自然也是Map類型的子類。
其父類MapPropertySource實現了PropertySource定義的Object getProperty(String name)
接口:
//MapPropertySource類
public Object getProperty(String key) {return ((Map)this.source).get(key);
}
根據key從Map類型的source對象中取值。
PropertySource的實現類ResourcePropertySource:
ResourcePropertySource作為PropertiesPropertySource的子類,在PropertiesPropertySource基礎上新增了讀取資源文件的能力。
public class ResourcePropertySource extends PropertiesPropertySource {public ResourcePropertySource(Resource resource) throws IOException {super(getNameForResource(resource), PropertiesLoaderUtils.loadProperties(new EncodedResource(resource)));
this.resourceName = null;
}
//...
}
其中PropertiesLoaderUtils.loadProperties(new EncodedResource(resource)))
會根據傳入的Resource對象指定的文件資源去加載、讀取并生成Properties對象。
PropertySource的容器類MutablePropertySources:
MutablePropertySources作為PropertySource的容器,在內部維持了一個PropertySource類型的列表,基于此對外提供了存儲、管理、查詢PropertySource對象能力的API:
public class MutablePropertySources implements PropertySources {private final List>propertySourceList;
//...
}
3.1.3 PropertyResolver類型介紹PropertyResolver接口介紹
public interface PropertyResolver {boolean containsProperty(String key);
String getProperty(String key);
String getProperty(String key, String defaultValue);
T getProperty(String key, ClasstargetType);
T getProperty(String key, ClasstargetType, T defaultValue);
String getRequiredProperty(String key) throws IllegalStateException;
T getRequiredProperty(String key, ClasstargetType) throws IllegalStateException;
String resolvePlaceholders(String text);
String resolveRequiredPlaceholders(String text) throws IllegalArgumentException;
}
PropertyResolver接口定義了根據key獲取value以及處理占位符字符串的能力。
PropertyResolver的實現類PropertySourcesPropertyResolver:
public class PropertySourcesPropertyResolver extends AbstractPropertyResolver {private final PropertySources propertySources;
public PropertySourcesPropertyResolver(PropertySources propertySources) {this.propertySources = propertySources;
}
//...
}
實例化PropertySourcesPropertyResolver對象時,需要傳入一個PropertySources作為入參。String getProperty(String key)
及其重載方法取值的實現原理:遍歷propertySources對象內部的PropertySource對象,依次從中取值,直到取值成功或者遍歷至最后一個PropertySource對象。String resolvePlaceholders(String text)
的入參為待解析的字符串(包含占位符),返回的字符串為解析后的結果。解析過程中需要通過String getProperty(String key)
及其重載方法從PropertySource對象列表中取值。
解析的核心方法在于:
protected String parseStringValue(String value, PropertyPlaceholderHelper.PlaceholderResolver placeholderResolver, @Nullable SetvisitedPlaceholders) { int startIndex = value.indexOf(this.placeholderPrefix);
if (startIndex == -1) { return value;
}
StringBuilder result = new StringBuilder(value);
while (startIndex != -1) { int endIndex = this.findPlaceholderEndIndex(result, startIndex);
if (endIndex != -1) { String placeholder = result.substring(startIndex + this.placeholderPrefix.length(), endIndex);
String originalPlaceholder = placeholder;
if (visitedPlaceholders == null) { visitedPlaceholders = new HashSet(4);
}
if (!((Set) visitedPlaceholders).add(placeholder)) { throw new IllegalArgumentException("Circular placeholder reference '" + placeholder + "' in property definitions");
}
// ??遞歸調用:先處理最內部的占位符
placeholder = this.parseStringValue(placeholder, placeholderResolver, (Set) visitedPlaceholders);
// 📍調用placeholderResolver對象處理占位符
// [placeholderResolver對象內部封裝了配置文件的屬性信息和解析過程]
String propVal = placeholderResolver.resolvePlaceholder(placeholder);
if (propVal == null && this.valueSeparator != null) { int separatorIndex = placeholder.indexOf(this.valueSeparator);
if (separatorIndex != -1) { // 📍取第一個冒號后的字符串為默人值
String actualPlaceholder = placeholder.substring(0, separatorIndex);
String defaultValue = placeholder.substring(separatorIndex + this.valueSeparator.length());
propVal = placeholderResolver.resolvePlaceholder(actualPlaceholder);
if (propVal == null) { // 📍解析失敗時,使用默認值填充
propVal = defaultValue;
}
}
}
if (propVal != null) { // ??遞歸調用:解析得到的變量值(因為配置文件中變量的值也可能包含占位符)
propVal = this.parseStringValue(propVal, placeholderResolver, (Set) visitedPlaceholders);
result.replace(startIndex, endIndex + this.placeholderSuffix.length(), propVal);
startIndex = result.indexOf(this.placeholderPrefix, startIndex + propVal.length());
} else { if (!this.ignoreUnresolvablePlaceholders) { throw new IllegalArgumentException("Could not resolve placeholder '" + placeholder + "' in value \"" + value + "\"");
}
startIndex = result.indexOf(this.placeholderPrefix, endIndex + this.placeholderSuffix.length());
}
((Set) visitedPlaceholders).remove(originalPlaceholder);
} else { startIndex = -1;
}
}
return result.toString();
}
代碼的整體邏輯比較簡單,通過遞歸操作🥷先解析最內側的占位符,得到一個中間值propVal(來源于配置文件或者環(huán)境變量等);propVal可能也包含占位符,因此也需要對其進行解析。遞歸返回后,會按照由外到內的順序依次進行。
3.2 原理Spring框架處理占位符問題時選擇的目標對象是BeanDefinition,因此無論以何種方式引入的Bean,處理過程均可統(tǒng)一。具體的實現方案是引入一個BeanFactoryPostProcessor類型的PropertySourcesPlaceholderConfigurer類,并將解析邏輯封裝在其內部;在Spring容器啟動過程中,通過invokeBeanFactoryPostProcessors(beanFactory);
進行觸發(fā)。
PropertySourcesPlaceholderConfigurer中的postProcessBeanFactory方法:
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {if (this.propertySources == null) {this.propertySources = new MutablePropertySources();
if (this.environment != null) { this.propertySources.addLast(
new PropertySource("environmentProperties", this.environment) {@Override
@Nullable
public String getProperty(String key) {return this.source.getProperty(key);
}
}
);
}
PropertySource>localPropertySource =
new PropertiesPropertySource("localProperties", mergeProperties());
if (this.localOverride) { this.propertySources.addFirst(localPropertySource);
} else { this.propertySources.addLast(localPropertySource);
}
}
processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
this.appliedPropertySources = this.propertySources;
}
主線邏輯較為簡單:
(1) 構建一個MutablePropertySources資源對象容器;
(2) 向其中加入名稱為environmentProperties的PropertySource對象,包含了系統(tǒng)屬性、應用屬性、環(huán)境變量等信息;其中application.yml中配置的屬性也包含在其中;
(3) 向其中加入名稱為localProperties的PropertySource對象,包含了引入的配置文件中的信息;
(4) 將MutablePropertySources作為構造參數創(chuàng)建一個PropertySourcesPropertyResolver解析器對象;
(5) 調用processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
處理占位符。
processProperties方法共兩個入參:beanFactory和PropertySourcesPropertyResolver解析器對象;beanFactory容器能夠獲取所有的BeanDefinition信息,PropertySourcesPropertyResolver解析器對象內部包含了所有的配置信息以及基于此封裝的解析能力。
跟進processProperties方法:
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
final ConfigurablePropertyResolver propertyResolver) throws BeansException {// 根據配置信息設置解析器,使得valueResolver與配置保持一致
doProcessProperties(beanFactoryToProcess, valueResolver);
}
跟進doProcessProperties方法:
// 簡化后,僅突出主線邏輯
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) { BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
for (String curName : beanNames) { BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
visitor.visitBeanDefinition(bd);
}
}
將解析器包裝為BeanDefinitionVisitor對象,遍歷IOC容器中的所有BeanDefinition,調用visitor.visitBeanDefinition(bd);
解析占位符。
跟進visitor.visitBeanDefinition(bd)方法:
public void visitBeanDefinition(BeanDefinition beanDefinition) {visitParentName(beanDefinition);
visitBeanClassName(beanDefinition);
visitFactoryBeanName(beanDefinition);
visitFactoryMethodName(beanDefinition);
visitScope(beanDefinition);
if (beanDefinition.hasPropertyValues()) {visitPropertyValues(beanDefinition.getPropertyValues());
}
if (beanDefinition.hasConstructorArgumentValues()) {ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
visitIndexedArgumentValues(cas.getIndexedArgumentValues());
visitGenericArgumentValues(cas.getGenericArgumentValues());
}
}
從名稱可以看出來,依次解析BeanDefinition的parentName、class、FactoryBeanName、FactoryMethodName、scope等Bean元信息;之后解析屬性值以及構造函數值信息。這些visitXxxx(beanDefinition);
方法內部的實現通過解析器實現占位符的解析。
最后想說一下,解析占位符的過程中涉及很多類,這些類的內部設計和相互引用編織得很巧妙,在寫框架代碼時具備很高的參考意義,建議詳細體會。
你是否還在尋找穩(wěn)定的海外服務器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機房具備T級流量清洗系統(tǒng)配攻擊溯源,準確流量調度確保服務器高可用性,企業(yè)級服務器適合批量采購,新人活動首月15元起,快前往官網查看詳情吧