Spring Boot: Bean definition overriding

在本文中,我將討論棘手的Spring Boot bean定義覆蓋機制。

爲了使您對該主題更加清楚,讓我們從小測驗開始。請看下一個簡單的例子。

因此,我們有2種配置,它們使用名稱beanName實例化bean,在主應用程序中,我們僅打印該bean的值(非常重要的是,它們都具有相同的名稱)。

那麼您認爲將要印刷什麼?

示例1

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ApplicationContext applicationContext = SpringApplication.run(Application.class, args);
        System.out.println(applicationContext.getBean("beanName"));
    }
}

@Configuration
class config1 {

    @Primary
    @Order(Ordered.HIGHEST_PRECEDENCE)
    @Bean
    String beanName() {
        return "BEAN1";
    }
}

@Configuration
class config2 {

    @Bean
    String beanName() {
        return "BEAN2";
    }
}

可能的答案:

  1. BEAN1 ”將被打印。可能是因爲它具有@Primary註釋,甚至還有@Order
  2. BEAN2 ”將被打印。
  3. 異常會被拋出,因爲它不允許有幾個豆同名。
  4. 還有其他版本嗎?

正確答案

奇怪的是,正確答案對於spring boot 1.*spring boot 2.*版本會有所不同。

如果您使用spring boot 1- 運行此代碼,**“ BEAN2”**將被打印在控制檯中。用spring boot 2- exception將被拋出。你知道正確的答案嗎?如果是,則可能是您在Pivotal工作:)

讓我們一個一個地走:對於spring boot 1。如果我們查看日誌,則會在此找到下一行:

INFO --- [main] o.s.b.f.s.DefaultListableBeanFactory:
Overriding bean definition for bean 'beanName' with a different definition:
replacing [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=true; factoryBeanName=config1; factoryMethodName=beanName; initMethodName=null; destroyMethodName=(inferred);
defined in class path resource [com/example/test/config1.class]] with [Root bean: class [null]; scope=; abstract=false; lazyInit=false; autowireMode=3; dependencyCheck=0; autowireCandidate=true; primary=false; factoryBeanName=config2; factoryMethodName=beanName; initMethodName=null; destroyMethodName=(inferred);
defined in class path resource [com/example/test/config2.class]]

因此,config1bean被覆蓋,config2 並打印了**“ BEAN2”**。

對於spring boot 2。如果我們查看日誌,則會在此找到下一行:

***************************
APPLICATION FAILED TO START
***************************
Description:
The bean 'beanName', defined in class path resource [com/example/test/config2.class],
could not be registered. A bean with that name has
already been defined in class path resource [com/example/test/config1.class]
and overriding is disabled.

Action:
Consider renaming one of the beans or enabling overriding
by setting spring.main.allow-bean-definition-overriding=true

因此,在spring boot 2默認情況下,行爲已更改,並且Bean覆蓋已不再是有效情況。如果要修復此問題並使其與之相似,spring boot 1則應添加下一個配置: spring.main.allow-bean-definition-overriding=true

從現在開始,他們以相同的方式工作。

但這還不是終點。讓我們檢查示例2:

示例2

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        ApplicationContext applicationContext = SpringApplication.run(Application.class, args);
        System.out.println(applicationContext.getBean("beanName"));
    }
}

@Configuration
class config1 {

    @Bean
    String beanName() {
        return "BEAN1";
    }
}

@Configuration
class a_config2 {

    @Bean
    String beanName() {
        return "BEAN2";
    }
}

因此完全相同,但是第二個配置類的名稱有所不同:現在是a_config2,但也可以這麼說config0

現在,如果我們運行此代碼,結果將爲BEAN1

那怎麼可能呢?答案。

  1. Spring完全忽略了具有相同名稱(如@Primary和)的bean的任何其他註釋@Order。在這種情況下,他們不會進行任何更改。
  2. Spring以無法預測的方式處理@Configurations。在示例2中,它按NAME的順序對配置類進行排序,因此基於該類可以覆蓋另一個,這在示例1示例2中可以看到。
  3. 在更復雜的應用程序中,可能有其他配置xml loaded with @Import(Configuration.class)/groovy/whatever。在這種情況下,行爲將再次有所不同。我不知道哪一個將被最新加載並覆蓋前一個。而且我在Spring文檔中沒有找到任何對此的有力解釋。

我發現,@Import總是總是首先加載,而XML配置總是最新,因此它將覆蓋其他所有內容。在這種情況下,名稱無關緊要。

請檢查最新示例:

@SpringBootApplication
@ImportResource("classpath:config.xml")
@Import(Config0.class)
public class Application {
    public static void main(String[] args) {
        ApplicationContext applicationContext = SpringApplication.run(Application.class, args);
        System.out.println(applicationContext.getBean("beanName"));
    }
}

@Configuration
class config1 {
    @Bean
    String beanName() {
        return "BEAN1";
    }
}

@Configuration
class config2 {
    @Bean
    String beanName() {
        return "BEAN2";
    }
}

//separate java config which is loaded by @Import
@Configuration
class Config0 {
    @Bean
    String beanName() {
        return "BEAN0";
    }
}

//separate xml config which is loaded by @ImportResource
<?xml version = "1.0" encoding = "UTF-8"?>
<beans xmlns = "http://www.springframework.org/schema/beans"
       xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation = "http://www.springframework.org/schema/beans
   http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

    <bean id = "beanName"  class = "java.lang.String">
        <constructor-arg value="XML_BEAN"></constructor-arg>
    </bean>

</beans>

因此,這裏的輸出將是:“ XML_BEAN”

因此,幾乎不可能預測哪個bean會覆蓋另一個bean,尤其是當您具有複雜的上下文且內部有許多不同的配置並且確實令人困惑時。

摘要

從此示例中可以看到,這種行爲是完全不可預測的,在這裏犯錯誤是非常容易的。我在這裏只能看到一條規則:

與另一個名稱相同(稍後處理)的Bean會覆蓋較舊的Bean,但尚不清楚以後將處理哪個。

導致我們如此困惑的機制稱爲bean覆蓋。當Spring遇到一個聲明與上下文中已經存在的另一個bean同名的bean時,使用它。

我面對這個問題的真實例子。我們有一個針對Spring RestTemplate的自定義配置。名稱只是restTemplate。在一段時間之後,我們從外部依賴項的配置中獲得了另外一個名稱完全相同的restTemplate。當然發生了,外部restTemplate用我們的自定義“調整”覆蓋了我們自己的模板

經過調查,我發現春季如何處理此類情況。

解決方案

  1. 首先,我強烈建議您啓用此配置: spring.main.allow-bean-definition-overriding=false 它會立即爲您提供一個信息,說明您具有相同名稱的bean,並且它們之間存在衝突。
  2. 如果此代碼是您的代碼,並且可以以任何方式更改Bean的名稱-只需執行此操作並注入所需的代碼即可。而且您將永遠不會面對這個問題。
  3. 如果出於某些原因,第2點對您而言不是一種情況-我建議您嘗試排除錯誤的bean。如您所見,很難預測哪個bean將被覆蓋,因此從上下文中刪除它會更好。

這是一個例子:

@SpringBootApplication
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = config2.class))
public class Application {

    public static void main(String[] args) {
        ApplicationContext applicationContext = SpringApplication.run(Application.class, args);
        System.out.println(applicationContext.getBean("beanName"));
    }
}

@Configuration
class config1 {

    @Bean
    String beanName() {
        return "BEAN1";
    }
}

@Configuration
class config2 {

    @Bean
    String beanName() {
        return "BEAN2";
    }
}

因此,在這種情況下,不會掃描config2.class,因此我們只有一個beanName,結果將是**“ BEAN1”**。

PS:如果您發現一些空白或有任何需要補充或討論的地方-請隨時發表評論。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章