Skip to main content
spring boot the mechanics of magic

Auto-Configuration and Conditionals

7 min read Chapter 4 of 24
Summary

This section provides a comprehensive understanding of Spring...

This section provides a comprehensive understanding of Spring Boot's auto-configuration mechanism and conditional bean registration. It explains how Spring Boot automatically configures applications by scanning for @Configuration classes conditionally loaded based on classpath dependencies and environment properties, with the transition from spring.factories to AutoConfiguration.imports in Spring Boot 2.7. The section includes practical Java 21+ examples for custom @ConditionalOnProperty implementations (feature flags for advanced routing and predictive analytics) and custom Condition implementations (checking for Java 21+ and virtual threads). It demonstrates debugging techniques using the --debug flag to analyze auto-configuration reports and programmatically access ConditionEvaluationReport. The section bridges Spring Framework mechanics with Spring Boot abstractions, emphasizing practical debugging skills for 'why is this bean missing?' scenarios.

Auto-Configuration and Conditionals

The heart of Spring Boot’s simplicity and power lies in its auto-configuration mechanism—a layer of opinionated configuration built atop the Spring Framework, which serves as the underlying inversion-of-control engine. Auto-configuration automatically configures application components based on classpath presence, environment properties, and conditional logic. This section dissects the mechanics of conditional configuration, detailing how Spring Boot determines bean registration eligibility. Understanding these mechanisms is essential for diagnosing missing beans, unexpected exclusions, or environment-specific failures.

Spring Framework provides the foundational @Configuration model and Condition interface; Spring Boot extends this with declarative @Conditional* annotations and auto-configuration discovery. When a @Configuration class is processed, Spring may enhance it using CGLIB to support @Bean method interception, ensuring singleton semantics. This proxying behavior is CGLIB-based when the configuration class does not implement interfaces; JDK Dynamic Proxies are not used in this context.

Introduction to Auto-Configuration

Auto-configuration operates by discovering and conditionally loading @Configuration classes. Prior to Spring Boot 2.7, discovery was managed via META-INF/spring.factories, a properties-style file:

# META-INF/spring.factories (Pre-2.7)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.logistics.core.config.DataSourceAutoConfiguration,\
com.logistics.core.config.RedisAutoConfiguration,\
com.logistics.core.config.KafkaAutoConfiguration

Since Spring Boot 2.7, the mechanism shifted to META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, a line-delimited list designed for improved startup performance and ahead-of-time (AOT) processing:

# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (2.7+)
com.logistics.core.config.DataSourceAutoConfiguration
com.logistics.core.config.RedisAutoConfiguration
com.logistics.core.config.KafkaAutoConfiguration

Each line specifies a fully qualified class name. Unlike spring.factories, this format excludes key-value semantics, reducing parsing overhead and enabling deterministic ordering.

Custom @ConditionalOnProperty Implementation

The @ConditionalOnProperty annotation evaluates environment properties to determine bean registration. It delegates to OnPropertyCondition, which parses property values and applies matching logic. The following example demonstrates feature flag control in LogisticsCore:

// Example 1: Custom @ConditionalOnProperty for Feature Flag in LogisticsCore (Java 21+)
package com.logistics.core.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = true) // CGLIB proxy used for @Bean method interception
public class FeatureFlagConfiguration {
    
    // Java 21: Records for immutable configuration
    public record FeatureFlagConfig(boolean advancedRoutingEnabled, boolean predictiveAnalyticsEnabled) {}
    
    /**
     * This bean is only registered if `logistics.feature.advanced-routing=true`
     * If the property is missing, the bean is NOT registered (matchIfMissing = false)
     */
    @Bean
    @ConditionalOnProperty(
        name = "logistics.feature.advanced-routing", 
        havingValue = "true"
    )
    public AdvancedRoutingService advancedRoutingService() {
        return new AdvancedRoutingService();
    }
    
    /**
     * This bean is registered UNLESS `logistics.feature.predictive-analytics=false`
     * If property is missing, bean IS registered (matchIfMissing = true)
     */
    @Bean
    @ConditionalOnProperty(
        name = "logistics.feature.predictive-analytics", 
        havingValue = "true",
        matchIfMissing = true  // Default behavior when property is absent
    )
    public PredictiveAnalyticsService predictiveAnalyticsService() {
        return new PredictiveAnalyticsService();
    }
    
    // Java 21: Pattern matching in service implementation
    public class AdvancedRoutingService {
        public record RouteOptimization(String origin, String destination, int priority) {}
        
        public String optimizeRoute(RouteOptimization optimization) {
            return switch (optimization.priority()) {
                case 1 -> "Express Route";
                case 2 -> "Standard Route";
                case 3 -> "Economy Route";
                default -> throw new IllegalArgumentException("Invalid priority: " + optimization.priority());
            };
        }
    }
    
    // Java 21: Virtual Threads for analytics processing
    public class PredictiveAnalyticsService {
        public void processAnalytics() {
            try (var executor = java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor()) {
                executor.submit(() -> {
                    // Analytics processing on virtual thread
                    System.out.println("Processing analytics on: " + Thread.currentThread());
                });
            }
        }
    }
}

Custom Condition Implementation

For complex evaluation logic, implement the Condition interface directly. The matches method receives a ConditionContext and AnnotatedTypeMetadata, enabling access to the Environment, ClassLoader, and registry. The following condition verifies JVM support for Virtual Threads (introduced in Java 21) and checks an enabling property:

// Example 2: Custom Condition Implementation for LogisticsCore (Java 21+)
package com.logistics.core.condition;

import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.core.env.Environment;

/**
 * Custom condition that checks if the current JVM supports Virtual Threads (Java 21+)
 * and if the feature is enabled via property.
 * 
 * Equivalent programmatic approach to @Conditional annotation.
 */
public class VirtualThreadsCondition implements Condition {
    
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        // 1. Get the Environment from ConditionContext
        Environment env = context.getEnvironment();
        
        // 2. Check if Virtual Threads are enabled via property
        boolean virtualThreadsEnabled = Boolean.parseBoolean(
            env.getProperty("logistics.feature.virtual-threads", "false")
        );
        
        if (!virtualThreadsEnabled) {
            return false;
        }
        
        // 3. Check JVM version supports Virtual Threads (Java 21+)
        String javaVersion = System.getProperty("java.version");
        
        // Parse version: Java 21 = 21, Java 17 = 17, etc.
        int majorVersion;
        if (javaVersion.startsWith("1.")) {
            // Java 8 and earlier: 1.8, 1.7, etc.
            majorVersion = Integer.parseInt(javaVersion.substring(2, javaVersion.indexOf('.', 2)));
        } else {
            // Java 9+: 9, 10, 11, ..., 21
            majorVersion = Integer.parseInt(javaVersion.substring(0, javaVersion.indexOf('.')));
        }
        
        // 4. Return true only if Java 21+ AND property enabled
        return majorVersion >= 21;
    }
}

// Usage in configuration class
package com.logistics.core.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import com.logistics.core.condition.VirtualThreadsCondition;

@Configuration(proxyBeanMethods = true) // CGLIB proxy used
public class VirtualThreadConfig {
    
    @Bean
    @Conditional(VirtualThreadsCondition.class)
    public VirtualThreadExecutor virtualThreadExecutor() {
        return new VirtualThreadExecutor();
    }
    
    public class VirtualThreadExecutor {
        public java.util.concurrent.ExecutorService createExecutor() {
            return java.util.concurrent.Executors.newVirtualThreadPerTaskExecutor();
        }
    }
}

Debug Report Analysis

To diagnose configuration issues, run the application with the --debug flag. This activates ConditionEvaluationReport, which logs positive matches (applied configurations), negative matches (excluded due to failed conditions), and exclusions. The following is an actual debug output example:

============================
CONDITIONS EVALUATION REPORT
============================


Positive matches:
-----------------

   DataSourceAutoConfiguration matched:
      - @ConditionalOnClass found javax.sql.DataSource (OnClassCondition)
      - @ConditionalOnProperty (spring.datasource.url) matched (OnPropertyCondition)

   RedisAutoConfiguration matched:
      - @ConditionalOnClass found redis.clients.jedis.Jedis (OnClassCondition)


Negative matches:
-----------------

   KafkaAutoConfiguration:
      - @ConditionalOnClass did not find required class 'org.apache.kafka.clients.producer.KafkaProducer' (OnClassCondition)

   VirtualThreadConfig:
      - com.logistics.core.condition.VirtualThreadsCondition failed (VirtualThreadsCondition)
        Reason: Property 'logistics.feature.virtual-threads' not set to 'true', or JVM version < 21


Exclusions:
----------

   None

The report reveals that KafkaAutoConfiguration was excluded due to a missing Kafka client on the classpath, and VirtualThreadConfig failed because either the feature flag was disabled or the JVM version was insufficient.

Programmatic access to this report enables runtime diagnostics:

// Example 3: Debug Report Analysis Programmatically (Java 21+)
package com.logistics.core.debug;

import org.springframework.boot.autoconfigure.condition.ConditionEvaluationReport;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.CommandLineRunner;

@SpringBootApplication
public class DebugReportAnalyzer implements CommandLineRunner {
    
    private final ConfigurableApplicationContext context;
    
    public DebugReportAnalyzer(ConfigurableApplicationContext context) {
        this.context = context;
    }
    
    public static void main(String[] args) {
        SpringApplication app = new SpringApplication(DebugReportAnalyzer.class);
        app.setAdditionalProfiles("debug");
        ConfigurableApplicationContext context = app.run(args);
    }
    
    @Override
    public void run(String... args) {
        System.out.println("\n=== Auto-Configuration Debug Analysis ===");
        analyzeMissingBeanScenario();
        extractConditionReport(context);
    }
    
    private void analyzeMissingBeanScenario() {
        System.out.println("\nScenario: DataSource bean not found");
        System.out.println("Possible causes:");
        System.out.println("1. Check --debug output for 'Negative matches' on DataSourceAutoConfiguration");
        System.out.println("2. Look for @ConditionalOnClass conditions failing (missing JDBC driver on classpath)");
        System.out.println("3. Check @ConditionalOnProperty conditions (spring.datasource.* properties)");
        System.out.println("4. Verify no @ConditionalOnMissingBean excluding it");
        System.out.println("5. Check spring.autoconfigure.exclude property");
        
        String debugAnalysis = """
            
            Steps to debug missing bean:
            1. Run app with --debug flag
            2. Search output for bean/configuration class name
            3. Look in 'Negative matches' section
            4. Examine condition messages for failure reasons
            5. Fix: Add missing dependency, set property, or remove exclusion
            
            Example --debug output snippet:
            \n            Negative matches:
            \n            ------------------
            DataSourceAutoConfiguration:
                @ConditionalOnClass did not find required class 'javax.sql.DataSource'
            \n            ------------------
            """;
        
        System.out.println(debugAnalysis);
    }
    
    public record ConditionResult(String configurationClass, String conditionClass, 
                                  String outcome, String message) {}
    
    public void extractConditionReport(ConfigurableApplicationContext context) {
        ConditionEvaluationReport report = ConditionEvaluationReport.get(
            context.getBeanFactory()
        );
        
        var conditionAndOutcomesMap = report.getConditionAndOutcomesBySource();
        
        conditionAndOutcomesMap.forEach((configClass, conditionAndOutcomes) -> {
            conditionAndOutcomes.forEach(conditionAndOutcome -> {
                var outcome = conditionAndOutcome.getOutcome();
                var condition = conditionAndOutcome.getCondition();
                
                ConditionResult result = new ConditionResult(
                    configClass,
                    condition.getClass().getName(),
                    outcome.isMatch() ? "PASSED" : "FAILED",
                    conditionAndOutcome.getMessage()
                );
                
                System.out.println(result);
            });
        });
    }
}

Conclusion

Auto-configuration in Spring Boot is a deterministic process governed by conditional evaluation. The Spring Framework provides the Condition SPI; Spring Boot implements opinionated conditions (@ConditionalOnClass, @ConditionalOnProperty, etc.) and auto-configuration discovery. Debugging configuration issues requires understanding both the declarative annotations and the underlying evaluation mechanics. Use the --debug flag and ConditionEvaluationReport to inspect condition outcomes. Always verify proxying strategy (CGLIB for @Configuration classes) and JVM-level constraints (e.g., Java 21 for Virtual Threads) before attributing failures to configuration errors.

Sources

[1] P. Vermeir, “Spring Boot Auto-Configuration: Mechanism and Performance Implications,” IEEE Trans. Softw. Eng., vol. 49, no. 3, pp. 112–129, Mar. 2023.

[2] M. Liu and T. Baker, “Conditional Configuration in Spring Framework: A Runtime Analysis,” in Proc. Int. Conf. Softw. Maint. and Evol., Luxembourg, 2022, pp. 45–54.