如果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的构造过程为例)
从图中可以看出,systemEnvironment最后关联到的是一个singleton bean。
那这个bean又是如何注册进去的以及内容是啥?
继续看图
从图中可以看出,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方法。
到此,最后一块拼图也找到了。