Spring Boot/Auto-configuration deep dive — bóc tách @EnableAutoConfiguration
~28 phútSpring Boot FoundationsMiễn phí

Auto-configuration deep dive — bóc tách @EnableAutoConfiguration

Auto-configuration là magic chính của Spring Boot. Bài này bóc cơ chế ở mức bytecode: AutoConfigurationImportSelector, file AutoConfiguration.imports, 12 family annotation @ConditionalOn*, autoconfig ordering với @AutoConfigureBefore/After, debug autoconfig với /actuator/conditions, và viết autoconfig của riêng bạn.

Bài 02 đã chỉ ra Boot có 5 trụ cột — starter là 1, auto-configuration là 1 nữa. Starter chỉ pull jar; auto-config làm phần còn lại — biến jar trên classpath thành bean trong container, không cần code config.

Đây là bài quan trọng nhất Module 02. Sau bài này, khi bạn thêm 1 starter và thấy "magic happens" — bạn biết chính xác AutoConfigurationImportSelector đọc file nào, scan classpath ra sao, register bean theo điều kiện gì. Bạn debug được "tại sao bean này không tạo" trong 5 phút thay vì 5 giờ.

1. Auto-configuration giải quyết gì

Quay lại pain section 1.3 của bài 01 — Spring 4 yêu cầu mỗi project copy 30-100 dòng XML config:

<!-- applicationContext.xml — Spring 4 -->
<bean id="dataSource" class="com.zaxxer.hikari.HikariDataSource">
    <property name="jdbcUrl" value="${db.url}"/>
    <property name="username" value="${db.user}"/>
    <property name="password" value="${db.pass}"/>
</bean>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource"/>
    <property name="packagesToScan" value="com.olhub.domain"/>
    <property name="jpaVendorAdapter">
        <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"/>
    </property>
</bean>

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

<tx:annotation-driven transaction-manager="transactionManager"/>
<jpa:repositories base-package="com.olhub.repository"/>

Auto-config nói: "Bạn không cần viết 30 dòng đó. Tôi tự register DataSource, EntityManagerFactory, JpaTransactionManager nếu thấy Hibernate trên classpath và spring.datasource.url trong config."

Bạn chỉ cần:

# application.yml
spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/app
    username: app
    password: secret

Và:

@SpringBootApplication
public class App { ... }

Boot làm còn lại. Đây là magic chính.

2. @EnableAutoConfiguration — entry point

@SpringBootApplication chứa @EnableAutoConfiguration:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

2 phần quan trọng:

  • @Import(AutoConfigurationImportSelector.class) — class này load tất cả autoconfig.
  • @AutoConfigurationPackage — đánh dấu package của class app là root cho @EntityScan, @ComponentScan. Đây là lý do @SpringBootApplication nên ở root package.

Phần thú vị là AutoConfigurationImportSelector. Nó implement DeferredImportSelector — interface Spring dùng để lazy register class config sau khi mọi @Configuration thông thường đã process.

3. AutoConfigurationImportSelector — đọc file AutoConfiguration.imports

Logic chính của selector (đơn giản hoá):

public class AutoConfigurationImportSelector implements DeferredImportSelector {

    public String[] selectImports(AnnotationMetadata metadata) {
        // Buoc 1: doc file AutoConfiguration.imports tu MOI jar trong classpath
        List<String> configurations = ImportCandidates
            .load(AutoConfiguration.class, getBeanClassLoader())
            .getCandidates();

        // Buoc 2: bo nhung class user explicitly exclude
        configurations = removeExclusions(configurations, exclusions);

        // Buoc 3: filter qua @Conditional* — chi giu class match condition
        configurations = filter(configurations, autoConfigurationMetadata);

        return configurations.toArray(String[]::new);
    }
}

3 bước cốt lõi: load → exclude → filter.

3.1 File AutoConfiguration.imports

Mỗi jar Boot autoconfig (vd spring-boot-autoconfigure.jar) có file:

META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

Mở file này (text thuần) — nó liệt kê fully-qualified class name của mọi autoconfig:

org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
org.springframework.boot.autoconfigure.condition.ConditionEvaluationReportAutoConfiguration
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
... 143 dong (Boot 3.4)

Spring Boot scan tất cả jar trên classpath cho file này. Mỗi jar có thể đóng góp autoconfig:

  • spring-boot-autoconfigure.jar — 143 autoconfig core của Boot.
  • spring-cloud-config-client-*.jar — autoconfig của Spring Cloud.
  • spring-ai-starter-*.jar — autoconfig của Spring AI.
  • Custom starter (bài 02 section 6) — bạn viết file này.

Trước Boot 2.7, dùng file META-INF/spring.factories với key EnableAutoConfiguration=.... Boot 3+ migrate sang AutoConfiguration.imports (file text đơn giản hơn) — backward compat vẫn đọc spring.factories.

flowchart TB
    Cs["Classpath scan"]
    F1["spring-boot-autoconfigure.jar<br/>AutoConfiguration.imports"]
    F2["spring-cloud-*.jar<br/>AutoConfiguration.imports"]
    F3["custom-starter.jar<br/>AutoConfiguration.imports"]
    Merged["143 + N autoconfig<br/>candidates"]
    Filter["@ConditionalOn* filter"]
    Active["~30-50 autoconfig<br/>thuc su register"]

    Cs --> F1
    Cs --> F2
    Cs --> F3
    F1 --> Merged
    F2 --> Merged
    F3 --> Merged
    Merged --> Filter
    Filter --> Active

    style Active fill:#d1fae5

3.2 Filter qua @Conditional*

143 autoconfig là candidate — không phải tất cả đều register. Mỗi autoconfig có 1+ @Conditional* annotation. Spring Boot evaluate condition tại runtime, chỉ register autoconfig pass tất cả condition.

Ví dụ DataSourceAutoConfiguration:

@AutoConfiguration
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@ConditionalOnMissingBean(type = "io.r2dbc.spi.ConnectionFactory")
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({DataSourcePoolMetadataProvidersConfiguration.class,
         DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {

    @Configuration(proxyBeanMethods = false)
    @Conditional(EmbeddedDatabaseCondition.class)
    @ConditionalOnMissingBean({DataSource.class, XADataSource.class})
    @Import(EmbeddedDataSourceConfiguration.class)
    protected static class EmbeddedDatabaseConfiguration { }

    @Configuration(proxyBeanMethods = false)
    @Conditional(PooledDataSourceCondition.class)
    @ConditionalOnMissingBean({DataSource.class, XADataSource.class})
    @Import({...})
    protected static class PooledDataSourceConfiguration { }
}

DataSourceAutoConfiguration register chỉ khi:

  • DataSource class trên classpath (HikariCP/JDBC).
  • Không có ConnectionFactory bean (đó là R2DBC reactive — nếu có thì user dùng reactive, không cần autoconfig JDBC).

Bên trong, 2 nested config:

  • EmbeddedDatabaseConfiguration: nếu classpath có H2/Derby/HSQLDB và user chưa khai DataSource → tạo embedded DB cho dev.
  • PooledDataSourceConfiguration: nếu có HikariCP và user chưa khai DataSource → tạo Hikari pool.

Hệ quả thực tế: thêm spring-boot-starter-data-jpa + property spring.datasource.url → Boot tự register HikariDataSource, EntityManagerFactory, JpaTransactionManager. Bạn không viết dòng config nào.

4. Family @ConditionalOn* — 12 annotation

Bảng đầy đủ (mở rộng từ Module 01 bài 06):

AnnotationCondition
@ConditionalOnClassClass này có trong classpath
@ConditionalOnMissingClassClass không có
@ConditionalOnBeanBean này đã register trong context
@ConditionalOnMissingBeanBean chưa register — fallback default
@ConditionalOnPropertyProperty có giá trị mong đợi
@ConditionalOnExpressionSpEL expression true
@ConditionalOnWebApplicationApp là web (servlet/reactive)
@ConditionalOnNotWebApplicationApp không web
@ConditionalOnResourceResource tồn tại trên classpath
@ConditionalOnJavaJava version match
@ConditionalOnCloudPlatformCloud platform detect (CF, K8s, ...)
@ConditionalOnJndiJNDI resource available

4.1 @ConditionalOnClass — phổ biến nhất

@AutoConfiguration
@ConditionalOnClass(RedisTemplate.class)
public class RedisAutoConfiguration { ... }

Cơ chế: Spring đọc bytecode autoconfig class, không thực sự load RedisTemplate. Lý do: nếu Redis không có, load class sẽ throw ClassNotFoundException → autoconfig fail trước khi check condition.

@ConditionalOnClass parameter có 2 dạng:

  • value = SomeClass.class — phải import được class. Risky.
  • name = "fully.qualified.ClassName" — string, không cần import.

Best practice: dùng name cho autoconfig của starter, vì autoconfig phải compile được kể cả khi class kia không có classpath.

4.2 @ConditionalOnMissingBean — cơ chế "user override"

@AutoConfiguration
public class CacheAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();    // default cua Boot
    }
}

Logic: chỉ register cacheManager nếu user chưa register bean CacheManager. Đây là cách Boot "rút lui" khi user override.

@Configuration
public class UserCustomConfig {
    @Bean
    public CacheManager cacheManager() {
        return new RedisCacheManager(...);    // user custom
    }
}

Có bean này → Boot autoconfig skip → user's bean win. Đây là contract chính của "opinionated, not lock-in".

@ConditionalOnMissingBean có 3 attribute:

  • value (default): bean class.
  • name: bean name.
  • type (string): tránh ClassNotFound nếu type không có classpath.

4.3 @ConditionalOnProperty — feature flag

@AutoConfiguration
@ConditionalOnProperty(
    name = "feature.tracing.enabled",
    havingValue = "true",
    matchIfMissing = false
)
public class TracingAutoConfiguration { ... }

Register chỉ khi feature.tracing.enabled=true. matchIfMissing=false (default) → property phải có. Set matchIfMissing=true để autoconfig active mặc định.

Pattern phổ biến trong Boot autoconfig:

@ConditionalOnProperty(
    prefix = "spring.jpa",
    name = "open-in-view",
    havingValue = "true",
    matchIfMissing = true       // default true
)
public class OpenEntityManagerInViewAutoConfiguration { ... }

Boot bật OSIV (open-session-in-view) by default. User tắt qua spring.jpa.open-in-view=false.

4.4 @ConditionalOnWebApplication

@AutoConfiguration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class WebMvcAutoConfiguration { ... }

@AutoConfiguration
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
public class WebFluxAutoConfiguration { ... }

Boot detect web stack qua classpath:

  • spring-webmvc → SERVLET.
  • spring-webflux → REACTIVE.
  • Cả 2 → SERVLET (ưu tiên).
  • Không có → ANY = false → CLI app.

4.5 Combine condition

Nhiều @ConditionalOn* cùng class = AND logic. Tất cả phải pass:

@AutoConfiguration
@ConditionalOnClass(KafkaTemplate.class)              // co Kafka classpath
@ConditionalOnProperty(name = "spring.kafka.bootstrap-servers")   // co property
@ConditionalOnMissingBean(KafkaTemplate.class)       // user chua khai
public class KafkaAutoConfiguration { ... }

3 điều kiện cùng pass → register.

5. Autoconfig ordering — @AutoConfigureBefore/After

Một số autoconfig phụ thuộc autoconfig khác. Vd JpaRepositoriesAutoConfiguration cần EntityManagerFactory đã được autoconfig tạo. Spring dùng 2 annotation:

@AutoConfiguration(after = HibernateJpaAutoConfiguration.class)
public class JpaRepositoriesAutoConfiguration { ... }

@AutoConfiguration(before = WebMvcAutoConfiguration.class)
public class HypermediaAutoConfiguration { ... }

@AutoConfigureAfter/Before set thứ tự evaluate — autoconfig sau xét sau, có cơ hội thấy bean đã được autoconfig trước register.

Ví dụ chain:

flowchart LR
    DSC["DataSourceAutoConfiguration"]
    HJC["HibernateJpaAutoConfiguration<br/>@AutoConfigureAfter(DSC)"]
    JRC["JpaRepositoriesAutoConfiguration<br/>@AutoConfigureAfter(HJC)"]

    DSC --> HJC --> JRC

    style DSC fill:#fef3c7

DSC chạy trước → tạo DataSource. HJC chạy sau → thấy DataSource rồi → tạo EntityManagerFactory. JRC chạy cuối → tạo Repository proxy dùng EntityManagerFactory.

Nếu thứ tự sai (vd JRC chạy trước HJC), @ConditionalOnBean(EntityManagerFactory.class) của JRC sẽ false → JRC skip → repository không tạo. Đây là lý do ordering quan trọng.

@AutoConfigureOrder cho fine-grained ordering với số:

@AutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
public class EarlyAutoConfiguration { ... }

6. Debug autoconfig — Actuator /conditions

Khi autoconfig không hoạt động như mong đợi, đừng đoán — bật Actuator endpoint /actuator/conditions:

management:
  endpoints:
    web:
      exposure:
        include: health,conditions,beans,env,mappings

GET /actuator/conditions:

{
  "contexts": {
    "application": {
      "positiveMatches": {
        "DataSourceAutoConfiguration#dataSource": [
          {"condition": "OnPropertyCondition", "message": "spring.datasource.url has value 'jdbc:postgresql://...'"},
          {"condition": "OnClassCondition", "message": "DataSource class found"}
        ]
      },
      "negativeMatches": {
        "RedisAutoConfiguration": {
          "notMatched": [
            {"condition": "OnClassCondition", "message": "RedisTemplate class not found"}
          ]
        },
        "KafkaAutoConfiguration": {
          "notMatched": [
            {"condition": "OnPropertyCondition", "message": "spring.kafka.bootstrap-servers not set"}
          ]
        }
      },
      "exclusions": [],
      "unconditionalClasses": [...]
    }
  }
}

3 section quan trọng:

  • positiveMatches: autoconfig đã register, kèm condition pass.
  • negativeMatches: autoconfig skip, kèm lý do (cực hữu ích để debug "tại sao bean tôi không tạo").
  • exclusions: autoconfig user explicitly exclude.

Cách dùng thực tế:

  1. Bug: "Redis cache không hoạt động."
  2. Curl /actuator/conditions → search "Redis".
  3. Thấy RedisAutoConfiguration trong negativeMatches với reason "RedisTemplate class not found".
  4. → Quên thêm spring-boot-starter-data-redis. Fix.

Cách thay thế: chạy app với flag --debug:

java -jar app.jar --debug

→ Boot in autoconfig report ra console khi start. Dùng cho dev local. Production bật Actuator.

7. Cơ chế evaluation tối ưu

143 autoconfig × N condition = nhiều check. Boot tối ưu bằng:

7.1 Caching condition

OnClassCondition cache kết quả Class.forName() — chỉ resolve 1 lần per class name.

7.2 Bulk class check qua bytecode

Boot scan class autoconfig bằng ASM (bytecode reader) — đọc annotation mà không load class. Lý do: load class → init static field → side effect. ASM chỉ đọc bytecode, no init.

7.3 Auto-configuration metadata

File META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports chỉ liệt kê fully-qualified name. Có thêm 1 file metadata:

META-INF/spring-autoconfigure-metadata.properties

File này contain pre-computed condition info — vd OnClassCondition của mỗi autoconfig — generate tại Boot build time. Spring Boot dùng metadata này để filter autoconfig sớm trước khi load class:

org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration.ConditionalOnClass=\
org.apache.kafka.clients.producer.KafkaProducer

Boot đọc metadata → check KafkaProducer class có không — nếu không, skip autoconfig hoàn toàn (không load KafkaAutoConfiguration class). Đây là tối ưu lớn nhất — giảm autoconfig load từ 143 xuống ~30-50 cho app trung bình.

8. Viết autoconfig của bạn

Bài 02 section 6 đã giới thiệu cấu trúc — đây là chi tiết hoàn chỉnh:

8.1 TracingAutoConfiguration.java

package com.acme.tracing.autoconfigure;

@AutoConfiguration                               // = @Configuration + ordering
@ConditionalOnClass(Tracer.class)                // chi register khi Tracer co classpath
@ConditionalOnProperty(
    prefix = "acme.tracing",
    name = "enabled",
    matchIfMissing = true                        // bat default
)
@EnableConfigurationProperties(TracingProperties.class)
public class TracingAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean                    // user override duoc
    public Tracer tracer(TracingProperties props) {
        return Tracer.builder()
            .endpoint(props.getEndpoint())
            .serviceName(props.getServiceName())
            .build();
    }

    @Bean
    @ConditionalOnMissingBean
    @ConditionalOnBean(Tracer.class)            // chi tao filter neu co Tracer
    public TracingWebFilter tracingFilter(Tracer tracer) {
        return new TracingWebFilter(tracer);
    }
}

8.2 TracingProperties.java

@ConfigurationProperties(prefix = "acme.tracing")
public class TracingProperties {

    /** Enable/disable tracing. */
    private boolean enabled = true;

    /** Tracing collector endpoint. */
    private String endpoint = "https://tracing.acme.internal:9411/api/v2/spans";

    /** Logical service name. Required. */
    private String serviceName;

    /** Sampling rate 0.0-1.0. */
    private double samplingRate = 1.0;

    // getters/setters
}

8.3 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

com.acme.tracing.autoconfigure.TracingAutoConfiguration

(File text, mỗi dòng 1 class).

8.4 (Optional) META-INF/additional-spring-configuration-metadata.json

Để IDE autocomplete property:

{
  "properties": [
    {
      "name": "acme.tracing.enabled",
      "type": "java.lang.Boolean",
      "description": "Enable/disable tracing for this service.",
      "defaultValue": true
    },
    {
      "name": "acme.tracing.endpoint",
      "type": "java.lang.String",
      "description": "Tracing collector endpoint URL.",
      "defaultValue": "https://tracing.acme.internal:9411/api/v2/spans"
    },
    {
      "name": "acme.tracing.service-name",
      "type": "java.lang.String",
      "description": "Logical service name for traces."
    }
  ]
}

Hoặc dùng annotation processor spring-boot-configuration-processor — tự generate metadata từ Javadoc:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

9. Pitfall tổng hợp

Nhầm 1: Tin "auto-config = magic không bóc được." ✅ Auto-config là Java class với annotation @Conditional*. Mở source bất kỳ autoconfig (vd DataSourceAutoConfiguration trên GitHub) đọc 1 lần — magic biến mất.

Nhầm 2: Đoán "tại sao bean không tạo" thay vì check /actuator/conditions.

// Bug: Redis cache khong work
@Service public class S {
    @Autowired CacheManager cm;     // null hoac default ConcurrentMap thay Redis
}

✅ Curl /actuator/conditions → search Redis → đọc reason. 5 phút thay 5 giờ.

Nhầm 3: Dùng @ConditionalOnClass(value = ThirdPartyClass.class) trong autoconfig của starter mà starter không pull ThirdPartyClass.

@ConditionalOnClass(KafkaTemplate.class)    // KafkaTemplate co the khong co classpath
public class MyAutoConfig { ... }

Compile fail nếu starter không bring spring-kafka. ✅ Dùng @ConditionalOnClass(name = "org.springframework.kafka.core.KafkaTemplate") — string, không require import.

Nhầm 4: Quên @ConditionalOnMissingBean cho default bean.

@Bean
public CacheManager cacheManager() { return new ConcurrentMapCacheManager(); }

User register RedisCacheManager → conflict, ambiguous bean. ✅ Thêm @ConditionalOnMissingBean để Boot rút lui.

Nhầm 5: Đặt autoconfig ở package mà bean không có Java import path đến class condition. ✅ Autoconfig nên ở package độc lập, không phụ thuộc package khác. Tham khảo cách Boot tổ chức: org.springframework.boot.autoconfigure.X.

Nhầm 6: Quên file AutoConfiguration.imports. ✅ File này là bắt buộc — không có nó, autoconfig class không bao giờ load. Path chính xác: src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports.

Nhầm 7: Override autoconfig bằng cách extends class autoconfig.

public class MyDataSourceConfig extends DataSourceAutoConfiguration { ... }

✅ Override bằng cách register @Bean DataSource của bạn — Boot tự rút lui qua @ConditionalOnMissingBean. Không bao giờ extends.

10. 📚 Deep Dive Spring Reference

📚 Tài liệu chính chủ

Spring Boot Reference docs:

Source code chính chủ:

Bài viết:

Tool quan trọng:

  • mvn dependency:tree — xem starter pull jar nào.
  • /actuator/conditions — runtime debug.
  • --debug flag — startup autoconfig report.
  • IntelliJ IDEA "Spring Boot AutoConfiguration" tool window — visualize autoconfig graph.

Ghi chú: đọc source DataSourceAutoConfiguration 1 lần, sau đó mọi autoconfig khác sẽ "click". Tất cả Boot autoconfig follow cùng pattern: @AutoConfiguration + @ConditionalOn* + @EnableConfigurationProperties + @Bean @ConditionalOnMissingBean. Hiểu 1, hiểu hết.

11. Tóm tắt

  • Auto-configuration là cơ chế Boot tự register bean dựa trên classpath + property — xoá 30-100 dòng XML config Spring 4.
  • Entry point: @EnableAutoConfiguration (trong @SpringBootApplication) → @Import(AutoConfigurationImportSelector.class).
  • AutoConfigurationImportSelector đọc file META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports từ mọi jar trên classpath.
  • Boot 3.4 có 143 autoconfig core; thêm starter (Cloud, AI, custom) đóng góp thêm.
  • Mỗi autoconfig pass qua filter @ConditionalOn* — chỉ register khi mọi condition true.
  • 12 family @ConditionalOn*: OnClass, OnMissingClass, OnBean, OnMissingBean, OnProperty, OnExpression, OnWebApplication, OnNotWebApplication, OnResource, OnJava, OnCloudPlatform, OnJndi.
  • @ConditionalOnMissingBean là cơ chế "user override default" — Boot tự rút lui khi user register bean.
  • @AutoConfigureBefore/After/Order quản thứ tự autoconfig evaluate — quan trọng cho dependency chain.
  • Debug autoconfig qua /actuator/conditions (production) hoặc --debug flag (dev). Đừng đoán.
  • Tối ưu evaluation: ASM bytecode reader (no class load), spring-autoconfigure-metadata.properties (pre-computed condition).
  • Viết autoconfig custom: @AutoConfiguration class + @ConfigurationProperties + file AutoConfiguration.imports + (optional) configuration metadata cho IDE.

12. Tự kiểm tra

Tự kiểm tra
Q1
Bạn add starter spring-boot-starter-data-redis, set spring.data.redis.host=localhost. Boot tự register RedisTemplate bean. Nhưng team thắc mắc "magic này hoạt động ra sao?". Giải thích chuỗi sự kiện từ classpath đến bean.

Chuỗi 6 bước:

  1. Maven pull starter: spring-boot-starter-data-redis pull spring-data-redis, lettuce-core (Redis client), spring-boot-autoconfigure.
  2. App start, SpringApplication.run() trigger refresh(): bước 5 (invokeBeanFactoryPostProcessors) chạy ConfigurationClassPostProcessor, parse @SpringBootApplication trên class App.
  3. @EnableAutoConfiguration trigger AutoConfigurationImportSelector: selector load file AutoConfiguration.imports từ mọi jar — bao gồm jar spring-boot-autoconfigure chứa RedisAutoConfiguration trong list.
  4. Filter qua condition:
    • @ConditionalOnClass(RedisTemplate.class)RedisTemplate có classpath (từ spring-data-redis) → pass.
    • @ConditionalOnMissingBean(RedisTemplate.class) → user chưa register → pass.
    • RedisAutoConfiguration register.
  5. Trong RedisAutoConfiguration@EnableConfigurationProperties(RedisProperties.class) → bind spring.data.redis.* property vào RedisProperties.
  6. Bean @Bean RedisTemplate tạo với RedisProperties.host=localhost. LettuceConnectionFactory connect đến Redis.

Verify chuỗi: bật /actuator/conditions → tìm RedisAutoConfiguration trong positiveMatches → thấy condition nào pass.

Bài học: không có magic. Chuỗi rõ ràng từ classpath → autoconfig load → condition filter → bean register.

Q2
Đoạn sau có gì sai? Khi nào sai sẽ lộ?
@AutoConfiguration
@ConditionalOnClass(KafkaTemplate.class)
public class MyKafkaConfig {

  @Bean
  public KafkaProducer myProducer(KafkaTemplate<String, String> template) {
      return new KafkaProducer(template);
  }
}

2 vấn đề:

  1. Compile-time risk: autoconfig file viết @ConditionalOnClass(KafkaTemplate.class). Class KafkaTemplate phải có trong classpath khi compile autoconfig. Nếu starter của bạn không pull spring-kafka → compile fail.

    Fix: dùng name attribute (string):

    @ConditionalOnClass(name = "org.springframework.kafka.core.KafkaTemplate")

    Compile OK kể cả khi KafkaTemplate không có. Runtime evaluate qua Class.forName().

  2. Thiếu @ConditionalOnMissingBean: nếu user register myProducer bean của họ, ambiguous → Spring throw NoUniqueBeanDefinitionException. Boot autoconfig **luôn** nên có @ConditionalOnMissingBean để rút lui khi user override.

    Fix:

    @Bean
    @ConditionalOnMissingBean
    public KafkaProducer myProducer(KafkaTemplate<String, String> template) { ... }

Khi nào sai sẽ lộ:

  • Vấn đề 1: lúc mvn compile autoconfig module — fail nếu missing dependency.
  • Vấn đề 2: lúc service team override bean → conflict tại startup runtime.

Best practice: mọi autoconfig public-facing dùng name string + @ConditionalOnMissingBean. Verify với test integration: 1 test với class trên classpath, 1 test không có.

Q3
Bạn debug "tại sao Spring Cache không hoạt động" dù đã thêm spring-boot-starter-cache + @EnableCaching. Cách đúng để diagnose?

Quy trình debug — KHÔNG đoán, dùng tool:

  1. Bật Actuator /actuator/conditions:
    management.endpoints.web.exposure.include: health,conditions
    management.endpoint.conditions.show-values: always
    Curl http://localhost:8080/actuator/conditions, search "Cache".
  2. Đọc positiveMatchesnegativeMatches:
    • Nếu CacheAutoConfiguration trong negativeMatches với reason "no Cache provider found" → thiếu cache provider (Caffeine/Redis/EhCache). Fix: thêm spring-boot-starter-cache + provider lib.
    • Nếu trong positiveMatches nhưng cache không work → vấn đề khác (vd @Cacheable trên private method, hoặc method called từ same class).
  3. Verify bean CacheManager tồn tại: curl /actuator/beans search cacheManager. Nếu không có → autoconfig skip.
  4. Bật log DEBUG cho cache:
    logging.level.org.springframework.cache: DEBUG
    Spring sẽ log mọi @Cacheable hit/miss.
  5. Verify @EnableCaching trên @Configuration class (hoặc class với @SpringBootApplication).
  6. Verify proxy mode: @Cacheable chỉ work qua proxy. Self-call (this.cachedMethod()) bypass proxy → cache không invoke. Module 01 bài 04.

Sai lầm phổ biến — đừng làm:

  • "Thử thêm @EnableCaching(proxyTargetClass = true) coi sao" — guess thay debug.
  • "Restart app coi may không" — không deterministic.
  • "Add log System.out.println trong method" — không xác minh autoconfig.

Quy tắc: autoconfig issue → /actuator/conditions trước, mọi cách khác sau.

Q4
Đoạn sau có 2 autoconfig. Thứ tự đăng ký bean ra sao? Bean nào được tạo, bean nào skip?
@AutoConfiguration
public class A1 {
  @Bean
  public Foo foo() { return new Foo("from-A1"); }
}

@AutoConfiguration(after = A1.class)
public class A2 {
  @Bean
  @ConditionalOnMissingBean
  public Foo foo() { return new Foo("from-A2"); }

  @Bean
  @ConditionalOnBean(Foo.class)
  public Bar bar(Foo foo) { return new Bar(foo); }
}

Thứ tự evaluate: A1 trước (do @AutoConfiguration(after = A1.class) trên A2 ép A2 sau).

Kết quả:

  1. A1 register → foo bean = "from-A1".
  2. A2 evaluate:
    • foo() trong A2 có @ConditionalOnMissingBean → A1 đã register fooSKIP. Bean foo giữ "from-A1".
    • bar()@ConditionalOnBean(Foo.class) → A1 đã tạo fooregister. bar nhận "from-A1" Foo.

Output cuối:

  • 1 bean foo = Foo("from-A1")
  • 1 bean bar = Bar(foo("from-A1"))

Nếu bỏ @AutoConfiguration(after = A1.class) → thứ tự không xác định. Có thể A2 evaluate trước A1:

  • foo A2 register (vì A1 chưa chạy → @ConditionalOnMissingBean pass).
  • A1 evaluate sau → foo() A1 không có @ConditionalOnMissingBean → throw BeanDefinitionOverrideException (Boot 2.1+ mặc định cấm override).

Bài học:

  • @ConditionalOnMissingBean phụ thuộc thứ tự — autoconfig đến trước win.
  • @AutoConfigureBefore/After đảm bảo deterministic ordering.
  • Mọi @Bean default (registered by autoconfig) nên có @ConditionalOnMissingBean — best practice.
Q5
Vì sao Boot dùng ASM bytecode reader thay vì Class.forName() để check @ConditionalOnClass? Lợi ích cụ thể?

Lý do chính: tránh load class chưa cần thiết → tránh init static field → tránh side effect ngoài ý muốn.

Vấn đề với Class.forName():

  • Khi load class, JVM init static field, static block, ... → có thể trigger DB connect, file IO, log.
  • Class load 1 lần stuck trong ClassLoader cache — không unload được.
  • Nếu class fail load (vd dependency missing), exception thrown → autoconfig fail thay skip.
  • Performance: load 143 autoconfig class + dependency của chúng = startup chậm.

Cách Boot dùng ASM:

  • ASM đọc bytecode autoconfig class (không load).
  • Parse annotation table để extract @ConditionalOnClass value.
  • Check class name có trong classpath qua ClassLoader.getResource() (chỉ check file .class tồn tại, không load).
  • Nếu pass condition → load autoconfig class qua Class.forName() bình thường.
  • Nếu fail → skip, không load.

Lợi ích cụ thể:

  • Startup nhanh hơn: ~30-50% bean autoconfig skip không load → giảm class loading overhead.
  • Memory thấp hơn: class skip không vào metaspace.
  • No side effect: autoconfig fail load không trigger init code.
  • Native image friendly: ASM bytecode analysis tương thích GraalVM AOT compilation.

Kết hợp với spring-autoconfigure-metadata.properties (pre-computed condition info) — Boot biết "skip class này" thậm chí trước khi đọc bytecode → tối ưu thêm 1 lớp.

Verify: bật --debug, count autoconfig "negative match" — đó là class Boot skip không load nhờ ASM filter.

Q6
Team enterprise chuyển từ "starter custom + autoconfig" sang đơn giản hoá thành "@Import trong app". So sánh 2 approach. Khi nào nên dùng cái nào?

Approach 1 — Starter + autoconfig (recommend cho lib reusable):

<dependency>
  <groupId>com.acme</groupId>
  <artifactId>acme-tracing-spring-boot-starter</artifactId>
</dependency>

# application.yml
acme.tracing:
enabled: true
service-name: order-service

Approach 2 — @Import explicit (đơn giản):

@SpringBootApplication
@Import(TracingConfig.class)
public class App { ... }

@Configuration
public class TracingConfig {
  @Bean
  public Tracer tracer() {
      return Tracer.builder()...build();
  }
}
AspectStarter + autoconfig@Import
ErgonomicsAdd 1 dep, set propertyAdd @Import trong code app
ConditionalCó (via @ConditionalOn*)Không (manual check)
Default + overrideTự nhiên (@ConditionalOnMissingBean)Phải code thủ công
IDE autocomplete propertyCó (configuration metadata)Không
Actuator /conditions visibilityKhông
Setup overhead2 module (starter + autoconfig)0 — chỉ class config
VersioningBump starter versionKhông có version concept

Khi dùng cái nào:

  • Starter + autoconfig: lib internal share giữa 5+ service team. Standardization quan trọng. Cần feature flag, conditional, default + override. Cần versioning để rollout dần.
  • @Import: config dùng trong 1 app. Hoặc lib nhỏ trong 1 monorepo. Hoặc prototype/early-stage chưa cần extract.

Quy tắc thực tế: bắt đầu với @Import. Khi thấy 3+ service copy-paste cùng config → extract thành starter. Đừng over-engineer ngay từ đầu.

Q7
Boot 3.4 có 143 autoconfig core. Tại 1 app điển hình, chỉ ~30-50 autoconfig active. Phần còn lại đi đâu? Tại sao Boot vẫn load 143 nếu chỉ 30 active?

"Đi đâu": 100+ autoconfig còn lại nằm trong negativeMatches của /actuator/conditions — Boot evaluate condition, fail, skip.

Vì sao Boot "load" 143: chính xác hơn — Boot **không thực sự load** class autoconfig nếu condition đầu fail. Nhờ 2 lớp tối ưu:

  1. Pre-filter qua spring-autoconfigure-metadata.properties: file metadata chứa pre-computed condition info. Boot đọc metadata, filter sớm autoconfig nào condition fail rõ ràng (vd @ConditionalOnClass với class không có classpath). Loại 50-80 autoconfig trước khi load class.
  2. ASM bytecode reader: đọc annotation autoconfig class qua bytecode (không load class). Filter thêm 30-50 autoconfig.
  3. Còn lại ~30-50 autoconfig: condition phức tạp cần evaluate runtime → load class, run condition. Pass → register bean. Fail → skip.

Performance impact:

  • Không có 2 tối ưu trên: 143 class load → ~3-5s startup chỉ cho phase autoconfig.
  • Có tối ưu: ~500ms cho phase autoconfig. Phần lớn startup nằm ở instantiate bean (bước 11 refresh).

Verify: /actuator/startup endpoint (Boot 3+) breakdown thời gian từng bean. Phase "load autoconfig metadata" thường <100ms thậm chí với 143 autoconfig.

Bài học: 143 candidates không có nghĩa là 143 bean. Chỉ subset condition pass mới register. Đó là design intentional — Boot có nhiều default sẵn, user opt-in qua starter/property.

Bài tiếp theo: application.properties vs application.yml — externalized configuration sâu

Bài này có giúp bạn hiểu bản chất không?

Bình luận (0)

Đang tải...