Spring Boot Bean definition overriding

Posted by neller on Sun, 05 Dec 2021 21:54:44 +0100

In this article, I'll discuss the tricky Spring Boot bean definition override mechanism.

In order to make you more clear about the subject, let's start with a quiz. Look at the next simple example.

Therefore, we have two configurations that instantiate a bean with the name beanName. In the main application, we only print the value of the bean (very importantly, they all have the same name).

So what do you think will be printed?

Example 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";
    }
}

Possible answers:

  1. "BEAN1" will be printed. Maybe it has @ Primary annotation and even @ Order
  2. "BEAN2" will be printed.
  3. An exception is thrown because it does not allow several beans with the same name.
  4. Are there any other versions?

right key

Strangely, the correct answer will be different for spring boot 1. * and spring boot 2. *.

If you use spring boot 1- to run this code, "BEAN2" will be printed in the console. Using spring boot 2- exception will be thrown. Do you know the correct answer? If so, you may be working at pivot:)

Let's go one by one: for spring boot 1. If we look at the log, we will find the next line here:

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]]

Therefore, config1 bean is overwritten and "bean 2" is printed.

For spring boot 2. If we look at the log, we will find the next line here:

***************************
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

Therefore, by default in spring boot 2, the behavior has changed and Bean overrides are no longer valid. If you want to fix this problem and make it similar, spring boot 1 should add the next configuration: spring. Main. Allow Bean definition overriding = true

From now on, they work in the same way.

But this is not the end. Let's examine example 2:

Example 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";
    }
}

So it's exactly the same, but the name of the second configuration class is different: now it's a_config2, but you can also say config0.

Now, if we run this code, the result will be BEAN1

How is that possible? answer.

  1. Spring completely ignores any other annotation @ Order for bean s with the same name (such as @ Primary and). In this case, they will not make any changes.
  2. Spring handles @ Configurations in an unpredictable way. In example 2, it sorts the configuration classes in the order of NAME, so you can override another based on that class, as you can see in examples 1 and 2.
  3. In more complex applications, there may be other configurations xml loaded with @Import(Configuration.class)/groovy/whatever. In this case, the behavior will be different again. I don't know which one will be newly loaded and overwrite the previous one. And I haven't found any strong explanation for this in the Spring documentation.

I found that @ Import is always loaded first, and the XML configuration is always up-to-date, so it will overwrite everything else. In this case, the name does not matter.

Check the latest example:

@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>

Therefore, the output here will be: "XML_BEAN"

Therefore, it is almost impossible to predict which bean will overwrite another bean, especially when you have a complex context, many different configurations inside, and it is really confusing.

abstract

As you can see from this example, this behavior is completely unpredictable, and it is very easy to make mistakes here. I can only see one rule here:

A Bean with the same name as another (processed later) will overwrite the older Bean, but it is unclear which one will be processed later.

The mechanism that makes us so confused is called bean coverage. Spring uses it when it encounters a bean that declares the same name as another bean that already exists in the context.

I am facing a real example of this problem. We have a custom configuration for Spring RestTemplate. The name is just restTemplate. After some time, we get another restTemplate with exactly the same name from the configuration of the external dependency. Of course, the external restTemplate overwrites our own template with our custom "adjustment".

After investigation, I found out how to deal with such situations in spring.

Solution

  1. First, I strongly recommend that you enable this configuration: spring. Main. Allow bean definition overriding = false, which will immediately provide you with information that you have beans with the same name and there are conflicts between them.
  2. If this code is yours, and you can change the name of the Bean in any way - just do this and inject the required code. And you will never face this problem.
  3. If, for some reason, point 2 is not a situation for you - I suggest you try to exclude the wrong bean. As you can see, it's hard to predict which bean will be overwritten, so it's better to remove it from the context.

Here is an example:

@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";
    }
}

Therefore, in this case, config2.class will not be scanned, so we only have a beanName, and the result will be "BEAN1".

PS: if you find some gaps or have anything to add or discuss - please feel free to comment.

Topics: Spring Boot