当前的spring版本为: 4.0.6.RELEASE
问题官方地址:https://jira.spring.io/browse/SPR-9989
问题重现
@Component public class Tb { @Value("${tb.username:abcd}") private String username; public String getUsername() { return username; } }
以上为定义bean,其中属性表示需要去获取属性为tb.username的属性定义,默认值为abcd。然后在spring分别如下配置
<context:property-placeholder location="classpath:springa.properties" ignore-unresolvable="true" ignore-resource-not-found="true"/> <context:property-placeholder location="classpath:springb.properties" ignore-unresolvable="true" ignore-resource-not-found="true"/>
配置文件的值如下所示:
配置文件一:tb.username1=spring1
配置文件二:tb.username=spring2
因为在配置文件2中,有相应的配置定义,因此我们期望其返回username的值为 spring2。但是在实际运行中,此值即是abcd,如下输出所示:
public static void main(String[] args) { ApplicationContext context = new ClassPathXmlApplicationContext("classpath:applicationContext.xml"); Tb tb = context.getBean(Tb.class); System.out.println("->" + tb.getUsername()); } //输出值 ->abcd
本文即通过更换相应的属性解析器,用于解决此问题,以让spring能够正常的解析并输出我们需要的值。
问题分析
在spring3之后,在beaFactory中增加了以下的属性:
/** String resolvers to apply e.g. to annotation attribute values */ private final List<StringValueResolver> embeddedValueResolvers = new LinkedList<StringValueResolver>();
这个属性,即用于解决在bean配置中通过注解进行配置的属性值,在正常的spring.xml中配置的value=${db.username}这种配置属性,由在spring中配置的PropertySourcesPlaceholderConfigurer对象负责在容器初始化之后作为BeanFactoryPostProcessor进行信息解析。但对于通过注解配置的信息,则是由AutowiredAnnotationBeanPostProcessor在创建对象并进行属性设置时通过上文中的embeddedValueResolvers负责处理的。
我们来看一下相应的处理逻辑
public String resolveEmbeddedValue(String value) { String result = value; for (StringValueResolver resolver : this.embeddedValueResolvers) { if (result == null) { return null; } result = resolver.resolveStringValue(result); } return result; }
如上所示,它通过遍历集合对象,并将每一次的结果当作下一次的处理输入来处理。而这里的stringValueResolver,即是由我们在spring.xml中定义的property-placeholder来间接处理的。我们来分析一下相应的流程如下,
首先碰到配置一对象,它解析${tb.username:abcd},因此它不能解析,因此将返回默认值,即abcd。
再次由配置二对象解析,因为它同样不能解析abcd,因此将原样返回abcd。
这样最终的结果即为abcd,即是我们在console中看到的结果。
问题解决
从原spring issue原文也可以看到,解析此问题的主要思路即通过针对每一个context设置一个统一的valueRevoler,它通过读取所有的placeHolder对象,并将相应的属性进行merge起来,提供一个整体的属性解析器。在处理属性解析时,即可通过之前已经merge的属性对中进行解析,这样就避免了前一个解析器不能解析而返回默认值的情况。
同时,考虑到在一个进程中,可能会有多个context的存在(比如spring mvc web工程web context和core context的区别),因此要求在进行属性解析时,两者的属性也不能进行重合。
这样思路,就是创建一个统一的propertySrouces对象,它将在不同的 PropertySourcesPlaceholderConfigurer 共享数据,并且将多次的数据进行整合在一起。包装成一个统一的数据源提供给context进行处理。
原spring处理思路
我们先看一下原spring在postProcessBeanFactory如何处理的
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { if (this.propertySources == null) { this.propertySources = new MutablePropertySources(); ...... PropertySource<?> localPropertySource = new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME, mergeProperties()); if (this.localOverride) { this.propertySources.addFirst(localPropertySource); } else { this.propertySources.addLast(localPropertySource); } } processProperties(beanFactory, new PropertySourcesPropertyResolver(this.propertySources)); this.appliedPropertySources = this.propertySources; } protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) { ...... // New in Spring 3.0: resolve placeholders in embedded values such as annotation attributes. beanFactoryToProcess.addEmbeddedValueResolver(valueResolver); }
参考实现
可以看出,在上面的解析中,每次封装一个新的PropertySourcesPropertyResolver并传递给beanFactory作注解属性解析。那么,在我们的修改中,我们保证在一个context中,只需要单个context中,我们保证只有一个revolver就可以了。因此可以参考实现如下:
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { MutablePropertySources propertySources = propertySourcesMap.get(beanFactory); if(propertySources == null) { propertySources = new MutablePropertySources(); propertySourcesMap.put(beanFactory, propertySources); ...... processProperties(beanFactory, new PropertySourcesPropertyResolver(propertySources)); } PropertySource<?> localPropertySource = new PropertiesPropertySource(LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME + new Random(), mergeProperties());//todo 将random修改为其它递增数字 if(this.localOverride) { propertySources.addFirst(localPropertySource); } else { propertySources.addLast(localPropertySource); } this.appliedPropertySources = propertySources; }
如上的一个参考实现,我们维度一个全局的propertySourcesMap,并在每次处理时,直接读取此数据信息,并将新的localPropertySource加入此propertySources中即可。同时,仅在第一次将其注册入context中即可。当然实现还需要作进一步修改,但初步可采用此种方法,即保证在单个context,只注入单个valueResolver即可。
转载请标明出处:i flym
本文地址:https://www.iflym.com/index.php/code/201504150001.html