現在在配置類裏面創建一個bean date
@Configuration
public class DateConfig {
@Bean("date")
public Date date(){
return new Date();
}
}
時間並不是一成不變的,我想要獲取當前時間呢,應該怎麼覆蓋已經在容器內bean。我一開始想到使用org..cloud.context.scope.refresh.RefreshScope
,但是Spring boot項目並沒有使用到Spring Cloud包,這個走不通,就試着registerBean
動態註冊相同名字bean,想着能不能覆蓋容器內bean,畢竟所謂容器只不過是Map而已,只要通過機制覆蓋掉Map 上value 就可以實現動態刷新了。
private ApplicationContext applicationContext;
@GetMapping("setting/now")
public void dkd(){
GenericApplicationContext gac = (GenericApplicationContext)applicationContext;
gac.registerBean("date",Date.class);
}
執行這個請求,直接報錯了,拋出了一個BeanDefinitionOverrideException異常,bean不能被覆蓋。在DefaultListableBeanFactory.registerBeanDefinition
可以看到其中原因
public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition)
throws BeanDefinitionStoreException {
// 省略多餘代碼
BeanDefinition existingDefinition = this.beanDefinitionMap.get(beanName);
if (existingDefinition != null) { //對於已經存在bean
if (!isAllowBeanDefinitionOverriding()) { //如果allowBeanDefinitionOverriding 這個值爲false 這裏就會拋出異常
throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingDefinition);
}
else if (existingDefinition.getRole() < beanDefinition.getRole()) { //這裏是BeanDefinition
// e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE
if (logger.isInfoEnabled()) {
logger.info("Overriding user-defined bean definition for bean '" + beanName +
"' with a framework-generated bean definition: replacing [" +
existingDefinition + "] with [" + beanDefinition + "]");
}
}
else if (!beanDefinition.equals(existingDefinition)) {
if (logger.isDebugEnabled()) {
logger.debug("Overriding bean definition for bean '" + beanName +
"' with a different definition: replacing [" + existingDefinition +
"] with [" + beanDefinition + "]");
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("Overriding bean definition for bean '" + beanName +
"' with an equivalent definition: replacing [" + existingDefinition +
"] with [" + beanDefinition + "]");
}
}
this.beanDefinitionMap.put(beanName, beanDefinition);
}
//省略。。
然後發現這個allowBeanDefinitionOverriding 在SpringBoot 剛初始化時,在SpringApplication 會初始化這個值,在SpringApplication.prepareContext
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
applyInitializers(context);
listeners.contextPrepared(context);
bootstrapContext.close(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof AbstractAutowireCapableBeanFactory) {
((AbstractAutowireCapableBeanFactory) beanFactory).setAllowCircularReferences(this.allowCircularReferences);
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding); //設置到DefaultListableBeanFactory中
}
}
if (this.lazyInitialization) { //開啓懶加載配置
context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
}
context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}
接着看下配置文件值如何設置到SpringApplication.allowBeanDefinitionOverriding
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
ConfigurationPropertySources.attach(environment);
listeners.environmentPrepared(bootstrapContext, environment);
DefaultPropertiesPropertySource.moveToEnd(environment);
Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
"Environment prefix cannot be set via properties.");
bindToSpringApplication(environment); //將配置環境bind到屬性中
if (!this.isCustomEnvironment) {
environment = convertEnvironment(environment);
}
ConfigurationPropertySources.attach(environment);
return environment;
}
protected void bindToSpringApplication(ConfigurableEnvironment environment) {
try {
//將配置文件綁定到當前屬性上
//看起來就有ConfigurationProperties 那味了
Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
}
catch (Exception ex) {//略}
}
在application.properties 添加下面配置
spring.main.allow-bean-definition-overriding=true
重啓後重新執行HTTP 請求,沒有報錯了,重新獲取date 這個bean,時間也變成最新值了。
心得
添加多這一個配置估計爲了兼容不同組件間可能存在一些bean 衝突情況,後面初始化bean組件可以覆蓋Spring 內部已經創建組件。假如現在Spring 內部已經初始化bean A,並且成功加入到容器中了,這時加載再加載Spring 組件也有一個Class 繼承bean A,這是需要添加到容器中。如果沒有beanName 相同覆蓋的機制,組件在初始化就會失敗。
還有一點值得注意的,registerBean 這個方法只有在容器中刪除這個bean 緩存,如何已經將bean注入到對象屬性中,這時這個值不會變化的,需要手動調用beanFactory.getBean("beanName"),因爲只有這個bean不存在時候纔會執行初始化。如果有這種bean刷新場景可以使用@Lookup來生成一個代理方法。
@Lookup
public Date initDate() { //這裏會將容器內Date類型注入,每次調用方法,重新從容器獲取一次
return null;
}