Auto-Configuration and Conditionals
SummaryThis section provides a comprehensive understanding of Spring...
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.