記一次在開發中使用@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
方法每次讀取一行,然後根據 '='
或 ':'
來獲取 key
和 value
,而 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 boot
中 yml
、yaml
對應的載入類為 YamlPropertySourceLoader
。
測試
@Component@PropertySource(value = "test.yml", encoding = "utf-8", factory = TestFactory.class)@ConfigurationProperties(prefix = "com.test")public class IdCardServerConfig {
private String serverCode;
...
}複製程式碼