如果spring的配置文件使用了表达式来获取环境变量,测试的时候又希望能对systemEnvironment进行修改,加入新值,how to do it?

<bean id="aaa" class="xxx.bbb.Factory" factory-method="init">
    <constructor-arg value="#{ systemEnvironment['xxx'] }"/>
</bean>

创建一个类,实现BeanFactoryPostProcessor 接口,加入到applicationContext里面:

package test.env;

import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;

import java.util.HashMap;
import java.util.Map;

import static org.springframework.context.ConfigurableApplicationContext.SYSTEM_ENVIRONMENT_BEAN_NAME;

public class EnvModifier implements BeanFactoryPostProcessor {
    private Map<String, Object> env;

    public EnvModifier(Map<String, Object> env) {
        this.env = env;
    }

    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
        Map<String, Object> map = new HashMap<>((Map<String, Object>) beanFactory
                .getSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME));
        map.putAll(env);
        ((DefaultListableBeanFactory) beanFactory).destroySingleton(SYSTEM_ENVIRONMENT_BEAN_NAME);
        beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, map);
    }
}

applicationContext.xml的配置(profile的设置是因为只想用于测试):

<beans profile="xx_test">
    <bean class="test.env.EnvModifier">
        <constructor-arg>
            <map>
                <entry key="xxx" value="abc"/>
            </map>
        </constructor-arg>
    </bean>
</beans>

P.S: spring在查找实现了BeanFactoryPostProcessor 接口的bean时,会尝试去获取这个class,如果出现了ClassNotFoundException,那么不一定会抛异常,在某些情况下会直接忽略掉这个bean,最终导致环境变量木有被修改但是又看不到出错信息。

如果不求甚解,可以到此为止,以下是原理说明。


要求解,先要回答以下问题:
1. spring怎么解析表达式
2. spring如何通过表达式取值
3. systemEnvironment怎么关联到java中的环境变量


对于第1,2个问题,看源码流程图可解答(以下以FactoryBean的构造过程为例)

spring 替换资源文件 变量 spring环境变量_spring


从图中可以看出,systemEnvironment最后关联到的是一个singleton bean。


那这个bean又是如何注册进去的以及内容是啥?

继续看图

spring 替换资源文件 变量 spring环境变量_spring 替换资源文件 变量_02


从图中可以看出,spring的context在调用refresh方法时,内部调用会去获取java的环境变量(System.getenv()),并且注册到context中成为singleton。到此,问题3也回答了。


现在一切都明朗了,那接下来的问题就是如何修改这个systemEnvironment singleton bean。

java的System.getenv()返回的是Collections.unmodifiableMap,所以不能通过Map.put方法来增加或者修改变量。

重新注册一个同名的singleton bean到spring context。但是spring不允许这么做,以下是DefaultSingletonBeanRegistry的源码,可以看到不能覆盖singleton。

@Override
    public void registerSingleton(String beanName, Object singletonObject) throws IllegalStateException {
        Assert.notNull(beanName, "'beanName' must not be null");
        synchronized (this.singletonObjects) {
            Object oldObject = this.singletonObjects.get(beanName);
            if (oldObject != null) {
                throw new IllegalStateException("Could not register object [" + singletonObject +
                        "] under bean name '" + beanName + "': there is already object [" + oldObject + "] bound");
            }
            addSingleton(beanName, singletonObject);
        }
    }

但是从2可以看出,如果能先从singletonObjects里面删除掉systemEnvironment ,然后重新注册它,那就没问题了。还好DefaultSingletonBeanRegistry还真提供了这么一个方法:

public void destroySingleton(String beanName) {
        // Remove a registered singleton of the given name, if any.
        removeSingleton(beanName);

        // Destroy the corresponding DisposableBean instance.
        DisposableBean disposableBean;
        synchronized (this.disposableBeans) {
            disposableBean = (DisposableBean) this.disposableBeans.remove(beanName);
        }
        destroyBean(beanName, disposableBean);
    }

在removeSingleton方法里面,就会清除掉systemEnvironment对应的bean:

protected void removeSingleton(String beanName) {
        synchronized (this.singletonObjects) {
            this.singletonObjects.remove(beanName);
            this.singletonFactories.remove(beanName);
            this.earlySingletonObjects.remove(beanName);
            this.registeredSingletons.remove(beanName);
        }
    }

到此,修改的方法已经诞生了:

//prepare a local map named "env" for your variables;
Map<String, Object> map = new HashMap<>((Map<String, Object>) beanFactory.getSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME));
map.putAll(env);
((DefaultListableBeanFactory) beanFactory).destroySingleton(SYSTEM_ENVIRONMENT_BEAN_NAME);
beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, map);

最后的问题就是:
1. 如何获取beanFactory
2. 如何在spring解析生成context的过程中运行以上代码

从第二个图可以看出,spring在refresh过程中会内部调用一些实现了BeanFactoryPostProcessor接口的bean的postProcessBeanFactory方法。
到此,最后一块拼图也找到了。