在本文中,我将讨论棘手的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:如果您发现一些空白或有任何需要补充或讨论的地方-请随时发表评论。