The title could be a little misleading because @Fallback
annotation exists. And what I am asking is similar to this related question but requires bean names. Let's start with the...
I have a Spring Boot 3.4.x based application that leverages Spring Integration flows also to connect to JMS destinations. The flows are broken down into multiple IntegrationFlow
s, so that messages are passed along them. Because of this, keep in mind that I have about a hundred of different IntegrationFlow
beans, so I must distinguish them by name, and @Qualifier
is my friend.
Consider the requirement: an instance of the application may or may not have a JMS destination defined, or the application may run without JMS ConnectionFactory at all (especially if it's on Junit integration test, or on certain localhost profile where you don't have the broker). Flows (e.g. "myOutgoingFlow" as an example) are autowired by name, so a bean of that name must exist for the context to start.
The idea is that I want to mock the myOutgoingFlow
when either the ConnectionFactory
bean is not present or the my.destination
property not defined. @Conditional
seems to be our friend here, but I would consider writing a custom condition as last resort, let's try the normal tools first. There should then be two beans, myOutgoingFlow(ConnectionFactory, String destination)
and another fallback myOutgoingFlowNoOp()
which simply logs the message like if it was sent (and supports some mocking/spying in Junit...).
I tried to come up with this configuration
@AutoConfiguration
class MyAutoConfiguration {
@Order(0)
@Bean
@ConditionalOnBean(ConnectionFactory.class)
@ConditionalOnProperty(prefix="my",value="destination")
public IntegrationFlow myOutgoingFlow(ConnectionFactory connectionFactory, @Value("${my.destination}") String destination) { .... }
@Order
@Bean("myOutgoingFlow")
@ConditionalOnMissingBean(name="myOutgoingFlow")
public IntegrationFlow myOutgoingFlowNoOp() { ... }
}
I came up with the Order
very recently, thinking it could help. The idea is again: first check if both connection factory and property are defined, if so then go for the myOutgoingFlow
that uses connection factory, otherwise if any is not defined fall back to myOutgoingFlowNoOp
, giving it the name of myOutgoingFlow
. The name is particularly important because I will autowire myOutgoingFlowNoOp
as myOutgoingFlow
in the producer flow.
But none of my approaches work. I get an error during startup that the bean is defined twice, and if I look at the autoconfiguration debug log, I see something like (where like means real names are redacted as usual)
MyAutoConfiguration#myOutgoingFlow matched:
- @ConditionalOnProperty (my.destination) matched (OnPropertyCondition)
- @ConditionalOnBean (types: jakarta.jms.ConnectionFactory; SearchStrategy: all) found bean 'connectionFactory' (OnBeanCondition)
MyAutoConfiguration#myOutgoingFlowNoOp matched:
- @ConditionalOnMissingBean (names: myOutgoingFlow; SearchStrategy: all) did not find any beans (OnBeanCondition)
With or without the Order
, both beans are evaluated. Seems like myOutgoingFlowNoOp
ignores the fact that myOutgoingFlow
was matched and ready to be created. I was thinking that the two beans, as displayed, are defined in the same autoconfiguration class, so no before
/after
constraint is defined.
I could write my custom Condition
that checks for both the connection factory and the desired property, along with its opposite, so that they are mutually exclusive.
Do you think I can use Spring's standard @Conditional
to achieve what I need?
Secondarily, I wouldn't use @Fallback
because, to my understanding, it still creates the bean in the context, and is used only as autowire candidate. In my particular case, I must autowire flows by their very name, because there are plenties.
UPDATE
I have tried @ArtemBilan's suggestion not to use @Order but to use @AutoConfiguration's before
and after
mechanisms. I implemented them in two ways using static embedded classes.
Final implementation is something like
@AutoConfiguration(
before = {
MyAutoConfiguration.MyAutoConfigurationJms.class,
MyAutoConfiguration.MyAutoConfigurationNoOp.class,
}, after = JmsAutoConfiguration.class)
class MyAutoConfiguration {
@Bean MessageChannel myOutgoingMessageChannel(){...}
@AutoConfiguration
static class MyAutoConfigurationJms {
@Bean("myOutgoingFlow")
@ConditionalOnProperty(...)
@ConditionalOnBean(name="myConnectionFactory")
public IntegrationFlow myOutgoingFlow(...){...}
}
@AutoConfiguration(after=MyAutoConfigurationJms.class)
static class MyAutoConfigurationNoOp {
@Bean("myOutgoingFlow")
@ConditionalOnMissingBean(name="myOutgoingFlow")
public IntegrationFlow myOutgoingFlowNoOp(...){...}
}
}
However, the logs show that both beans were matched, in particular "myOutgoingFlowNoOp" matched because no "myOutgoingFlow" existed, but then the app context won't start because bean with same name is declared twice
It looks like your myOutgoingFlowNoOp()
means as a fallback only for tests.
You may use a @TestConfiguration
in that respective test without prod environment to declare such a bean, and Spring Boot would do the trick for us to inject those extra beans into the target application context.
See its JavaDocs for more info.
UPDATE
Another way is indeed use conditional, but not on beans, but directly on the class. And have the one with @ConditionalOnMissingBean(name="myOutgoingFlow")
marked as @AutoConfigureAfter
and point to another class where you have conditional myOutgoingFlow
bean definition. The point is to have beans loaded in a proper order.
The @Order
doesn't help here indeed. See its JavaDocs:
While such order values may influence priorities at injection points, please be aware that they do not influence singleton startup order which is an orthogonal concern determined by dependency relationships and @DependsOn declarations (influencing a runtime-determined dependency graph).
And that's why in Spring Boot itself that @AutoConfigureAfter/Before
concept was introduced.