springspring-bootspring-integrationspring-autoconfiguration

Spring Boot Autoconfiguration fallback bean factory


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...

Context

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 IntegrationFlows, 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...).

What I tried

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.

The problem

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


Solution

  • 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.