Search code examples
springspring-mvcconditional-statementsconfigurationinitialization

How to implement a chain of @ConditionalOnBean annotation dependent beans in a correct way?


I want to implement the folowing structure:

Main configuration class in my project:

@Configuration
@Import(MessageConsumerAutoConfig.class)
public class MainConfiguration {

   @Bean
   public MainConfig mainConfig() {
      if (some condition) {
         log.warning("Bean shouldn't be initialized");
         return null;
      }
      return new MainConfig()
   }

}

Service bean which implements MessageListener interface

@Service
public class MyMessageListener implements MessageListener  {
    ...
}

and module called message-consumer which also contains autoconfiguration

@Configuration 
public class MessageConsumerAutoConfig {

    @Bean
    @ConditionOnBean(MessageListener.class)
    public MessageConsumer messgageConsumer(MessageListener listener) {
        // some action with listener there
    }
}

This works fine untill i'm setting @ConditionOnBean(MainConfig.class) on my MyMessageListener class

@Service
@ConditionOnBean(MainConfig.class)
public class MyMessageListener implements MessageListener  {
    ...
}

Spring tries to call method public MessageConsumer messgageConsumer(MessageListener listener) even if there are no MessageListener.class objects in spring context found. Spring also skips initialization of the MyMessageListener bean.

NOTES: MessageListener is an interface from message-consumer module. The code above is just an example, not real project code I'm using Spring 4.3.9.RELEASE with spring-boot-starter 2.0.0.RELEASE

In my case I don't want to instantiate MyMessageListener bean if MainConfig.class bean not found in spring context and obviously MessageConsumer also shouldn't be instantiated. Someone have an ideas? How can I implement the case when a bean depeands on another bean which also depends on atother one?


Solution

  • if you want to via MainConfig is null to judge the condition, there is wrong, because the @ConditionalOnBean The condition can only match the bean definitions you can look the comment is this annotation

    so the @ConditionOnBean(MainConfig.class) in your MyMessageListener always valid, this usage is wrong, if you want to controll the initialization, you could use the following processing method. we use the variable to controller whole MainConfiguration, so your mainConfig and myMessageListener is instantiated only when the @ConditionalOnProperty is match.

    @Configuration
    @ConditionalOnProperty(prefix = "mainconfig", name = "enable", havingValue = "true")
    @Import(MessageConsumerAutoConfig.class)
    public class MainConfiguration {
    
        @Bean
        public MainConfig mainConfig() {
            return new MainConfig();
        }
    
    }
    

    but there is still the wrong in your writing. when the property mainconfig.enable = false.

    the @ConditionalOnBean(MainConfig.class) is match, so the final context is not exist the instance. but your @ConditionOnBean(MessageListener.class) that assing your MessageConsumerAutoConfig#messgageConsumer is also match like we talk about above, the it just check the bean definitions(not the final bean definitions), the MyMessageListener is already assign @Service, so it exists in the first phase before filter, the messgageConsumer invoke will throw exception that MessageConsumerAutoConfig require MessageListener but not found, you could write the simple processor to observe the process order

    @Component
    public class LookProcessor implements BeanFactoryPostProcessor {
    
        @Override
        public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
            Iterator<String> beanNames = beanFactory.getBeanNamesIterator();
            final HashSet<String> strings = new HashSet<>();
            beanNames.forEachRemaining(beanName->{
                strings.add(beanName);
            });
    
            System.out.println(strings.contains("mainConfiguration"));
            System.out.println(strings.contains("messageConsumerAutoConfig"));
            System.out.println(strings.contains("myMessageListener"));
            System.out.println(strings.contains("messgageConsumer"));
        }
    }
    
    

    after run it, you could observe the process in the org.springframework.boot.autoconfigure.condition.OnBeanCondition#getMatchOutcome process is before the final definitions registry.

    so the correct writing, it think it is, as follow

    @Configuration
    @ConditionalOnProperty(prefix = "mainconfig", name = "enable", havingValue = "true")
    @Import(MessageConsumerAutoConfig.class)
    public class MainConfiguration {
    
        @Bean
        public MainConfig mainConfig() {
            return new MainConfig();
        }
    
        @Bean
        public MyMessageListener myMessageListener() {
            return new MyMessageListener();
        }
    }
    
    public class MyMessageListener implements MessageListener  {
        ...
    }