天天看點

PropertySourcesPlaceholderConfigurer的原理及實戰注入機

作者:Java熱點

Spring提供配置解析功能

主要有一下xml檔案占位符解析和Java的屬性@Value的占位符解析配置這兩種場景進行分析和實作解析,如下面兩種案例。

PropertySourcesPlaceholderConfigurer的原理及實戰注入機

xml檔案的占位符解析配置

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"  init-method="init" destroy-method="close">
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>
複制代碼           

Java的屬性@Value的占位符解析配置

@Value 注解值進行屬性占位符解析和替換

@Value("${config}")
private String config;
複制代碼           

PropertyPlaceholderConfigurer和PropertySourcesPlaceholderConfigurer

通過配置xml來實作對Classpath下的配置檔案的占位符的屬性進行注入,或者實作Java的屬性@Value的占位符解析配置。

  • 在Spring3.1版本之前是通過PropertyPlaceholderConfigurer實作的。
  • 在Spring3.1之後則是通過PropertySourcesPlaceholderConfigurer實作的。
注意:在Spring Context 3.1或者更高版本中,預設使用PropertySourcesPlaceholderConfigurer工具替換了PlaceholderConfigurerSupport,而<=3.0較老的Spring Context中,為了保持和之前的版本相容,預設還是使用PropertyPlaceholderConfigurer。

PropertyPlaceholderConfigurer和PropertySourcesPlaceholderConfigurer的實作分析

  • PropertyPlaceholderConfigurer本質是基于PlaceholderConfigurerSupport實作讀取配置的。
  • PropertySourcesPlaceholderConfigurer是PlaceholderConfigurerSupport的特殊化實作。

下圖介紹對應的配置解析的繼承關系圖譜。

PropertySourcesPlaceholderConfigurer的原理及實戰注入機

PropertyPlaceholderConfigurer和PropertySourcesPlaceholderConfigurer的執行目标

PropertyPlaceholderConfigurer和PropertyPlaceholderConfigurer在使用上并無本質的差別,兩者的根本目标是将配置檔案生成KV對,真正的注入工作并不由它們本身執行。

PropertySourcesPlaceholderConfigurer它用于解析bean定義中的屬性值,以及注解@Value的值,使用的屬性來源是目前的Spring Environment對象,以及設定給自己的PropertySources對象。

Spring Boot 自動配置類 PropertyPlaceholderAutoConfiguration

@Configuration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class PropertyPlaceholderAutoConfiguration {
	@Bean
	@ConditionalOnMissingBean(search = SearchStrategy.CURRENT)
	public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
		return new PropertySourcesPlaceholderConfigurer();
	}
}
複制代碼           

PropertyPlaceholderAutoConfiguration定義一個PropertySourcesPlaceholderConfigurer bean,該bean作為一個BeanFactoryPostProcessor,會在容器啟動時容器後置處理階段執行自己的任務。BeanFactoryPostProcessor的優先級又優于其餘的Bean。是以可以實作在bean初始化之前的注入。

postProcessBeanFactory方法的執行

如果外部指定了this.propertySources, 則直接使用它,否則從目前Spring的Environment 對象和自身的 #mergeProperties 方法調用傳回的 Properties 對象建構屬性源對象 this.propertySources

@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
		if (this.propertySources == null) {
			this.propertySources = new MutablePropertySources();
			if (this.environment != null) {
				this.propertySources.addLast(
					new PropertySource<Environment>(ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME, 
						this.environment) {
						@Override
						@Nullable
						public String getProperty(String key) {
							return this.source.getProperty(key);
						}
					}
				);
			}
			try {
				PropertySource<?> localPropertySource =
						new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties());
				if (this.localOverride) {
					this.propertySources.addFirst(localPropertySource);
				}
				else {
					this.propertySources.addLast(localPropertySource);
				}
			}
			catch (IOException ex) {
				throw new BeanInitializationException("Could not load properties", ex);
			}
		}
		processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources));
		this.appliedPropertySources = this.propertySources;
	}
複制代碼           

構造一個基于特定屬性源 this.propertySources 對屬性值進行解析的屬性值解析器PropertySourcesPropertyResolver, 對容器中所有的 bean 定義中的屬性值,構造函數參數值。

/**
	 * Visit each bean definition in the given bean factory and attempt to replace ${...} property
	 * placeholders with values from the given properties.
	 */
	protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
			final ConfigurablePropertyResolver propertyResolver) throws BeansException {
       // 設定屬性值解析器所使用的占位符格式參數,預設為:
       // 占位符字首 ${
		propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
       // 占位符字尾 }
		propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
       // 預設值分隔符 :
		propertyResolver.setValueSeparator(this.valueSeparator);
       // 結合屬性 this. ignoreUnresolvablePlaceholders對propertyResolver 作進一步封裝,
       // 封裝出來一個 StringValueResolver valueResolver,這是最終要應用的屬性值解析器
		StringValueResolver valueResolver = strVal -> {
			String resolved = (this.ignoreUnresolvablePlaceholders ?
					propertyResolver.resolvePlaceholders(strVal) :
					propertyResolver.resolveRequiredPlaceholders(strVal));
			if (this.trimValues) {
				resolved = resolved.trim();
			}
			return (resolved.equals(this.nullValue) ? null : resolved);
		};
       // 調用基類PlaceholderConfigurerSupport實作的對容器中所有 bean定義進行周遊處理屬性值中占位符解析的邏輯
		doProcessProperties(beanFactoryToProcess, valueResolver);
	}
複制代碼           

doProcessProperties的方法目的是為了添加解析器StringValueResolver

protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
            StringValueResolver valueResolver) {
        // ignore
        ....

        // New in Spring 2.5: resolve placeholders in alias target names and aliases as well.
        beanFactoryToProcess.resolveAliases(valueResolver);

        // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes.
        beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
    }
複制代碼           

這裡的ddEmbeddedValueResolver(StringValueResolver) 是為一個 LinkedList添加值。在取用的時候是優先從連結清單頭開始取用的。 一旦發現無法找到值,直接就抛異常了。這個就對外展現出 PropertySourcesPlaceholderConfigurer 的唯一性。

然而Spring内部還是有多個PropertySourcesPlaceholderConfigurer, 隻不過除了排列在隊首的 PropertySourcesPlaceholderConfigurer之外全都被忽略掉了。

PropertySourcesPlaceholderConfigurer屬性注入的原理

AbstractApplicationContext#obtainFreshBeanFactory

Spring架構進行植入元素注入時機

針對于元素的注入依賴于 AutowiredAnnotationBeanPostProcessor#postProcessPropertyValues1。

AbstractApplicationContext#finishBeanFactoryInitialization方法

在Spring初始化流程中,執行AbstractApplicationContext#finishBeanFactoryInitialization方法。 該方法裡面發生的主要流程為Spring業務Bean初始化。 實際流程跟Spring Bean的初始化沒有任務差別。

InstantiationAwareBeanPostProcessor

  • 通過對接口 InstantiationAwareBeanPostProcessor 實作類的方法進行執行。 僅此而已。

AutowiredAnnotationBeanPostProcessor

  • InjectionMetadataInjectionMetadataInjectedElementInjectedElement這個類是 InstantiationAwareBeanPostProcessor的一個實作類。

@Value和@Autowired注解實際執行

  1. 用于@Value和@Autowired注解實際執行方法postProcessPropertyValues排程實際排程InjectedElement子類被注入值的擷取來自于DefaultListableBeanFactory将對應@Value(“${configValue}”)裡面的值替換的來源值,是PropertySourcesPlaceholderConfigurer生成的StringValueResolver。
  2. Spring原生的Bean是單例的它直接被儲存在了AbstractBeanFactory執行Field.set(Object, Object)或者Method.invoke(Object, Object[])。
是以,可以看出 PropertySourcesPlaceholderConfigurer 或者 PropertyPlaceholderConfigurer僅僅是做了一個配置檔案的解析工作,真正的注入并不由它們完成,而是托付給了Spring 的Bean初始化流程。這兩個類實作了BeanFactoryPostProcessor 接口,這個接口的優先級高于後續的Spring Bean。

通過解析了的PropertySourcesPlaceholderConfigurer查詢得到元素值。 沒有則抛出異常,如下源碼:

DefaultListableBeanFactory#doResolveDependency

@Value 注解值進行屬性占位符解析和替換

// 擷取注解的 value() 值。被寫死為 Class<? extends Annotation> valueAnnotationType = Value.class;
// 見類 QualifierAnnotationAutowireCandidateResolver
Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor);
if (value != null) {
    if (value instanceof String) {
        // 通過PropertySourcesPlaceholderConfigurer寫入的鍵值對元素擷取元素的值.
        // 方法内注冊了多個StringValueResolver,循環查找值。提供者為PropertySourcesPlaceholderConfigurer,是以配置多個解析器的時候是以最後的配置為準的。
        String strVal = resolveEmbeddedValue((String) value);
        BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null);
        value = evaluateBeanDefinitionString(strVal, bd);
    }
    TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter());
    return (descriptor.getField() != null ?
            converter.convertIfNecessary(value, type, descriptor.getField()) :
            converter.convertIfNecessary(value, type, descriptor.getMethodParameter()));
}
複制代碼           

讀取配置的方式介紹

xml檔案讀取配置資訊案例

通過PropertyPlaceholderConfigurer進行配置Bean方式

單個配置檔案。

<bean id="propertyConfigurer"class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
   <property name="location">
     <value>conf/sqlmap/jdbc.properties</value>
   </property>
    <property name="fileEncoding">
      <value>UTF-8</value>
    </property>
</bean>
複制代碼           

多個配置檔案

注意這兩種value值的寫法

<bean id="propertyConfigurer"class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">  
    <property name="locations">
        <list>
            <value>/WEB-INF/mail.properties</value>  
            <value>classpath: conf/sqlmap/jdbc.properties</value>
     </list>
    </property>
</bean>
複制代碼           

Spring标簽方式

<context:property-placeholder location="classpath*:/WEB-INF/mail.properties" />
複制代碼           

這總方式的原理就是構造一個PropertySourcesPlaceholderConfigurer, (3.1之前是PropertyPlaceholderConfigurer)

  • ContextNamespaceHandler#init
  • PropertyPlaceholderBeanDefinitionParser#doParse

注入配置觸發點

Spring初始化Context的時候讀取XML配置, 這個流程優先于Spring普通Bean初始化。配合掃包(<context:component-scan />)得到的Bean進而實作對XML裡面配置的Bean的載入。

  • PropertySourcesPlaceholderConfigurer本質上是一個BeanFactoryPostProcessor。解析XML的流程在BeanFactoryPostProcessor之前, 優先将配置檔案的路徑以及名字通過Setter傳入PropertySourcesPlaceholderConfigurer。

總結Spring Value注入流程

建構PropertySourcesPlaceholderConfigurerBean或者PropertyPlaceholderConfigurerBean的元件

  1. 配置Spring @Value("val2Inject") 方式擷取配置檔案的屬性,需要依賴于在Spring XML裡面配置<context:property-placeholder /> 或者PropertySourcesPlaceholderConfigurerBean來添加配置檔案的名稱。
  2. 讀取到context:property-placeholder标簽或者PropertySourcesPlaceholderConfigurer 解析并執行個體化一個PropertySourcesPlaceholderConfigurer。同時向其中注入配置檔案路徑、名稱PropertySourcesPlaceholderConfigurer自身生成多個StringValueResolver備用,Bean準備完畢。
  3. Spring在初始化非BeanFactoryPostProcessor的Bean的時候,AutowiredAnnotationBeanPostProcessor負責找到Bean内有@Value注解的Field或者Method
  4. 通過PropertySourcesPlaceholderConfigurer尋找合适的StringValueResolver并解析得到val值。注入給@Value的Field或Method。
  5. AutowiredAnnotationBeanPostProcessor負責@Autowired和@Value兩個注解的解析。

@PropertySource注解配置讀取單個或多個配置檔案

單個配置檔案:

@PropertySource(value = "classpath:config/application-config.properties")
複制代碼           

多個配置檔案:

@PropertySource(value = {"classpath:config/application-config1.properties","classpath:config/application-config2.properties"})
複制代碼           

@PropertySource注解使用有兩種方式

  1. @PropertySource + Environment,通過@PropertySource注解将properties配置檔案中的值存儲到Spring的Environment中,Environment接口提供方法去讀取配置檔案中的值,參數是properties檔案中定義的key值。
  2. @PropertySource(PropertySourcesPlaceholderConfigurer) +@Value

@PropertySource + Environment

@Configuration
@ComponentScan(basePackages = "com.libo.config")
@PropertySource(value = "classpath:config/application-config.properties")
public class TestPropertieEnvironment { 

    @Autowired
    Environment environment;
    public String properties(){
        String key = this.environment.getProperty("config.key");
        System.out.println(key);
        return null;
    }
}
複制代碼           

配置檔案config.properties:

config.key=1
config.value=2
複制代碼           

測試類操作

public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(TestPropertieEnvironment.class);
        ServiceConfiguration hc2 = (TestPropertieEnvironment) context.getBean("testPropertieEnvironment");
        hc2.properties();
    }
}
複制代碼           

@PropertySource(PropertySourcesPlaceholderConfigurer)+@Value

PropertySourcesPlaceholderConfigurer是PlaceholderConfigurerSupport的特殊化實作。它用于解析bean定義中的屬性值,以及注解@Value的值,使用的屬性來源是目前的Spring Environment對象,以及設定給自己的PropertySources對象。

  • 大于3.1更高版本中,預設使用該工具替換了PlaceholderConfigurerSupport
  • <=3.0較老的Spring中,為了保持和之前的版本相容,預設還是使用PropertyPlaceholderConfigurer。

建立PropertySourcesPlaceholderConfigurer

建立PropertiesConfig

@Component
@PropertySource(value = "classpath:config/application-config.properties")
public class PropertiesConfig {
    @Value("${config.value}")
    private String value;
    @Value("${config.key}")
    private String key;
 
}
複制代碼           
測試類忽略!

自定義PropertyPlaceholderConfigurer

@Configuration
@ComponentScan(basePackages = "com.libo.config")
public class PropertiesConfiguration2 { 

    @Bean 
    public static PropertyPlaceholderConfigurer configurer() { 
         PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
         Resource resources = new ClassPathResource( "config/appplication-config.properties" );
         ppc.setLocation(resources);
         return ppc; 
    } 
    
    @Bean
    public Configs2 configs2(@Value("${ds.user}") String user, @Value("${key1}") String key1) {
        Configs2 configs = new Configs2();
        configs.setApiKeyId(user);
        configs.setSecretApiKey(key1);
        System.out.println("in ServiceConfiguration" + configs);
        return configs;
    }
    
}

@Service
public class TestConfigs2 {

    @Autowired
    Configs2 configs2;
    
    @Autowired
    Configs configs;
    
    public void testConfigs2() {
        System.out.println("configs:"+configs.getApiKeyId());
        System.out.println("configs2:"+configs2.getApiKeyId());
    }
}
複制代碼           

測試類

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(ServiceConfiguration2.class);       
        TestConfigs2 hc2 = (TestConfigs2) context.getBean("testConfigs2");
        hc2.testConfigs2();
    }
}
複制代碼           

此外需要注意的是:PropertySource是可以支援ignoreResourceNotFound支援無法擷取配置檔案的i情況。

Spring4版本的PropertySources的注解

在Spring 4版本中,Spring提供了一個新的注解——@PropertySources,從名字就可以猜測到它是為多配置檔案而準備的。

@PropertySources({
//@PropertySource("classpath:db.properties"),
@PropertySource(value="classpath:db.properties", ignoreResourceNotFound=true),
@PropertySource("classpath:spring/config.properties")
	public class AppConfig {
    @Value("${key1}")
    private String key1;
    
    @Value("${key2}")
    private String key2;

    @Override
    public String toString() {
        return "AppConfig [key1=" + key1 + ", key2=" + key2 + "]";
    } 
}           

繼續閱讀