@PropertySource 註解實現讀取 yml 檔案

123在掘金發表於2019-01-27

記一次在開發中使用@PropertySource註解載入yml檔案

背景: 業務需要建立多個配置類,基於 yml 檔案擁有簡潔的層次結構,遂配置檔案選擇 yml 型別。但在實際的開發中遇到使用 @PropertySource 註解無法載入 yml 配置檔案問題。

分析過程:

首先我們先來分析一下 @PropertySource 註解的原始碼:

public @interface PropertySource { 
/** 載入資源的名稱 */ String name() default "";
/** * 載入資源的路徑,可使用classpath,如: * "classpath:/config/test.yml" * 如有多個檔案路徑放在{
}中,使用','號隔開,如: * {"classpath:/config/test1.yml","classpath:/config/test2.yml"
} * 除使用classpath外,還可使用檔案的地址,如: * "file:/rest/application.properties" */ String[] value();
/** 此屬性為根據資源路徑找不到檔案後是否報錯, 預設為是 false */ boolean ignoreResourceNotFound() default false;
/** 此為讀取檔案的編碼, 若配置中有中文建議使用 'utf-8' */ String encoding() default "";
/** * 關鍵:此為讀取資原始檔的工程類, 預設為: * 'PropertySourceFactory.class' */ Class<
? extends PropertySourceFactory>
factory() default PropertySourceFactory.class;

}複製程式碼

從原始碼可以看出,讀取資原始檔 PropertySourceFactory 介面是關鍵,加下來開啟 PropertySourceFactory 介面的原始碼:

public interface PropertySourceFactory { 
PropertySource<
?>
createPropertySource(@Nullable String var1, EncodedResource var2) throws IOException;

}複製程式碼

發現其中只有一個建立屬性資源介面的方法,接下來我們找到實現這個方法的類:

public class DefaultPropertySourceFactory implements PropertySourceFactory { 
public DefaultPropertySourceFactory() {

} public PropertySource<
?>
createPropertySource(@Nullable String name, EncodedResource resource) throws IOException {
return name != null ? new ResourcePropertySource(name, resource) : new ResourcePropertySource(resource);

}
}複製程式碼

在這個類中我們發現其返回了一個物件 ResourcePropertySource ,找到 DefaultPropertySourceFactory 類使用的兩個 ResourcePropertySource 類的構造方法:

    public ResourcePropertySource(String name, EncodedResource resource) throws IOException { 
super(name, PropertiesLoaderUtils.loadProperties(resource));
this.resourceName = getNameForResource(resource.getResource());

} public ResourcePropertySource(EncodedResource resource) throws IOException {
super(getNameForResource(resource.getResource()), PropertiesLoaderUtils.loadProperties(resource));
this.resourceName = null;

}複製程式碼

在上面程式碼中,兩個構造方法都使用了 PropertiesLoaderUtils.loadProperties() 這個屬性的方法, 一直點下去, 會發現這麼一段程式碼:

static void fillProperties(Properties props, EncodedResource resource, PropertiesPersister persister)   throws IOException { 
InputStream stream = null;
Reader reader = null;
try {
String filename = resource.getResource().getFilename();
// private static final String XML_FILE_EXTENSION = ".xml";
if (filename != null &
&
filename.endsWith(XML_FILE_EXTENSION)) {
stream = resource.getInputStream();
persister.loadFromXml(props, stream);

} else if (resource.requiresReader()) {
reader = resource.getReader();
persister.load(props, reader);

} else {
stream = resource.getInputStream();
persister.load(props, stream);

}
} finally {
if (stream != null) {
stream.close();

} if (reader != null) {
reader.close();

}
}
}複製程式碼

由上可知,@PropertySource 註解也可以用來載入 xml 檔案,接下來根據 persister.load(props, stream) 方法一直點下去會找到下面一段程式碼:

private void load0 (LineReader lr) throws IOException { 
char[] convtBuf = new char[1024];
int limit;
int keyLen;
int valueStart;
char c;
boolean hasSep;
boolean precedingBackslash;
/** * 每次讀取一行 */ while ((limit = lr.readLine()) >
= 0) {
c = 0;
keyLen = 0;
valueStart = limit;
hasSep = false;
//System.out.println("line=<
"
+ new String(lineBuf, 0, limit) + ">
"
);
precedingBackslash = false;
/** * 遍歷一行字的每一個字元 * 若字元中出現 '='':'' ''\t''\f' 則跳出迴圈 */ while (keyLen <
limit) {
c = lr.lineBuf[keyLen];
//need check if escaped. // 如果當前遍歷字元為 '='':' 則跳出迴圈 if ((c == '=' || c == ':') &
&
!precedingBackslash) {
valueStart = keyLen + 1;
hasSep = true;
break;

} // 如果當前遍歷字元為 ' ''\t''\f' 跳出迴圈, // 但在接下來的迴圈中還需要繼續遍歷知道找到 '='':' else if ((c == ' ' || c == '\t' || c == '\f') &
&
!precedingBackslash) {
valueStart = keyLen + 1;
break;

} // 檢查是否轉義 if (c == '\\') {
precedingBackslash = !precedingBackslash;

} else {
precedingBackslash = false;

} // 每次迴圈,keyLen + 1 keyLen++;

} /** * 判斷valueStart(值的開始下標)是否小於讀取行的長度,若小於,則進入迴圈 */ while (valueStart <
limit) {
c = lr.lineBuf[valueStart];
// 判斷當前字元是否等於空格、製表符、換頁符。都不等於則進入迴圈 if (c != ' ' &
&
c != '\t' &
&
c != '\f') {
// 當 hasSep 為false時代表上個 while (keyLen <
limit) 迴圈跳出時c為 空格或製表符或換頁符 // 這裡繼續迴圈直到找到'='':'號為止 // 由此可見 在配置檔案中'='':' 號前可有空格、製表符、換頁符 if (!hasSep &
&
(c == '=' || c == ':')) {
hasSep = true;

} else {
break;

}
} // 每次迴圈,valueStart + 1 valueStart++;

} // 獲取配置檔案中的key,value並儲存 String key = loadConvert(lr.lineBuf, 0, keyLen, convtBuf);
String value = loadConvert(lr.lineBuf, valueStart, limit - valueStart, convtBuf);
put(key, value);

}
}複製程式碼

上面 load0 方法每次讀取一行,然後根據 '='':' 來獲取 keyvalue,而 yml 具有鮮明層次結構的特點則不能由此方法讀取。

綜上分析可知,@PropertySource 註解讀取屬性檔案的關鍵在於 PropertySourceFactory 介面中的 createPropertySource 方法,所以我們想要實現 @PropertySource 註解讀取 yml 檔案就需要實現 createPropertySource 方法,在 @PropertySource 註解其是通過 DefaultPropertySourceFactory 類來實現這個方法,我們只需要繼承此類,並重寫其 createPropertySource 方法即可,實現程式碼如下:

    @Override    public PropertySource<
?>
createPropertySource(String name, EncodedResource resource) throws IOException {
if (resource == null){
return super.createPropertySource(name, resource);

} List<
PropertySource<
?>
>
sources = new YamlPropertySourceLoader().load(resource.getResource().getFilename(), resource.getResource());
return sources.get(0);

}複製程式碼

注: spring bootymlyaml 對應的載入類為 YamlPropertySourceLoader

測試

@Component@PropertySource(value = "test.yml", encoding = "utf-8", factory = TestFactory.class)@ConfigurationProperties(prefix = "com.test")public class IdCardServerConfig { 
private String serverCode;
...
}複製程式碼

來源:https://juejin.im/post/5c4aea2e51882522c03ea475#comment

相關文章