Spring註解系列——@PropertySource

2023-04-07 12:00:29

在Spring框架中@PropertySource註解是非常常用的一個註解,其主要作用是將外部化設定解析成key-value鍵值對"存入"Spring容器的Environment環境中,以便在Spring應用中可以通過@Value或者預留位置${key}的形式來使用這些設定。

使用案列

// @PropertySource需要和@Configuration配個使用
// @PropertySource載入的組態檔時需要注意載入的順序,後面載入的設定會覆蓋前面載入的設定
// @PropertySource支援重複註解
// value值不僅支援classpath表示式,還支援任意合法的URI表示式
@Configuration
@PropertySource(value = "classpath:/my.properties",encoding = "UTF8")
@PropertySource(value = "classpath:/my2.properties",encoding = "UTF8",ignoreResourceNotFound = true)
public static class PropertyConfig {
}

@Component
public class App {
    @Value("${key1:default-val}")
    private String value;

    @Value("${key2:default-val2}")
    private String value2;
}

下面是組態檔my.properties和my2.properties的具體內容。

# my.properties
key1=自由之路

# my2.properties
key1=程式設計師
key2=自由之路

Spring容器啟動時,會將my.properties和my2.properties的內容載入到Environment中,並在App類的依賴注入環節,將key1和key2的值注入到對應的屬性。

自定義PropertySource工廠

閱讀@PropertySource的原始碼,我們發現還有一個factory屬性。從這個屬性的字面意思看,我們不難猜測出這個屬性設定的是用於產生PropertySource的工廠。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Repeatable(PropertySources.class)
public @interface PropertySource {

	String name() default "";
    
	String[] value();
	
    boolean ignoreResourceNotFound() default false;

	String encoding() default "";

	Class<? extends PropertySourceFactory> factory() default PropertySourceFactory.class;

}

要深入理解PropertySourceFactory,我們先要知道以下的背景知識。

在Spring中,設定的來源有很多。Spring將設定來源統一抽象成 PropertySource 這個抽象類,Spring中內建的常用的 PropertySource 有以下這些

  • MapPropertySource

  • CommandLinePropertySource

  • PropertiesPropertySource

  • SystemEnvironmentPropertySource

  • ResourcePropertySource

ResourcePropertySource這個類將一系列設定來源統一成ResourcePropertySource,可以說是對 PropertySource 的進一步封裝。

PropertySourceFactory 介面,用於產生PropertySource。Spring中,PropertySourceFactory 預設的實現是DefaultPropertySourceFactory,用於生產 ResourcePropertySource。

經過上面的介紹,我們知道如果沒有設定@PropertySource的factory屬性的話,預設的PropertySourceFactory使用的就是DefaultPropertySourceFactory。當然,我們也可以自定義PropertySourceFactory,用於「生產」我們自定義的PropertySource。下面就演示一個將yaml檔案解析成MapPropertySource的使用案列。

/**
 * Spring中內建的解析yaml的處理器
 * YamlProcessor
 *  - YamlMapFactoryBean  --> 解析成Map
 *  - YamlPropertiesFactoryBean  --> 解析成Properties
 */
public class YamlMapSourceFactory implements PropertySourceFactory {
    @Override
    public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
        YamlMapFactoryBean yamlMapFactoryBean = new YamlMapFactoryBean();
        yamlMapFactoryBean.setResources(resource.getResource());
        Map<String, Object> map = yamlMapFactoryBean.getObject();
        return new MapPropertySource(name, map);
    }
}

// 加了factory屬性,必須加name屬性
// 有了factory機制,我們可以做很多自定一的擴充套件,比如設定可以從遠端來
@PropertySource(name = "my.yaml",value = "classpath:/my.yaml",encoding = "UTF8",factory = YamlMapSourceFactory.class)
public static class PropertyConfig {
}

原理簡析

到這邊我們對@PropertySource已經有了一個感性的認識,知道了其主要作用是將各種型別的外部化組態檔以key-value的形式載入到Spring的Environment中。這個部分我們從原始碼的角度來分析下Spring是怎麼處理@PropertySource這個註解的。分析原始碼可以加深我們對@PropertySource的認識(看原始碼不是目的,是為了加深理解,學習Spring的設計思想)。

@PropertySource註解的處理是在ConfigurationClassPostProcessor中進行觸發的。最終會呼叫到ConfigurationClassParser的processPropertySource方法。

// ConfigurationClassParser#processPropertySource
private void processPropertySource(AnnotationAttributes propertySource) throws IOException {
    String name = propertySource.getString("name");
    if (!StringUtils.hasLength(name)) {
        name = null;
    }
    String encoding = propertySource.getString("encoding");
    if (!StringUtils.hasLength(encoding)) {
        encoding = null;
    }
    String[] locations = propertySource.getStringArray("value");
    Assert.isTrue(locations.length > 0, "At least one @PropertySource(value) location is required");
    boolean ignoreResourceNotFound = propertySource.getBoolean("ignoreResourceNotFound");

    Class<? extends PropertySourceFactory> factoryClass = propertySource.getClass("factory");
    // 如果有自定義工廠就使用自定義工廠,沒有自定義工廠就使用DefaultPropertySourceFactory
    PropertySourceFactory factory = (factoryClass == PropertySourceFactory.class ?
            DEFAULT_PROPERTY_SOURCE_FACTORY : BeanUtils.instantiateClass(factoryClass));
    // 遍歷各個location地址
    for (String location : locations) {
        try {
            // location地址支援預留位置的形式
            String resolvedLocation = this.environment.resolveRequiredPlaceholders(location);
            // 獲取Resource
            Resource resource = this.resourceLoader.getResource(resolvedLocation);
            addPropertySource(factory.createPropertySource(name, new EncodedResource(resource, encoding)));
        }
        catch (IllegalArgumentException | FileNotFoundException | UnknownHostException | SocketException ex) {
            // Placeholders not resolvable or resource not found when trying to open it
            if (ignoreResourceNotFound) {
                if (logger.isInfoEnabled()) {
                    logger.info("Properties location [" + location + "] not resolvable: " + ex.getMessage());
                }
            }
            else {
                throw ex;
            }
        }
    }
}

總的來說,Spring處理@PropertySource的原始碼非常簡單,這邊就不再過多贅述了。