Problem
I have two separate classes marked as @Configuration
, both set up beans with bean factory methods annotated with @Bean
(please correct me if I have the wording off, I'm not an expert).
The first:
@Configuration
public class KidneyConfig {
@Bean
KidneyBean makeKidneyBean() { return new KidneyBean() }
}
The dependent one:
public class ChiliConfig {
@ConditionalOnBean(KidneyBean.class)
@Bean
Chili makeChili(KidneyBean kidneyBean) { return new Chili(kidneyBean); ]
}
The problem: Building everything as an application into a jar file (spring boot application, but I don't think that plays a role here) I can sometimes start the application, sometimes it will fail claiming that the condition failed.
Analysis
(I learned Spring internals on the go analyzing this bug, so please bear with inaccuracies):
When an ApplicationContext
is built it will call refresh
, and from there do a classpath scan to detect beans. The classpath scan is order dependent, so the bean definitions are in different order in the ApplicationContext
.
Later in refresh()
Spring will happen to call the ConfigurationClassParser
which will scan @Configuration
classes for @Bean
methods. If it finds one it will use ConfigurationClass
to locate the methods annotated with @Bean
.
ConfigurationClass
in turn will evaluate the Conditionals
right away in this moment, so if my KidneyBean
was already scanned by ConfigurationClass
then everything will start up fine.
But if the Chili
is scanned earlier than the KidneyBean
, then the @ConditionalOnBean
will fail - although just a moment later the ConfigurationClassParser
will load the KidneyBean
.
I'm not qualified to suggest how to fix this.
(Debugging this I found there's a post processor that validates for common problems - if using @ConditionalOnBean
is not supported in the way I did, then one solution might be making the post processor detect that situation and complain during startup.)
Comment From: froh42
Sorry, for the changes in the title, had a few typos in the comment after a long debugging session.
Comment From: froh42
I have pushed a small demo showing this problem to https://github.com/froh42/spring-issue-demo-26964.
Its readme tries to explain it again, now that I'm not sleepy.
The demo application has both a ChiliConfig and a KidneyConfig in the situation described above. Starting it, the start will fail because ChiliConfig and its ConditionalOnBean are evaluated before KidneyConfig.
Renaming the KidneyConfig to AAAKidneyConfig will result in the KindneyConfig being loaded before ChiliConfig and the application will run.
See https://github.com/froh42/spring-issue-demo-26964/tree/master/src/main/java/com/example/problem for the code
Comment From: sbrannen
Hi @froh42,
I am not quite sure why you are using @ConditionalOnBean
in your sample application.
If you remove the use of @ConditionalOnBean
from your makeChiliBean()
method, your application starts up correctly.
Please note that declaring the dependency on a KidneyBean
via a method parameter in makeChiliBean()
instructs Spring that makeChiliBean()
cannot be invoked without a KidneyBean
present in the ApplicationContext
.
Does that suit your needs?
If not, please explain what you are trying to achieve by using @ConditionalOnBean
.
Comment From: froh42
Hi @sbrannen , yes in the sample application I don't need the @ConditionalOnBean at all - and in my real application I have worked around the problem by switching away @ConditionalOnBean to @ConditionalOnProperty (using a property to control that part of the application is a valid solution for me).
The sample application includes the @ConditionalOnBean, because it is simply required to trigger the problem. Yes, when I remove it, it starts up correctly.
The sample is just the most reduced version of some code I could come up with to showcase the problem.
I did find this by finding out (and working around) the issue that the applicaton my team is developing did not start up in certain cases. I can't publish that one here, unfortunately.
What I can do: In the demo application, I can document the place where to set a breakpoint and condition for the breakpoint, so it is easy to debug to exactly the place where I think the problem occurs. I'll do that right away and add another comment, then.
Comment From: froh42
Hi @sbrannen again, I updated the https://github.com/froh42/spring-issue-demo-26964/blob/master/readme.md on the demo app with what I found out about where the problem exactly happens. (Stack traces, screenshots and my interpretation of things).
Comment From: ttddyy
@froh42 I had bit by similar case before.
You should only use @ConditionalOnBean
in auto-configuration classes as it comes from autoconfigure
module in Spring Boot.
@ConditionalOnBean
is dependent on the order of processing @Configuration
classes.
The javadoc clearly states the order dependency and recommends the usage in auto configurations.
The condition can only match the bean definitions that have been processed by the application context so far and, as such, it is strongly recommended to use this condition on auto-configuration classes only. If a candidate bean may be created by another auto-configuration, make sure that the one using this condition runs after.
The auto-configuration classes are guaranteed to be processed after user configurations. Also, it has a mechanism to force relative orders within auto configurations by @AutoConfigure[Before|After]
.
Without a mechanism to order the configuration classes, the order of processing the @Configuration
class that has @ConditionalOnBean
is not deterministic. Therefore, you should only use it within auto configuration as it is the intended usage of the condition.
Comment From: froh42
@ttddyy yes, I do aggree. Of course I didn't re-read the documentation before applying the @ConditionalOnBean
That's why a good mitigation for the problem might just be printing just a warning if you DO use @ConditionalOnBean
in this manner - this would have shortened debugging time. I do think a log.warning
could be called in that case, that just says that.
People like me - stumbling into this trap - would then find at least a string to google for and find the documentation.
To the spring maintainers: As I did something that is not recommended and well documented, I think spring behavior won't be changed. Do you think, detecting this situation and adding a warning log message (or event debug log message, when you start with --debug argument) might be a good call? I learned my lesson, but other might experience this as well.
Comment From: sbrannen
I have worked around the problem by switching away @ConditionalOnBean to @ConditionalOnProperty (using a property to control that part of the application is a valid solution for me).
Have you considered using bean definition profiles (@Profile(...)
) instead of @ConditionalOnProperty
?
I think profiles might be a better fit for your use case.
As @ttddyy pointed out, @ConditionalOnBean
is not intended to be used the way you were using it.
@ttddyy, thanks for providing your detailed answer!
Do you think, detecting this situation and adding a warning log message (or event debug log message, when you start with --debug argument) might be a good call? I learned my lesson, but other might experience this as well.
Since @ConditionalOnBean
is a Spring Boot feature, I'll let the Spring Boot team decide if they wish to implement some form of warning for using @ConditionalOnBean
in a @Configuration
class that is not participating in auto-configuration.
Comment From: wilkinsona
Thanks for the suggestion, @froh42. Unfortunately, there's no way for us to detect if @ConditionalOnBean
has been used in a regular @Configuration
class or a @Configuration
class that is or has been imported by an auto-configuration class. The current warnings in the javadoc and reference documentation will have to suffice.