Spring Boot Auto-configuration
1. Giới Thiệu
1.1 Auto-configuration là gì?
Auto-configuration là một trong những tính năng quan trọng nhất của Spring Boot, cho phép framework tự động cấu hình ứng dụng dựa trên: - Dependencies có trong classpath - Beans đã được định nghĩa - Properties configuration - Environment variables
1.2 Cách thức hoạt động
@SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
@SpringBootApplication composite annotation bao gồm:
- @EnableAutoConfiguration: Kích hoạt auto-configuration
- @ComponentScan: Quét các components trong package hiện tại
- @Configuration: Đánh dấu class là source của bean definitions
1.3 Auto-configuration Classes Loading
// Spring Boot 2.7+ sử dụng spring.factories
# META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyAutoConfiguration
com.example.AnotherAutoConfiguration
// Spring Boot 2.6 và trước đó
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration,\
com.example.AnotherAutoConfiguration
2. Custom Auto-configuration
2.1 Tạo Auto-configuration Class
@AutoConfiguration
@ConditionalOnClass(DataSource.class)
@EnableConfigurationProperties(DatabaseProperties.class)
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
public class CustomDatabaseAutoConfiguration {
private final DatabaseProperties properties;
public CustomDatabaseAutoConfiguration(DatabaseProperties properties) {
this.properties = properties;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "app.database", name = "enabled", havingValue = "true")
public DataSource customDataSource() {
HikariConfig config = new HikariConfig();
config.setJdbcUrl(properties.getUrl());
config.setUsername(properties.getUsername());
config.setPassword(properties.getPassword());
config.setMaximumPoolSize(properties.getMaxPoolSize());
config.setMinimumIdle(properties.getMinIdle());
config.setConnectionTimeout(properties.getConnectionTimeout());
config.setIdleTimeout(properties.getIdleTimeout());
return new HikariDataSource(config);
}
@Bean
@ConditionalOnBean(DataSource.class)
@ConditionalOnClass(JdbcTemplate.class)
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
2.2 Configuration Properties với Validation
@ConfigurationProperties(prefix = "app.database")
@Validated
@Data
public class DatabaseProperties {
@NotBlank(message = "Database URL is required")
private String url;
@NotBlank(message = "Username is required")
private String username;
@NotBlank(message = "Password is required")
private String password;
@Min(value = 1, message = "Max pool size must be greater than 0")
@Max(value = 100, message = "Max pool size must not exceed 100")
private int maxPoolSize = 10;
@Min(value = 0, message = "Min idle must not be negative")
private int minIdle = 5;
@Min(value = 1000, message = "Connection timeout must be at least 1 second")
private long connectionTimeout = 30000;
@Min(value = 10000, message = "Idle timeout must be at least 10 seconds")
private long idleTimeout = 600000;
private boolean enabled = true;
private Pool pool = new Pool();
@Data
public static class Pool {
private int initialSize = 5;
private int maxActive = 20;
private int maxIdle = 10;
private long maxWait = 60000;
}
}
2.3 Configuration Metadata cho IDE Support
{
"groups": [
{
"name": "app.database",
"type": "com.example.DatabaseProperties",
"sourceType": "com.example.DatabaseProperties"
}
],
"properties": [
{
"name": "app.database.url",
"type": "java.lang.String",
"description": "JDBC URL for the database connection",
"defaultValue": null
},
{
"name": "app.database.username",
"type": "java.lang.String",
"description": "Username for database authentication",
"defaultValue": null
},
{
"name": "app.database.max-pool-size",
"type": "java.lang.Integer",
"description": "Maximum number of connections in the pool",
"defaultValue": 10
}
],
"hints": [
{
"name": "app.database.url",
"values": [
{
"value": "jdbc:mysql://localhost:3306/mydb",
"description": "MySQL database URL"
},
{
"value": "jdbc:postgresql://localhost:5432/mydb",
"description": "PostgreSQL database URL"
}
]
}
]
}
3. Advanced Conditional Auto-configuration
3.1 Complex Conditional Logic
@Configuration
@ConditionalOnClass({RedisConnectionFactory.class, RedisTemplate.class})
@ConditionalOnProperty(prefix = "spring.redis", name = "enabled", matchIfMissing = true)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
return template;
}
@Bean
@ConditionalOnClass(RedisLockRegistry.class)
@ConditionalOnProperty(prefix = "spring.redis.lock", name = "enabled", havingValue = "true")
public RedisLockRegistry redisLockRegistry(RedisConnectionFactory connectionFactory) {
return new RedisLockRegistry(connectionFactory, "locks");
}
}
3.2 Custom Condition Classes
public class OnCloudPlatformCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(
ConditionContext context,
AnnotatedTypeMetadata metadata) {
CloudPlatform cloudPlatform = CloudPlatform.getActive(context.getEnvironment());
if (cloudPlatform != null) {
return ConditionOutcome.match(
"Detected cloud platform: " + cloudPlatform.name());
}
return ConditionOutcome.noMatch("No cloud platform detected");
}
}
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Conditional(OnCloudPlatformCondition.class)
public @interface ConditionalOnCloudPlatform {
}
@Configuration
@ConditionalOnCloudPlatform
public class CloudConfiguration {
// Cloud-specific configuration
}
3.3 Multiple Conditions với Logical Operators
@Configuration
@ConditionalOnClass(name = {
"org.springframework.data.redis.core.RedisTemplate",
"org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession"
})
@ConditionalOnProperty(prefix = "spring.session", name = "store-type", havingValue = "redis")
@ConditionalOnBean(RedisConnectionFactory.class)
@EnableConfigurationProperties(SessionProperties.class)
public class RedisSessionAutoConfiguration {
// Redis session configuration
}
4. Auto-configuration Ordering và Dependencies
4.1 Configuration Ordering
@AutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@ConditionalOnClass(Servlet.class)
public class WebServerConfiguration {
// High priority web server configuration
}
@AutoConfiguration
@AutoConfigureAfter({
DataSourceAutoConfiguration.class,
HibernateJpaAutoConfiguration.class
})
@AutoConfigureBefore(TransactionAutoConfiguration.class)
public class CustomJpaConfiguration {
// JPA configuration that depends on DataSource and Hibernate
// but needs to be configured before Transaction management
}
4.2 Conditional Dependencies
@Configuration
@ConditionalOnClass(PlatformTransactionManager.class)
@ConditionalOnBean(DataSource.class)
@ConditionalOnMissingBean(TransactionManager.class)
public class TransactionConfiguration {
@Bean
@Primary
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
@Bean
@ConditionalOnClass(JpaTransactionManager.class)
@ConditionalOnBean(EntityManagerFactory.class)
public JpaTransactionManager jpaTransactionManager(EntityManagerFactory emf) {
return new JpaTransactionManager(emf);
}
}
5. Testing Auto-configuration
5.1 Context Testing
@SpringBootTest
@TestPropertySource(properties = {
"app.database.enabled=true",
"app.database.url=jdbc:h2:mem:testdb",
"app.database.username=sa",
"app.database.password="
})
class DatabaseAutoConfigurationTest {
@Autowired
private ApplicationContext context;
@Test
void whenPropertiesSet_thenDataSourceBeanExists() {
assertThat(context).hasSingleBean(DataSource.class);
assertThat(context).hasSingleBean(JdbcTemplate.class);
}
@Test
void whenDataSourceExists_thenIsHikariDataSource() {
DataSource dataSource = context.getBean(DataSource.class);
assertThat(dataSource).isInstanceOf(HikariDataSource.class);
}
}
5.2 ApplicationContextRunner Testing
class RedisAutoConfigurationTest {
private final ApplicationContextRunner contextRunner =
new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RedisAutoConfiguration.class));
@Test
void whenRedisOnClasspath_thenRedisTemplateAutoConfigured() {
contextRunner
.withClassLoader(new FilteredClassLoader(RedisConnectionFactory.class))
.run(context -> {
assertThat(context).doesNotHaveBean(RedisTemplate.class);
});
}
@Test
void whenPropertiesSet_thenRedisConfigured() {
contextRunner
.withPropertyValues(
"spring.redis.host=localhost",
"spring.redis.port=6379",
"spring.redis.database=0"
)
.run(context -> {
assertThat(context).hasSingleBean(RedisTemplate.class);
assertThat(context).hasSingleBean(StringRedisTemplate.class);
});
}
@Test
void whenCustomRedisTemplate_thenBackOff() {
contextRunner
.withUserConfiguration(CustomRedisConfiguration.class)
.run(context -> {
assertThat(context).hasSingleBean(RedisTemplate.class);
assertThat(context.getBean(RedisTemplate.class))
.isSameAs(context.getBean("customRedisTemplate"));
});
}
@TestConfiguration
static class CustomRedisConfiguration {
@Bean
public RedisTemplate<Object, Object> customRedisTemplate() {
return new RedisTemplate<>();
}
}
}
5.3 Integration Testing với TestContainers
@SpringBootTest
@Testcontainers
class DatabaseAutoConfigurationIntegrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb")
.withUsername("test")
.withPassword("test");
@Autowired
private DataSource dataSource;
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void whenContainerStarted_thenConnectionEstablished() throws SQLException {
try (Connection connection = dataSource.getConnection()) {
assertThat(connection.isValid(1)).isTrue();
}
}
@Test
void whenJdbcTemplateConfigured_thenCanExecuteQueries() {
Integer result = jdbcTemplate.queryForObject("SELECT 1", Integer.class);
assertThat(result).isEqualTo(1);
}
}
6. Debugging và Monitoring Auto-configuration
6.1 Debug Mode
# application.yml
debug: true
logging:
level:
org.springframework.boot.autoconfigure: DEBUG
org.springframework.boot.autoconfigure.condition: TRACE
6.2 Conditions Report
@RestController
@ConditionalOnProperty(name = "management.endpoints.web.exposure.include", havingValue = "conditions")
public class AutoConfigurationReportController {
@Autowired
private ConditionEvaluationReport conditionEvaluationReport;
@GetMapping("/admin/autoconfig")
public Map<String, Object> getAutoConfigurationReport() {
Map<String, Object> report = new HashMap<>();
conditionEvaluationReport.getConditionAndOutcomesBySource()
.forEach((source, conditionAndOutcomes) -> {
Map<String, Object> sourceReport = new HashMap<>();
sourceReport.put("matched", conditionAndOutcomes.isFullMatch());
List<String> conditions = conditionAndOutcomes.iterator()
.asIterator()
.stream()
.map(conditionAndOutcome ->
conditionAndOutcome.getCondition().getClass().getSimpleName() +
": " + conditionAndOutcome.getOutcome().getMessage())
.toList();
sourceReport.put("conditions", conditions);
report.put(source, sourceReport);
});
return report;
}
}
6.3 Custom FailureAnalyzer
public class DatabaseConnectionFailureAnalyzer extends AbstractFailureAnalyzer<SQLException> {
@Override
protected FailureAnalysis analyze(Throwable rootFailure, SQLException cause) {
String description = "Failed to connect to database: " + cause.getMessage();
String action = "Verify that your database is running and accessible. " +
"Check the following properties:\n" +
"\t- app.database.url\n" +
"\t- app.database.username\n" +
"\t- app.database.password";
return new FailureAnalysis(description, action, cause);
}
}
7. Advanced Patterns và Best Practices
7.1 Configuration Slicing
@AutoConfiguration
@ConditionalOnClass(ReactiveRedisTemplate.class)
@EnableConfigurationProperties(RedisProperties.class)
public class ReactiveRedisAutoConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ReactiveRedisTemplate.class)
static class ReactiveRedisTemplateConfiguration {
@Bean
@ConditionalOnMissingBean(name = "reactiveRedisTemplate")
@ConditionalOnBean(ReactiveRedisConnectionFactory.class)
public ReactiveRedisTemplate<Object, Object> reactiveRedisTemplate(
ReactiveRedisConnectionFactory connectionFactory) {
return new ReactiveRedisTemplate<>(connectionFactory,
RedisSerializationContext.java());
}
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisReactiveHealthIndicator.class)
static class ReactiveRedisHealthConfiguration {
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledHealthIndicator("redis")
public RedisReactiveHealthIndicator redisReactiveHealthIndicator(
ReactiveRedisConnectionFactory connectionFactory) {
return new RedisReactiveHealthIndicator(connectionFactory);
}
}
}
7.2 Starter Module Structure
redis-spring-boot-starter/
├── src/main/java/
│ ├── com/example/redis/
│ │ ├── autoconfigure/
│ │ │ ├── RedisAutoConfiguration.java
│ │ │ ├── RedisProperties.java
│ │ │ └── condition/
│ │ │ └── OnRedisCondition.java
│ │ └── health/
│ │ └── RedisHealthIndicator.java
├── src/main/resources/
│ ├── META-INF/
│ │ ├── spring/
│ │ │ └── org.springframework.boot.autoconfigure.AutoConfiguration.imports
│ │ └── spring-configuration-metadata.json
│ └── application.yml
└── pom.xml
7.3 Environment-specific Auto-configuration
@Profile("!test")
@AutoConfiguration
@ConditionalOnClass(CloudSqlJdbcInfoProvider.class)
@ConditionalOnProperty(prefix = "spring.cloud.gcp.sql", name = "enabled", matchIfMissing = true)
public class CloudSqlAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public CloudSqlJdbcInfoProvider cloudSqlJdbcInfoProvider(
GcpProjectIdProvider projectIdProvider,
CredentialsProvider credentialsProvider) {
return new DefaultCloudSqlJdbcInfoProvider(projectIdProvider, credentialsProvider);
}
}
@Profile("development")
@AutoConfiguration
@ConditionalOnClass(EmbeddedDatabase.class)
public class DevelopmentDatabaseAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DataSource embeddedDataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("schema.sql")
.addScript("data.sql")
.build();
}
}
8. Performance Considerations
8.1 Lazy Initialization
@AutoConfiguration
@ConditionalOnClass(HeavyService.class)
@EnableConfigurationProperties(HeavyServiceProperties.class)
public class HeavyServiceAutoConfiguration {
@Bean
@Lazy
@ConditionalOnMissingBean
public HeavyService heavyService(HeavyServiceProperties properties) {
return new HeavyService(properties);
}
@Bean
@ConditionalOnProperty(prefix = "app.heavy-service", name = "cache.enabled", havingValue = "true")
public CacheManager heavyServiceCacheManager() {
return CacheManagerBuilder.newCacheManagerBuilder()
.withCache("heavy-cache",
CacheConfigurationBuilder.newCacheConfigurationBuilder(
String.class, Object.class,
ResourcePoolsBuilder.heap(1000)))
.build(true);
}
}
8.2 Configuration Class Proxy Behavior
// Avoid proxying for better performance
@Configuration(proxyBeanMethods = false)
public class OptimizedConfiguration {
@Bean
public ServiceA serviceA() {
return new ServiceA();
}
@Bean
public ServiceB serviceB() {
// Don't call serviceA() method - inject as parameter instead
return new ServiceB();
}
}
9. Common Pitfalls và Solutions
9.1 Circular Dependencies
// Problem: Circular dependency
@Configuration
public class ProblematicConfiguration {
@Bean
public ServiceA serviceA(ServiceB serviceB) {
return new ServiceA(serviceB);
}
@Bean
public ServiceB serviceB(ServiceA serviceA) {
return new ServiceB(serviceA);
}
}
// Solution: Use @Lazy or refactor dependencies
@Configuration
public class FixedConfiguration {
@Bean
public ServiceA serviceA(@Lazy ServiceB serviceB) {
return new ServiceA(serviceB);
}
@Bean
public ServiceB serviceB() {
return new ServiceB();
}
@EventListener
public void onApplicationReady(ApplicationReadyEvent event) {
ServiceA serviceA = event.getApplicationContext().getBean(ServiceA.class);
ServiceB serviceB = event.getApplicationContext().getBean(ServiceB.class);
serviceB.setServiceA(serviceA);
}
}
9.2 Configuration Property Binding Issues
// Wrong: Missing @EnableConfigurationProperties
@AutoConfiguration
public class BadConfiguration {
// Properties won't be bound properly
@Autowired
private MyProperties properties;
}
// Correct: Proper property binding
@AutoConfiguration
@EnableConfigurationProperties(MyProperties.class)
public class GoodConfiguration {
private final MyProperties properties;
public GoodConfiguration(MyProperties properties) {
this.properties = properties;
}
}
10. Migration Guide
10.1 Spring Boot 2.x to 3.x
// Old way (Spring Boot 2.x)
@Configuration
@ConditionalOnClass(SomeClass.class)
public class OldAutoConfiguration {
// Configuration
}
// New way (Spring Boot 3.x)
@AutoConfiguration
@ConditionalOnClass(SomeClass.class)
public class NewAutoConfiguration {
// Same configuration, but with @AutoConfiguration
}
10.2 spring.factories to imports file
# Old: META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.MyAutoConfiguration
# New: META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.MyAutoConfiguration