@PropertySource 註解實現讀取 yml 文件

記一次在開發中使用@PropertySource註解加載yml文件

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

分析過程:

首先咱們先來分析一下 @PropertySource 註解的源碼:bash

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 接口的源碼:app

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

複製代碼

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

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() 這個屬性的方法, 一直點下去, 會發現這麼一段代碼:ui

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) 方法一直點下去會找到下面一段代碼:this

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 對應的加載類爲 YamlPropertySourceLoaderspa

測試

@Component
@PropertySource(value = "test.yml", encoding = "utf-8", factory = TestFactory.class)
@ConfigurationProperties(prefix = "com.test")
public class IdCardServerConfig {

    private String serverCode;
    
    ...
}
複製代碼
相關文章
相關標籤/搜索