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

11. References

11.1 Official Documentation

11.2 Advanced Topics