spring 多placeHolder问题的解决方案

当前的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

相关文章:

作者: flym

I am flym,the master of the site:)

发表评论

电子邮件地址不会被公开。 必填项已用*标注