BeanFactory后置处理器之PropertySourcesPlaceholderConfigurer
- Spring 中的实例
- SpringBoot 中的实例
Spring原来xml配置的时候,经常使用application.properties的配置数据库源码
<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.user}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
慢慢演变使用@Value注解,通过Java代码配置:
public class JdbcBean {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.user}")
private String user;
@Value("${jdbc.password}")
private String password;
}
那么这个是如何实现的呢?原来,Spring提供了一种配置解析的功能,在Spring3.1版本之前是通过PropertyPlaceholderConfigurer实现的。而3.1之后则是通过PropertySourcesPlaceholderConfigurer 实现的。Spring已经发展到5.x了,所以今天我们主要来解析一下PropertySourcesPlaceholderConfigurer 。
自定义一个PropertySourcesPlaceholderConfigurer
我们可以在代码中,创建一个PropertySourcesPlaceholderConfigurer,并指定它解析的配置文件地址,如下:
@Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(){
PropertySourcesPlaceholderConfigurer propertySources = null;
try{
propertySources = new PropertySourcesPlaceholderConfigurer();
ClassPathResource classPathResource = new ClassPathResource("application.properties");
propertySources.setLocation(classPathResource);
}catch(Exception ex){
ex.printStackTrace();
}
return propertySources;
}
使用注解方式:
@Component
@PropertySource("classpath:application.properties")
public class JdbcBean {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.user}")
private String user;
@Value("${jdbc.password}")
private String password;
}
Spring Bean的创建过程
首先我们来看一下Bean的创建过程(AbstractApplicationContext的refresh过程中的一些调用),由于这个过程在这里不是我们主要要讲解的,所以大略体会一下,并且记住其中有一个叫BeanFactoryPostProcessor的,后面我们还会提到它,创建过程如下:
- 实例化BeanFactoryPostProcessor实现类
- 调用BeanFactoryPostProcessor#postProcessBeanFactory
- 实例化BeanPostProcessor实现类
- 调用InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation
- 实例化Bean
- 调用InstantiationAwareBeanProcessor#postProcessAfterInstantiation
- 调用InstantiationAwareBeanPostProcessor#postProcessPropertyValues
- 为Bean注入属性
- 调用BeanNameAware#setBeanName
- 调用BeanClassLoaderAware#setBeanClassLoader
- 调用BeanFactoryAware#setBeanFactory
- 调用BeanPostProcessor#postProcessBeforeInitialization
- 调用InitializingBean#afterPropertiesSet
- 调用Bean的init-method
- 调用BeanPostProcessor#postProcessAfterInitialization
源码分析
数据源加载
在Spring3.1之后,建议使用PropertySourcesPlaceholderConfigurer来取代PropertyPlaceholderConfigurer。
可以发现,其实PropertySourcesPlaceholderConfigurer是BeanFactoryPostProcessor的一个子类,所以在Bean的创建过程中可以得知,在执行过程中会调用postProcessBeanFactory,所以我们查找下对应的方法,定义如下:
@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;
}
属性来源分为两种:
- 以Environment为属性源的environmentProperties
- 通过loadProperties(Properties props)加载本地资源文件作为属性源的localProperties。属性源加载完毕后,将占位符替换为属性源中的属性。
占位符解析
属性源都加载完毕,接下来就是占位符的填充,源码如下:
protected void processProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
final ConfigurablePropertyResolver propertyResolver) throws BeansException {
//this.placeholderPrefix为 ${
propertyResolver.setPlaceholderPrefix(this.placeholderPrefix);
//this.placeholderSuffix为 }
propertyResolver.setPlaceholderSuffix(this.placeholderSuffix);
//this.valueSeparator为 :
propertyResolver.setValueSeparator(this.valueSeparator);
// 使用lambda表达式创建一个StringValueResolver
StringValueResolver valueResolver = strVal -> {
// 解析占位符,此处只能解析占位符
String resolved = (ignoreUnresolvablePlaceholders ?
propertyResolver.resolvePlaceholders(strVal) :
propertyResolver.resolveRequiredPlaceholders(strVal));
if (trimValues) {
resolved = resolved.trim();
}
return (resolved.equals(nullValue) ? null : resolved);
};
// 调用父类的doProcessProperties 把属性扫描到Bean的身上去
doProcessProperties(beanFactoryToProcess, valueResolver);
}
protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess,
StringValueResolver valueResolver) {
BeanDefinitionVisitor visitor = new BeanDefinitionVisitor(valueResolver);
String[] beanNames = beanFactoryToProcess.getBeanDefinitionNames();
for (String curName : beanNames) {
//排除自身&&必须是同一个beanFactory
if (!(curName.equals(this.beanName) && beanFactoryToProcess.equals(this.beanFactory))) {
BeanDefinition bd = beanFactoryToProcess.getBeanDefinition(curName);
try {
visitor.visitBeanDefinition(bd);
}
catch (Exception ex) {
throw new BeanDefinitionStoreException(bd.getResourceDescription(), curName, ex.getMessage(), ex);
}
}
}
// 解析别名目标名称和别名中的占位符
beanFactoryToProcess.resolveAliases(valueResolver);
//在嵌入值(例如注释属性)中解析占位符
beanFactoryToProcess.addEmbeddedValueResolver(valueResolver);
}
上面就是对PropertySourcesPlaceholderConfigurer工作原理的源码解析,概括来说分为两步:
- 属性源装配
- environmentProperties
- localProperties
- 占位符解析
- 解析占位符中的key
- 将key替换成对应的属性值
PropertySourcesPlaceholderConfigurer因为汇聚了Environment、多个PropertySource;所以它能够控制取值优先级、顺序,并且还提供了访问的方法,后期再想获取也不成问题。
@Autowired
private ApplicationContext applicationContext;
// 通过它,可以把生效的配置都拿到
@Autowired
private PropertySourcesPlaceholderConfigurer configurer;
public void getData() {
Environment environment = applicationContext.getEnvironment();
PropertySources appliedPropertySources = configurer.getAppliedPropertySources();
System.out.println(environment.containsProperty("bean.scope")); //false 注意环境里是没有这个key的
System.out.println(appliedPropertySources);
// 获取环境的和我们自己导入的
PropertySource<?> envProperties = appliedPropertySources.get(PropertySourcesPlaceholderConfigurer.ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME);
PropertySource<?> localProperties = appliedPropertySources.get(PropertySourcesPlaceholderConfigurer.LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME);
System.out.println(envProperties.getSource() == environment); //true 可以看到这个envProperties的source和环境里的是同一个
System.out.println(localProperties.containsProperty("bean.scope"));//true 本地配置里是包含这个属性的
}
还有另外一个类PropertyOverrideConfigurer,PropertyOverrideConfigurer类似于PropertySourcesPlaceholderConfigurer,与PropertyPlaceholderConfigurer 不同的是:PropertyOverrideConfigurer 利用属性文件的相关信息,覆盖XML 配置文件中定义。即PropertyOverrideConfigurer允许XML 配置文件中有默认的配置信息。
需要注意的是Properties属性文件:
beanName.property=value //第一个.前面一定是beanName
请保证这个beanName一定存在。它会根据beanName找到这个bean,然后override这个bean的相关属性值的。
因为这个类使用得相对较少,但使用步骤基本同上,因此此处就不再叙述了。
Springboot多属性文件配置
配置文件后缀有两种: .properties和.yml
要完成多属性配置需要自定义PropertySourcesPlaceholderConfigurer 这个Bean
properties配置方法
/**
* 这里必须是static函数
* 如果不是 application.propertise 将读取不到
* application.properties 是默认加载的,这里配置自己的properties就好
* @return
*/
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
// properties 加载方式
Resource[] resources = {
//new ClassPathResource("application.properties"),
//ClassPathResource针对的是resource目录下的文件
new ClassPathResource("jdbc.properties")
};
configurer.setLocations(resources);
return configurer;
}
注意:这个Bean 的函数必须是static的,否则会加载不到application.properties中的内容
这里不需要将application.properties也加进来,因为application.properties是默认加进来的,这里只要写其他的属性文件就好了
yml配置方法
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
// yml 加载方式
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
//这里可以传入多个自定义属性文件
yaml.setResources(new ClassPathResource("jdbc.yml"));
configurer.setProperties(yaml.getObject());
configurer.setLocations(resources);
return configurer;
}
yml 和properties的配置的注意点是一样的,只是需要YamlPropertiesFactoryBean来加载
-
如果引用到属性文件中的值
jdbc.propertise
jdbc.url=jdbc:mysql://localhost:3306/test?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&transformedBitIsBoolean=true&serverTimezone=GMT%2B8 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.user=root jdbc.password=root@123
JdbcConfig.java
@Data @Configuration // 这里不需要了 //@PropertySource("classpath:jdbc.properties") public class JdbcConfig { @Value("${jdbc.url}") private String url; @Value("${jdbc.driver}") private String driver; @Value("${jdbc.user}") private String user; @Value("${jdbc.password}") private String password; }
这里用@Configuration或@Component都可以获得到值,
注意:这里不需要使用@PropertySource了,直接用就可以了
在Controller中使用(其他地方也可,这里是举例)
public class TestController { @Autowired private JdbcConfig jdbcConfig; @PostMapping("/hello") public String Hello() { return jdbcConfig.getUrl() + " " + jdbcConfig.getDriver() + " " + jdbcConfig.getUser() + " " + jdbcConfig.getPassword(); }
更简洁的写法
@Configuration
public class SystemConfig {
private static List<Resource> resourceList = new ArrayList<>();
static {
resourceList.add(new ClassPathResource("jdbc.properties"));
}
@Value("${spring.datasource.password}")
private String datasourcePassword;
/**
* 这里必须是static函数
* 如果不是 application.propertise 将读取不到
* application.properties 是默认加载的,这里配置自己的properties就好
* @return
*/
@Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer configurer = new PropertySourcesPlaceholderConfigurer();
// properties 加载方式
configurer.setLocations(resourceList.stream().toArray(Resource[]::new));
return configurer;
}
public String getDatasourcePassword() {
return datasourcePassword;
}
}
完结