Affects: 5.2.6.RELEASE


Issue description

I ran into an issue with a rather large project today which I think raised a bug or a non handled corner case in the @Autowired injection process. I'll try to detail it as best as I can but please bear since it's hard to explain and english isn't my first language. A sample project is also provided.

In this large project, a number of beans are defined in a big @Configuration class and some others are defined as @Component and instantiated using component scanning in Spring Boot (component scanning does not seem to be part of the problem). Some beans defined in the @Configuration class depend on beans defined as @Component so we autowire them in the @Configuration class.

The issue we ran into was that a bean instantiated in the configuration class got a null for an autowired instance of a dependency. There are no circular dependencies and the @Autowired annotation is not required=false.

After long hours of debugging of the dependency injection path to get to that point, I realize that the order of the @Autowired fields had an influence, moving the problematic field up in the class definition solved the null instance problem. Analyzing the situation some more, I was able to craft a sample to reproduce the issue.

I understand that this is a circular dependency on some level (not in the dependencies itself but in the related classes and fields necessary to instantiate them) so I would totally understand if we don't want to support this. However, I think an exception should be thrown instead of going ahead with the null instance. I also agree with saying that this "hybrid DI" with configuration classes and components is not ideal and is easily fixable in the sample project I provided but might be harder or hidden between multiple configuration classes for a real world large project like the project we had the issue with.

Sample project

You can find the project here.

Class descriptions

In the beans package, you'll find BeanA, BeanB, BeanC, BeanD, BeanE. Those are dummy beans to reproduce the issue with the only particularity that each bean as a non null check for each member in the constructor or in a @PostConstruct for autowired beans.

In the configurations package, you'll find 2 subpackages with the configuration classes I use for the example. The only difference is the order of the @Autowired fields in NotWorkingConfigClass and WorkingConfigClass.

To run the example, simply run the main class in NullAutowiredApplication. Use the proper @Import to reproduce a working scenario and a non working scenario.

To make the application "fail" silently with a null instance, just remove the Assert.noNullElements(beanA); line in BeanB, the application will boot without any error but there will be a null instance of BeanA in the BeanB instance. I print the BeanA instance in the console too.

Dependency injection path

The DI path is the following for the non working scenario : 1. BeanE is instantiated 2. Autowiring is being done on BeanE, it requires a BeanD instance 3. It finds the BeanD constructor NotWorkingConfigClass, it instantiates the configuration class 4. It starts autowiring the @Autowired fields in the order they appear 5. It starts with BeanC, instantiate the class and starts autowiring fields 6. BeanA is instantiated and injected in BeanC 7. BeanB constructor is found in NotWorkingConfigClass 8. It uses that constructor to get the instance of BeanB however BeanA is not yet autowired in this class, even though it has been instantiated earlier and has no dependencies! <-- error here

The DI path is the following for the working scenario : 1. BeanE is instantiated 2. Autowiring is being done on BeanE, it requires a BeanD instance 3. It finds the BeanD constructor WorkingConfigClass, it instantiates the configuration class 4. It starts autowiring the @Autowired fields in the order they appear 5. BeanA is instantiated and autowired in WorkingConfigClass 5. It then proceed with BeanC, instantiate the class and starts autowiring fields 6. BeanA is injected in BeanC 7. BeanC is injected in WorkingConfigClass 8. BeanD is finally instantiated with everything in place and injected in BeanE -> success!

Sorry for the long post and hopefully I made it clear enough for you, let me know if I can be of any help.

Thanks!

Comment From: jebeaudet

@jhoeller Hopefully pings are not too frowned upon, if yes I'm sorry in advance.

Any opinion on this? I personally feel it's a major issue that could probably be solved with the existing circular dependencies safeguards, autowiring null silently is pretty dangerous!

Thanks

Comment From: YukerZeng

Yes Agree, I also facing this issue. Even thought it can be avoid by some other workround. But the autowiring null is a little strange.

Comment From: snicoll

The only valid outcome for such scenario with a cyclic dependency like that is to throw an exception. I've upgraded your sample to a supported version and this leads to:

```


APPLICATION FAILED TO START


Description:

The dependencies of some of the beans in the application context form a cycle:

com.example.nullautowired.beans.BeanE (field private com.example.nullautowired.beans.BeanD com.example.nullautowired.beans.BeanE.beanD) ┌─────┐ | com.example.nullautowired.configuration.notworking.NotWorkingConfigClass (field private com.example.nullautowired.beans.BeanC com.example.nullautowired.configuration.notworking.NotWorkingConfigClass.beanC) ↑ ↓ | com.example.nullautowired.beans.BeanC (field private com.example.nullautowired.beans.BeanB com.example.nullautowired.beans.BeanC.beanB) └─────┘ ````

I don't think that trying to make it work is a good idea. Given the proper exception you get now, I am going to close this. Sorry it took so long to review it.

Comment From: jebeaudet

I can also confirm my sample does not produce the null injection on Spring boot 3.1.x, 3.0.x and 2.7.x so it must have been fixed somewher between 2.2.x and 2.7.x. The circular dependency message is indeed the right outcome for this.

Great job and thanks for the feedback, better late than never :D