Question

I've declared two beans of same class type. Initialized them to be @Lazy. @Autowiring one bean of them automatically initialized the other bean as well. I was surprised to see that behavior. Just curious to know more about the mechanism.

Code

//bean
public class HelloWorld {
    public HelloWorld(String msg){
         System.out.println( msg + ", " + this);
    }   
}

@Configuration
@Lazy
public class SpringAppContext {

     @Bean(name="helloworld1")
     public HelloWorld helloworld1(){
        return new HelloWorld("helloworld1");
     }  
     @Bean(name="helloworld2")
     public HelloWorld helloworld2(){
        return new HelloWorld("helloworld2");
     }
}

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes={SpringAppContext.class})
public class SpringBeanLazyLoadTest {
     @Autowired
     private HelloWorld helloworld2;

     @Test // this test is lame but just trying out.
     public void print(){
        System.out.println("Autowired: " + helloworld2);
     }
}

Output

helloworld2, my.entp.spring.HelloWorld@3a9bba
helloworld1, my.entp.spring.HelloWorld@163f7a1 // why was helloworld1 initialized?
Autowired: my.entp.spring.HelloWorld@3a9bba

If you observe the output, you may notice that the helloworld1 bean is initialized when helloworld2 is @Autowired.

I tested by removing @Autowired and it produced expected results: initialized none of the beans.

Was it helpful?

Solution

When using @Autowired directly, the injection method is byType. In other words, the container sees

 @Autowired
 private HelloWorld helloworld2;

and tries to find a bean of type HelloWorld in the ApplicationContext to inject.

The process of resolving the bean to be injected consists of getting all candidate beans which consists of creating the beans. So the beans being @Lazy doesn't change anything. They will still have to be created in order to be injected.

To clarify M. Deinum's comment on the question, you've given your beans names. For example,

 @Bean(name="helloworld1")

When the injection process takes place, Spring will find all candidate beans that can be injected. If there is more than one, it will filter through them and try to find the best candidate. If it can't, it will throw exceptions. One of the steps for finding a better candidate is comparing the bean name with the name of the target field. Since yours match, the bean named helloworld2 will be chosen.

By removing @Autowired, the beans are never requested from the ApplicationContext and therefore never initialized.

OTHER TIPS

From Spring docs

However, when a lazy-initialized bean is a dependency of a singleton bean that is not lazy-initialized, the ApplicationContext creates the lazy-initialized bean at startup, because it must satisfy the singleton's dependencies. The lazy-initialized bean is injected into a singleton bean elsewhere that is not lazy-initialized.

In your test case, the lazy bean is eagerly initialized as the default behaviour of Spring test facility is to prepare the test class instance fully (by injecting all dependencies eagerly) and then hand it over to JUnit. The exact place where it happens is DependencyInjectionTestExecutionListener

protected void injectDependencies(final TestContext testContext) throws Exception {
        Object bean = testContext.getTestInstance();
        AutowireCapableBeanFactory beanFactory = testContext.getApplicationContext().getAutowireCapableBeanFactory();
        beanFactory.autowireBeanProperties(bean, AutowireCapableBeanFactory.AUTOWIRE_NO, false);
        beanFactory.initializeBean(bean, testContext.getTestClass().getName());
        testContext.removeAttribute(REINJECT_DEPENDENCIES_ATTRIBUTE);
    }

Old post but still. Now you can do that:

 @Bean(name="helloworld1", autowire=Autowire.BY_NAME)
 public HelloWorld helloworld1(){
    return new HelloWorld("helloworld1");
 }  
 @Bean(name="helloworld2", autowire=Autowire.BY_NAME)
 public HelloWorld helloworld2(){
    return new HelloWorld("helloworld2");
 }

And/or @Qualifier :

 @Autowired
 @Qualifier("helloworld2")
 private HelloWorld hello;

This seems to be a Spring bug at that time if helloworld1 bean was being initialized as well. The @Lazy annotation will play a role in deciding when the Spring beans get initialized irrespective of where it is being called from (tests or otherwise).

Just tried this with Spring 5.1.0 and it correctly initializes only the helloworld2 bean.

If you happen to remove the @Lazy annotation, then both helloworld1 and helloworld2 beans will be initialized. This is due to the fact that Spring will instantiate all eager beans as part of the refresh phase. Once all the Spring beans are discovered in the invokeBeanFactoryPostProcessors phase, Spring calls preInstantiateSingletons for all the beans in the beanFactory which will initialize all the eager beans (some Lazy beans too if they are needed as part of eager bean initialization).

If your case, since the top level bean SpringAppContext is lazy, this applies to both the HelloWorld type beans as well.

If you remove the @Autowired, no beans are eagerly created nor needed for auto-wiring and hence no initialization.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top