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