Spring里的占位符

spring里的占位符通常表现的形式是:

<bean id="dataSource" destroy-method="close" class="org.apache.commons.dbcp.BasicDataSource">
<property name="url" value="${jdbc.url}"/>
</bean>

或者

@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
@Value("${jdbc.url}")
private String url;
}

Spring应用在有时会出现占位符配置没有注入,原因可能是多样的。

本文介绍两种比较复杂的情况。

占位符是在Spring生命周期的什么时候处理的

Spirng在生命周期里关于Bean的处理大概可以分为下面几步:

  1. 加载Bean定义(从xml或者从@Import等)
  2. 处理BeanFactoryPostProcessor
  3. 实例化Bean
  4. 处理Bean的property注入
  5. 处理BeanPostProcessor

当然这只是比较理想的状态,实际上因为Spring Context在构造时,也需要创建很多内部的Bean,应用在接口实现里也会做自己的各种逻辑,整个流程会非常复杂。

那么占位符(${}表达式)是在什么时候被处理的?

  • 实际上是在org.springframework.context.support.PropertySourcesPlaceholderConfigurer里处理的,它会访问了每一个bean的BeanDefinition,然后做占位符的处理
  • PropertySourcesPlaceholderConfigurer实现了BeanFactoryPostProcessor接口
  • PropertySourcesPlaceholderConfigurer的 order是Ordered.LOWEST_PRECEDENCE,也就是最低优先级的

结合上面的Spring的生命周期,如果Bean的创建和使用在PropertySourcesPlaceholderConfigurer之前,那么就有可能出现占位符没有被处理的情况。

例子1:Mybatis 的 MapperScannerConfigurer引起的占位符没有处理

例子代码:mybatis-demo.zip

  • 首先应用自己在代码里创建了一个DataSource,其中${db.user}是希望从application.properties里注入的。代码在运行时会打印出user的实际值。

    @Configuration
    public class MyDataSourceConfig {
    @Bean(name = "dataSource1")
    public DataSource dataSource1(@Value("${db.user}") String user) {
    System.err.println("user: " + user);
    JdbcDataSource ds = new JdbcDataSource();
    ds.setURL("jdbc:h2:˜/test");
    ds.setUser(user);
    return ds;
    }
    }
  • 然后应用用代码的方式来初始化mybatis相关的配置,依赖上面创建的DataSource对象

    @Configuration
    public class MybatisConfig1 { @Bean(name = "sqlSessionFactory1")
    public SqlSessionFactory sqlSessionFactory1(DataSource dataSource1) throws Exception {
    SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
    org.apache.ibatis.session.Configuration ibatisConfiguration = new org.apache.ibatis.session.Configuration();
    sqlSessionFactoryBean.setConfiguration(ibatisConfiguration); sqlSessionFactoryBean.setDataSource(dataSource1);
    sqlSessionFactoryBean.setTypeAliasesPackage("sample.mybatis.domain");
    return sqlSessionFactoryBean.getObject();
    } @Bean
    MapperScannerConfigurer mapperScannerConfigurer(SqlSessionFactory sqlSessionFactory1) {
    MapperScannerConfigurer mapperScannerConfigurer = new MapperScannerConfigurer();
    mapperScannerConfigurer.setSqlSessionFactoryBeanName("sqlSessionFactory1");
    mapperScannerConfigurer.setBasePackage("sample.mybatis.mapper");
    return mapperScannerConfigurer;
    }
    }

当代码运行时,输出结果是:

user: ${db.user}

为什么会user这个变量没有被注入?

分析下Bean定义,可以发现MapperScannerConfigurer它实现了BeanDefinitionRegistryPostProcessor。这个接口在是Spring扫描Bean定义时会回调的,远早于BeanFactoryPostProcessor

所以原因是:

  • MapperScannerConfigurer它实现了BeanDefinitionRegistryPostProcessor,所以它会Spring的早期会被创建
  • 从bean的依赖关系来看,mapperScannerConfigurer依赖了sqlSessionFactory1,sqlSessionFactory1依赖了dataSource1
  • MyDataSourceConfig里的dataSource1被提前初始化,没有经过PropertySourcesPlaceholderConfigurer的处理,所以@Value("${db.user}") String user 里的占位符没有被处理

要解决这个问题,可以在代码里,显式来处理占位符:

environment.resolvePlaceholders("${db.user}")

package org.mybatis.spring.mapper;

import static org.springframework.util.Assert.notNull;

import java.lang.annotation.Annotation;
import java.util.Map; import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.PropertyValue;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.PropertyResourceConfigurer;
import org.springframework.beans.factory.config.TypedStringValue;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.StringUtils; /**
* BeanDefinitionRegistryPostProcessor that searches recursively starting from a base package for
* interfaces and registers them as {@code MapperFactoryBean}. Note that only interfaces with at
* least one method will be registered; concrete classes will be ignored.
* <p>
* This class was a {code BeanFactoryPostProcessor} until 1.0.1 version. It changed to
* {@code BeanDefinitionRegistryPostProcessor} in 1.0.2. See https://jira.springsource.org/browse/SPR-8269
* for the details.
* <p>
* The {@code basePackage} property can contain more than one package name, separated by either
* commas or semicolons.
* <p>
* This class supports filtering the mappers created by either specifying a marker interface or an
* annotation. The {@code annotationClass} property specifies an annotation to search for. The
* {@code markerInterface} property specifies a parent interface to search for. If both properties
* are specified, mappers are added for interfaces that match <em>either</em> criteria. By default,
* these two properties are null, so all interfaces in the given {@code basePackage} are added as
* mappers.
* <p>
* This configurer enables autowire for all the beans that it creates so that they are
* automatically autowired with the proper {@code SqlSessionFactory} or {@code SqlSessionTemplate}.
* If there is more than one {@code SqlSessionFactory} in the application, however, autowiring
* cannot be used. In this case you must explicitly specify either an {@code SqlSessionFactory} or
* an {@code SqlSessionTemplate} to use via the <em>bean name</em> properties. Bean names are used
* rather than actual objects because Spring does not initialize property placeholders until after
* this class is processed.
* <p>
* Passing in an actual object which may require placeholders (i.e. DB user password) will fail.
* Using bean names defers actual object creation until later in the startup
* process, after all placeholder substituation is completed. However, note that this configurer
* does support property placeholders of its <em>own</em> properties. The <code>basePackage</code>
* and bean name properties all support <code>${property}</code> style substitution.
* <p>
* Configuration sample:
* <p>
*
* <pre class="code">
* {@code
* <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
* <property name="basePackage" value="org.mybatis.spring.sample.mapper" />
* <!-- optional unless there are multiple session factories defined -->
* <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
* </bean>
* }
* </pre>
*
* @author Hunter Presnall
* @author Eduardo Macarron
*
* @see MapperFactoryBean
* @see ClassPathMapperScanner
*/
public class MapperScannerConfigurer implements BeanDefinitionRegistryPostProcessor, InitializingBean, ApplicationContextAware, BeanNameAware { private String basePackage; private boolean addToConfig = true; private SqlSessionFactory sqlSessionFactory; private SqlSessionTemplate sqlSessionTemplate; private String sqlSessionFactoryBeanName; private String sqlSessionTemplateBeanName; private Class<? extends Annotation> annotationClass; private Class<?> markerInterface; private ApplicationContext applicationContext; private String beanName; private boolean processPropertyPlaceHolders; private BeanNameGenerator nameGenerator;

例子2:Spring boot自身实现问题,导致Bean被提前初始化

例子代码:demo.zip

Spring Boot里提供了@ConditionalOnBean,这个方便用户在不同条件下来创建bean。里面提供了判断是否存在bean上有某个注解的功能。

@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnBeanCondition.class)
public @interface ConditionalOnBean {
/**
* The annotation type decorating a bean that should be checked. The condition matches
* when any of the annotations specified is defined on a bean in the
* {@link ApplicationContext}.
* @return the class-level annotation types to check
*/
Class<? extends Annotation>[] annotation() default {};

比如用户自己定义了一个Annotation:

@Target({ ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation {
}

然后用下面的写法来创建abc这个bean,意思是当用户显式使用了@MyAnnotation(比如放在main class上),才会创建这个bean。

@Configuration
public class MyAutoConfiguration {
@Bean
// if comment this line, it will be fine.
@ConditionalOnBean(annotation = { MyAnnotation.class })
public String abc() {
return "abc";
}
}

这个功能很好,但是在spring boot 1.4.5 版本之前都有问题,会导致FactoryBean提前初始化。

在例子里,通过xml创建了javaVersion这个bean,想获取到Java的版本号。这里使用的是spring提供的一个调用static函数创建bean的技巧。

<bean id="sysProps" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetClass" value="java.lang.System" />
<property name="targetMethod" value="getProperties" />
</bean> <bean id="javaVersion" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
<property name="targetObject" ref="sysProps" />
<property name="targetMethod" value="getProperty" />
<property name="arguments" value="${java.version.key}" />
</bean>

我们在代码里获取到这个javaVersion,然后打印出来:

@SpringBootApplication
@ImportResource("classpath:/demo.xml")
public class DemoApplication { public static void main(String[] args) {
ConfigurableApplicationContext context = SpringApplication.run(DemoApplication.class, args);
System.err.println(context.getBean("javaVersion"));
}
}

在实际运行时,发现javaVersion的值是null。

这个其实是spring boot的锅,要搞清楚这个问题,先要看@ConditionalOnBean的实现。

  • @ConditionalOnBean实际上是在ConfigurationClassPostProcessor里被处理的,它实现了BeanDefinitionRegistryPostProcessor
  • BeanDefinitionRegistryPostProcessor是在spring早期被处理的
  • @ConditionalOnBean的具体处理代码在org.springframework.boot.autoconfigure.condition.OnBeanCondition
  • OnBeanCondition在获取bean的Annotation时,调用了beanFactory.getBeanNamesForAnnotation

    private String[] getBeanNamesForAnnotation(
    ConfigurableListableBeanFactory beanFactory, String type,
    ClassLoader classLoader, boolean considerHierarchy) throws LinkageError {
    String[] result = NO_BEANS;
    try {
    @SuppressWarnings("unchecked")
    Class<? extends Annotation> typeClass = (Class<? extends Annotation>) ClassUtils
    .forName(type, classLoader);
    result = beanFactory.getBeanNamesForAnnotation(typeClass);
  • beanFactory.getBeanNamesForAnnotation 会导致FactoryBean提前初始化,创建出javaVersion里,传入的${java.version.key}没有被处理,值为null。

  • spring boot 1.4.5 修复了这个问题:https://github.com/spring-projects/spring-boot/issues/8269

实现spring boot starter要注意不能导致bean提前初始化

用户在实现spring boot starter时,通常会实现Spring的一些接口,比如BeanFactoryPostProcessor接口,在处理时,要注意不能调用类似beanFactory.getBeansOfTypebeanFactory.getBeanNamesForAnnotation 这些函数,因为会导致一些bean提前初始化。

而上面有提到PropertySourcesPlaceholderConfigurer的order是最低优先级的,所以用户自己实现的BeanFactoryPostProcessor接口在被回调时很有可能占位符还没有被处理。

对于用户自己定义的@ConfigurationProperties对象的注入,可以用类似下面的代码:

@ConfigurationProperties(prefix = "spring.my")
public class MyProperties {
String key;
}

  

 
public static MyProperties buildMyProperties(ConfigurableEnvironment environment) {
MyProperties myProperties = new MyProperties(); if (environment != null) {
MutablePropertySources propertySources = environment.getPropertySources();
new RelaxedDataBinder(myProperties, "spring.my").bind(new PropertySourcesPropertyValues(propertySources));
} return myProperties;
}

 

总结

  • 占位符(${}表达式)是在PropertySourcesPlaceholderConfigurer里处理的,也就是BeanFactoryPostProcessor接口
  • spring的生命周期是比较复杂的事情,在实现了一些早期的接口时要小心,不能导致spring bean提前初始化
  • 在早期的接口实现里,如果想要处理占位符,可以利用spring自身的api,比如 environment.resolvePlaceholders("${db.user}")
 
 http://blog.csdn.net/hengyunabc/article/details/75453307

深入Spring Boot:那些注入不了的Spring占位符(${}表达式)的更多相关文章

  1. Spring Boot使用Maven打包替换资源文件占位符

    在Spring Boot开发中,通过Maven构建项目依赖是一件比较舒心的事,可以为我们省去处理冲突等大部分问题,将更多的精力用于业务功能上.近期在项目中,由于项目集成了其他外部系统资源文件,需要根据 ...

  2. spring boot 配置注入

    spring boot配置注入有变量方式和类方式(参见:<spring boot 自定义配置属性的各种方式>),变量中又要注意静态变量的注入(参见:spring boot 给静态变量注入值 ...

  3. Spring Boot动态注入删除bean

    Spring Boot动态注入删除bean 概述 因为如果采用配置文件或者注解,我们要加入对象的话,还要重启服务,如果我们想要避免这一情况就得采用动态处理bean,包括:动态注入,动态删除. 动态注入 ...

  4. spring boot 系列之六:深入理解spring boot的自动配置

    我们知道,spring boot自动配置功能可以根据不同情况来决定spring配置应该用哪个,不应该用哪个,举个例子: Spring的JdbcTemplate是不是在Classpath里面?如果是,并 ...

  5. # 曹工说Spring Boot源码(10)-- Spring解析xml文件,到底从中得到了什么(context:annotation-config 解析)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  6. 曹工说Spring Boot源码(12)-- Spring解析xml文件,到底从中得到了什么(context:component-scan完整解析)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  7. 曹工说Spring Boot源码(15)-- Spring从xml文件里到底得到了什么(context:load-time-weaver 完整解析)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  8. 曹工说Spring Boot源码(16)-- Spring从xml文件里到底得到了什么(aop:config完整解析【上】)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  9. 曹工说Spring Boot源码(18)-- Spring AOP源码分析三部曲,终于快讲完了 (aop:config完整解析【下】)

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

  10. 曹工说Spring Boot源码(29)-- Spring 解决循环依赖为什么使用三级缓存,而不是二级缓存

    写在前面的话 相关背景及资源: 曹工说Spring Boot源码(1)-- Bean Definition到底是什么,附spring思维导图分享 曹工说Spring Boot源码(2)-- Bean ...

随机推荐

  1. ITU-T G.1081 IPTV性能监测点 (Performance monitoring points for IPTV)

    ITU-T 建议书 G.1081 IPTV性能监测点 Performance monitoring points for IPTV Summary Successful deployment of I ...

  2. 测试access函数

    测试程序: 测试结果: chown root access.out 将用户ID改为root chmod u+s access.out 打开 set-user-ID位

  3. Cursor类取出数据

    重点是要调用 moveToFirst(); long id=intent.getLongExtra("id",0); Cursor cr =dbh.selectDataById(i ...

  4. centOS 安装(光安装 和 u盘安装)

    光盘安装用这个: http://www.williamlong.info/archives/1912.html 是否保留win7,要作好相关配置.有些插件可以不装. 网络设置:不好弄 如果用u盘安装, ...

  5. Android ROM开发(二)——ROM架构以及Updater-Script脚本分析,常见的Status错误解决办法

    Android ROM开发(二)--ROM架构以及Updater-Script脚本分析,常见的Status错误解决办法 怪自己二了,写好的不小心弄没了,现在只好重新写一些了,上篇简单的配置了一下环境, ...

  6. How tomcat works 读书笔记十二 StandardContext 下

    对重载的支持 tomcat里容器对重载功能的支持是依靠Load的(在目前就是WebLoader).当在绑定载入器的容器时 public void setContainer(Container cont ...

  7. SharePoint 调查添加图片支持

    前言:今天,碰到一个有趣的问题,就是SharePoint调查里面,添加对于图片的支持,众所周知,SharePoint的调查就支持那么几种字段类型的问题,当然,我们可以开发实现,不过,这个不是我们今天介 ...

  8. Which SQL statement is the trump card to the senior software developer

    Which SQL statement is the trump card to the senior software developer                    MA Genfeng ...

  9. shc/unshc加/解密shell脚本

    一.加密软件shcshc是linux的一款加密脚本的插件东西比较安全我们可以利用wget将文件放在root目录下也可以通过sftp放在root目录也可以直接利用cd命令选择目录一切随意shc官网:ht ...

  10. day08_Servlet学习笔记

     ============================================================ 一.什么是Servlet?(非常非常重要) servlet 是运行在 Web ...