javaspringspring-bootactivemq-artemisspring-autoconfiguration

What determines spring boots bean registration order for auto configurations?


I have a multi-subproject Spring Boot 3.0 application built by gradle. In modules A and B I have beans with the annotations

@Bean
@ConditionalOnBean(jakarta.jms.ConnectionFactory.class)

declared.

The actual app subproject containing the runnable boot application depends on both of these modules. Also module B depends on A. And app also has a runtimeOnly dependency on ActiveMQ Artemis to let Spring Boot's ArtemisAutoConfiguration do the configuration of the JMS connection factory via ArtemisAutoConfiguration.

However, when I look at the condition evaluation report after the application started I see that the condition did not match for the bean defined in subproject A but it did match for the bean defined in subproject B. Since ConditionalOnBean is evaluated during bean registration phase I suppose the bean definition for artemis is registered only after the bean in subproject A is registered but before the one in subproject B.

Can anyone explain what determines this order?

I already had a look at BOOT-INF/classpath.idx in the spring boot jar. But this shows A and B before any artemis and spring-boot-autoconfiguration entries. So I suppose it's not that.

NOTE: yes, using @AutoConfigureAfter(ArtemisAutoConfiguration.class) on the auto configuration of subproject A fixes this. I'm just curious to know why this was actually necessary to apply.


Solution

  • Essentially the order of spring boot auto configurations is determined by org.springframework.boot.autoconfigure.AutoConfigurationSorter. The ordering of auto configuration classes implemented there honors @AutoConfigureOrder, @AutoConfigureBefore and @AutoConfigureAfter as expected. However these ordering hints for auto configuration classes are only applied after the classes have initially been sorted alphabetically.

    This means a @ConditionalOnBean might evaluate to true if the bean is defined in an auto configuration class that is alphabetically after the auto configuration class that deines a bean that matches the @ConditionalOnBean.

    For example suppose that there is a very basic spring boot app:

    @SpringBootApplication
    @EnableJms
    public class DemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    

    that has 2 auto configurations in its classpath:

    package a;
    
    import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class EarlyConfiguration {
    
        @Bean
        @ConditionalOnBean(jakarta.jms.ConnectionFactory.class)
        public Object early() {
            return new Object();
        }
        
    }
    
    package z;
    
    import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    public class LateConfiguration {
    
        @Bean
        @ConditionalOnBean(jakarta.jms.ConnectionFactory.class)
        public Object late() {
            return new Object();
        }
        
    }
    

    With the appropriate application.yml that configures a jakarta.jms.ConnectionFactory the condition evaluation report is:

    2024-06-27T14:05:52.859+02:00 DEBUG 21060 --- [demo] [           main] .s.b.a.l.ConditionEvaluationReportLogger :
    
    
    ============================
    CONDITIONS EVALUATION REPORT
    ============================
    
    
    Positive matches:
    -----------------
    
    [...]
    
       ArtemisConnectionFactoryConfiguration.SimpleConnectionFactoryConfiguration matched:
          - @ConditionalOnProperty (spring.artemis.pool.enabled=false) matched (OnPropertyCondition)
    
    [...]
    
       LateConfiguration#late matched:
          - @ConditionalOnBean (types: jakarta.jms.ConnectionFactory; SearchStrategy: all) found bean 'jmsConnectionFactory' (OnBeanCondition)
    
    [...]
    
    
    Negative matches:
    -----------------
    
    [...]
    
       EarlyConfiguration#early:
          Did not match:
             - @ConditionalOnBean (types: jakarta.jms.ConnectionFactory; SearchStrategy: all) did not find any beans of type jakarta.jms.ConnectionFactory (OnBeanCondition)
    
    [...]
    
    Exclusions:
    -----------
    
        None
    
    
    Unconditional classes:
    ----------------------
    
        org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
    
        org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration
    
        z.LateConfiguration
    
        org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
    
        org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
    
        a.EarlyConfiguration
    
        org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
    
        org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
    

    I also noticed that component scaning on application classes may also have an influence on the order of bean registration. If for example

    @SpringBootApplication
    @EnableJms
    @ComponentScan(basePackages = "z")
    public class DemoApplication {
        public static void main(String[] args) {
            SpringApplication.run(DemoApplication.class, args);
        }
    }
    

    Then z.LateConfiguration would also not match because the configuration class is not discovered by the auto configuration but by the component scanning of the DemoApplication class which takes place earlier.