Spring Core & Boot/@ConditionalOn* family + back-off pattern + autoconfig ordering
31/41
Bài 31 / 41~13 phútSpring Boot & Auto-configurationMiễn phí lượt xem

@ConditionalOn* family + back-off pattern + autoconfig ordering

Bóc tách 12 annotation @ConditionalOn* trong auto-configuration: cơ chế điều kiện, back-off pattern qua @ConditionalOnMissingBean, thứ tự evaluate qua @AutoConfigureBefore/After, và debug bằng Actuator /conditions.

TL;DR: @ConditionalOn* là cơ chế tất cả-hoặc-không-có-gì của autoconfig — mỗi annotation đặt một câu hỏi về môi trường runtime (classpath? bean đã có chưa? property có không?), chỉ register bean khi mọi điều kiện pass. Trong đó @ConditionalOnMissingBeanchìa khoá "back-off": autoconfig đăng ký bean mặc định nhưng kiểm tra trước xem user đã khai rồi chưa — nếu rồi thì nhường. @AutoConfigureBefore/After xác định thứ tự evaluate giữa các autoconfig: cần thiết vì @ConditionalOnMissingBean phụ thuộc thứ tự — autoconfig đến trước sẽ win. Debug bằng /actuator/conditions thay vì đoán.

Bài 03 — Auto-configuration deep dive bóc toàn bộ pipeline AutoConfigurationImportSelector → file AutoConfiguration.imports → 143 candidate → filter. Bài này đào sâu một bước của filter đó: các annotation điều kiện và lý do thứ tự evaluate quan trọng. Bài xây trên nền @Conditionalbài @Conditional bean.

1. Bài toán: autoconfig register bean không điều kiện là thảm hoạ

Giả sử Spring Boot không có điều kiện nào — mọi autoconfig luôn register bean:

// Gia su khong co @Conditional
@AutoConfiguration
public class CacheAutoConfiguration {
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();   // luon tao, khong hoi gi
    }
}

Và team bạn muốn dùng Redis cho cache:

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

Kết quả: Spring context có 2 bean CacheManager — conflict → NoUniqueBeanDefinitionException ngay lúc startup.

Đây là lý do mọi @Bean trong autoconfig đều cần điều kiện: autoconfig phải biết khi nào nên register, khi nào nên nhường. Hai nhóm điều kiện giải quyết 2 câu hỏi khác nhau.

2. Family 12 annotation @ConditionalOn*

Spring Boot cung cấp 12 annotation điều kiện sẵn, mỗi cái hỏi một thứ về môi trường:

AnnotationCâu hỏi
@ConditionalOnClassClass này có trong classpath không?
@ConditionalOnMissingClassClass này không có trong classpath?
@ConditionalOnBeanBean kiểu/tên này đã register trong context chưa?
@ConditionalOnMissingBeanBean kiểu/tên này chưa register?
@ConditionalOnPropertyProperty có giá trị mong đợi không?
@ConditionalOnExpressionSpEL expression có true không?
@ConditionalOnWebApplicationApp đang chạy trong web context?
@ConditionalOnNotWebApplicationApp không phải web context?
@ConditionalOnResourceResource (classpath:/…) tồn tại không?
@ConditionalOnJavaJava version match không?
@ConditionalOnCloudPlatformCloud platform detect (K8s, CF, …)?
@ConditionalOnJndiJNDI resource available?

Nhiều annotation cùng class = AND logic: tất cả phải pass, thiếu một là skip.

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

Ba điều kiện song song — fail bất kỳ cái nào → autoconfig toàn bộ skip.

2.1 @ConditionalOnClass — đo classpath

@AutoConfiguration
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
public class RedisAutoConfiguration { ... }

Lý do dùng name (string) thay value (class literal): autoconfig phải compile được kể cả khi RedisTemplate không có classpath. Nếu dùng value = RedisTemplate.class và class không có → compile fail. String name tránh vấn đề này — runtime evaluate qua ClassLoader.getResource().

Bài 03 đã giải thích Boot dùng ASM bytecode reader thay Class.forName() để check condition này mà không load class.

2.2 @ConditionalOnProperty — feature flag

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

matchIfMissing = true nghĩa là "nếu property vắng mặt thì coi như điều kiện pass". Dùng để bật tính năng theo mặc định, cho user tắt qua spring.jpa.open-in-view=false.

3. Back-off pattern — @ConditionalOnMissingBean

Đây là cơ chế cốt lõi của "opinionated but not lock-in". Một autoconfig điển hình:

@AutoConfiguration
public class CacheAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean    // chi tao neu user CHUA co CacheManager bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager();   // default
    }
}

Luồng evaluation chỉ có một câu hỏi: đã có bean kiểu CacheManager trong context chưa? Có rồi thì Boot bỏ qua @Bean của mình và dùng bean của user; chưa có thì tạo ConcurrentMapCacheManager làm default.

Vì sao đây là "chìa khoá override": user chỉ cần khai bean với đúng kiểu, Boot tự nhận ra và nhường. Không cần annotation đặc biệt, không cần file config nào — chỉ cần @Bean:

@Configuration
public class AppConfig {
    @Bean
    public CacheManager cacheManager() {
        // Boot thay CacheManager da ton tai -> skip CacheAutoConfiguration.cacheManager()
        return new RedisCacheManager(redisConnectionFactory());
    }
}

Tính chất này cho phép "đúng là default ra của hộp, sai thì override một chỗ" — không phải sửa source của Boot hay exclude autoconfig toàn bộ. Hai thao tác đó nghe như thay thế nhau, nhưng thứ bạn mất kèm thì khác hẳn:

Đối chiếu hai cách đổi CacheManager sang Redis: cách khai @Bean CacheManager của bạn thì @ConditionalOnMissingBean thấy bean đã có nên nhường đúng bean đó, phần còn lại của autoconfig vẫn chạy gồm binding property, bean phụ và listener trong cùng class; cách exclude cả CacheAutoConfiguration thì cả class bị loại khỏi danh sách ngay từ bước filter nên mất luôn binding property, listener và bean phụ, phải tự khai lại bằng tay

3.1 Ba attribute của @ConditionalOnMissingBean

@ConditionalOnMissingBean                           // check theo kieu cua @Bean return type
@ConditionalOnMissingBean(CacheManager.class)       // check theo class cu the
@ConditionalOnMissingBean(name = "cacheManager")    // check theo bean name
@ConditionalOnMissingBean(
    type = "org.springframework.cache.CacheManager" // string -- tranh ClassNotFound
)

Dùng type (string) khi interface/class có thể không có classpath — tương tự lý do dùng name trong @ConditionalOnClass.

3.2 Pitfall back-off: annotation thiếu → bean conflict

// SAI -- boot autoconfig khong co @ConditionalOnMissingBean:
@AutoConfiguration
public class TracingAutoConfiguration {
    @Bean
    public Tracer tracer() { return new DefaultTracer(); }
}

// User cung khai Tracer:
@Configuration
public class AppConfig {
    @Bean
    public Tracer tracer() { return new JaegerTracer(...); }
}

Spring context có 2 bean TracerNoUniqueBeanDefinitionException lúc startup. Fix:

@Bean
@ConditionalOnMissingBean    // bat buoc
public Tracer tracer() { return new DefaultTracer(); }

Quy tắc vàng: mọi @Bean trong autoconfig public nên có @ConditionalOnMissingBean — không có ngoại lệ trừ khi bạn chủ ý forbid override.

4. Ordering — @AutoConfigureBefore/After

4.1 Vì sao thứ tự quan trọng

@ConditionalOnMissingBean phụ thuộc vào bean nào đã có trong context tại thời điểm evaluate. Nếu hai autoconfig không có thứ tự rõ ràng, kết quả không xác định:

// A1 va A2 khong co ordering
@AutoConfiguration
public class A1 {
    @Bean
    public Foo foo() { return new Foo("from-A1"); }
}

@AutoConfiguration
public class A2 {
    @Bean
    @ConditionalOnMissingBean   // neu A2 chay truoc A1 -> Foo chua co -> register
    public Foo foo() { return new Foo("from-A2"); }
}

Nếu A2 evaluate trước A1: A2 register Foo("from-A2"). Sau đó A1 evaluate — không có @ConditionalOnMissingBean → cố tạo thêm FooBeanDefinitionOverrideException.

Nếu A1 evaluate trước A2: A1 register Foo("from-A1"). A2 thấy Foo đã có → skip. Ổn.

Đối chiếu hai thứ tự evaluate cho cùng một cặp autoconfig: A khai @Bean Foo không điều kiện, B khai @Bean Foo có @ConditionalOnMissingBean; nhánh A được xét trước thì A đăng ký Foo from-A, B thấy Foo đã có nên nhường, context có đúng một bean Foo; nhánh B được xét trước thì B thấy chưa có Foo nên đăng ký Foo from-B, A không có điều kiện nên đăng ký đè lên và nổ BeanDefinitionOverrideException ngay lúc startup

Kết quả phụ thuộc vào thứ tự không xác định → bug không tái hiện được.

4.2 @AutoConfigureBefore/After — đặt ordering rõ ràng

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

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

Chuỗi phụ thuộc chạy theo đúng thứ tự khai báo: DataSourceAutoConfiguration tạo DataSourceHibernateJpaAutoConfiguration tạo EntityManagerFactoryJpaRepositoriesAutoConfiguration tạo repository proxy. Mỗi bước xây trên kết quả của bước trước. Nếu JpaRepositoriesAutoConfiguration chạy trước HibernateJpaAutoConfiguration, @ConditionalOnBean(EntityManagerFactory.class) của nó sẽ false → skip → repository không tạo → @Autowired fail lúc runtime.

4.3 Cú pháp — 3 cách

// Cach 1: annotation attribute (Boot 2.7+, uu tien)
@AutoConfiguration(after = HibernateJpaAutoConfiguration.class)
@AutoConfiguration(before = WebMvcAutoConfiguration.class)

// Cach 2: annotation rieng (tuong duong, cu hon)
@AutoConfiguration
@AutoConfigureAfter(HibernateJpaAutoConfiguration.class)

@AutoConfiguration
@AutoConfigureBefore(WebMvcAutoConfiguration.class)

// Cach 3: fine-grained so nguyen
@AutoConfiguration
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)

@AutoConfigureOrder dùng khi cần ép một autoconfig chạy rất sớm hoặc rất muộn so với toàn bộ tập. @AutoConfigureBefore/After ưu tiên hơn vì express ý định rõ hơn.

4.4 Cơ chế bên dưới — tại sao ordering không phải ép chạy sequential

@AutoConfigureBefore/After không ép Spring chạy autoconfig theo đúng thứ tự đó. Thực ra:

  1. AutoConfigurationSorter thu thập tất cả @AutoConfigureBefore/After relationship từ metadata.
  2. Build DAG (đồ thị có hướng không chu trình) từ các relationship.
  3. Topological sort → danh sách thứ tự an toàn.
  4. DeferredImportSelector import autoconfig theo thứ tự đó.

Hệ quả: nếu bạn khai @AutoConfigureAfter(NonExistentAutoConfig.class) → dependency đó ignored (không crash). Boot chỉ tôn trọng dependency tồn tại. Circular dependency (A after B, B after A) → Boot throws IllegalStateException tại startup.

5. Debug bằng Actuator /conditions

Khi autoconfig không ra kết quả như mong đợi — đừng đoán. Bật endpoint:

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,conditions

GET /actuator/conditions trả về:

{
  "contexts": {
    "application": {
      "positiveMatches": {
        "CacheAutoConfiguration#cacheManager": [
          {
            "condition": "OnMissingBeanCondition",
            "message": "No qualifying bean of type 'CacheManager' found"
          }
        ]
      },
      "negativeMatches": {
        "RedisAutoConfiguration": {
          "notMatched": [
            {
              "condition": "OnClassCondition",
              "message": "did not find required class 'org.springframework.data.redis.core.RedisTemplate'"
            }
          ]
        }
      }
    }
  }
}

Đọc output:

  • positiveMatches: autoconfig đã register + lý do condition pass.
  • negativeMatches: autoconfig skip + lý do fail. Đây là chỗ debug "tại sao bean không tạo".
  • exclusions: autoconfig bị user loại bỏ qua @SpringBootApplication(exclude = ...).

Ví dụ debug thực tế:

Bug: Redis cache không work dù đã thêm starter.

1. GET /actuator/conditions
2. Search "Redis" trong negativeMatches
3. Thay vì: "RedisAutoConfiguration" ở positiveMatches
   Thay vào đó: "RedisAutoConfiguration" ở negativeMatches
   Reason: "did not find required class RedisTemplate"
4. Kiểm tra pom.xml -- quên add spring-boot-starter-data-redis
5. Fix: them dependency, restart, check positiveMatches

Thay thế cho production: chạy java -jar app.jar --debug in autoconfig report ra console lúc startup — tiện cho dev local.

Cơ chế bên dưới — ConditionEvaluationReport

Toàn bộ thông tin /actuator/conditions đến từ ConditionEvaluationReport — một object Spring build trong quá trình ConfigurationClassPostProcessor evaluate condition. Mỗi lần một condition được đánh giá, kết quả được ghi vào report này với lý do chi tiết (ConditionMessage).

Đây cũng là lý do --debug flag in được report đầy đủ: Boot đăng ký ConditionEvaluationReportLoggingListener lắng nghe event ContextRefreshedEvent, đọc report từ context, in ra log ở level DEBUG. Hai đường ra — endpoint Actuator và console log — đọc chung một object trong context, nên chúng không bao giờ lệch nhau.

Hiểu cơ chế này giúp debug sâu hơn: nếu condition evaluate sai kết quả, bạn có thể breakpoint trong OnClassCondition.getMatchOutcome() hoặc OnMissingBeanCondition.getMatchOutcome() để xem chính xác Spring nhìn thấy gì.

Pitfall

Pitfall 1 — @ConditionalOnMissingBean thiếu trên default bean:

// SAI:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.build();    // luon register, conflict voi app config
}

// DUNG:
@Bean
@ConditionalOnMissingBean(SecurityFilterChain.class)
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http.build();
}

Pitfall 2 — @AutoConfigureAfter trên một class không phải @AutoConfiguration:

@AutoConfigureAfter/Before chỉ có nghĩa khi đặt trên @AutoConfiguration class. Đặt trên @Configuration thường → Spring bỏ qua hoàn toàn, không có ordering effect.

Pitfall 3 — Ordering giữa @ConditionalOnBean và bean tạo bởi user config:

@AutoConfiguration
@ConditionalOnBean(DataSource.class)
public class MyAutoConfig { ... }

Nếu DataSource được tạo trong @Configuration của user — liệu condition có pass không? Có, vì DeferredImportSelector (được dùng bởi autoconfig) evaluate sau tất cả @Configuration thông thường. Đây là lý do autoconfig dùng DeferredImportSelector thay ImportSelector thường: đảm bảo user bean đã có trước khi condition đánh giá.

Pitfall 4 — Đoán thay debug:

SAI: "Thử exclude DataSourceAutoConfiguration xem sao..."
DUNG: GET /actuator/conditions -> tim lý do -> fix root cause

Liên hệ các bài khác

  • @Conditional bean: bài nền — @ConditionalOn* của Boot xây trên @Conditional core Spring. Hiểu Condition interface giúp đọc source của mọi @ConditionalOn* annotation.
  • 03 — Auto-configuration deep dive: bài nguồn — pipeline đầy đủ từ AutoConfigurationImportSelector đến bean register; bài này đào sâu một bước trong pipeline đó (condition + ordering).
  • @EnableAutoConfiguration: entry point kích hoạt cả hệ thống — @AutoConfiguration(after = ...) chỉ có nghĩa khi được AutoConfigurationImportSelector load.

Tóm tắt

  • @ConditionalOn* đặt điều kiện cho bean/autoconfig — 12 annotation, AND logic khi combine.
  • @ConditionalOnMissingBean là cơ chế back-off: autoconfig khai bean mặc định nhưng nhường khi user đã có bean cùng kiểu — "opinionated but not lock-in".
  • Mọi @Bean default trong autoconfig bắt buộc@ConditionalOnMissingBean — thiếu → conflict khi user override.
  • @AutoConfigureBefore/After xác định thứ tự evaluate. @ConditionalOnMissingBean phụ thuộc thứ tự — autoconfig đến trước win.
  • Cơ chế ordering: AutoConfigurationSorter build DAG từ relationship rồi topological sort — không phải ép sequential run.
  • DeferredImportSelector đảm bảo autoconfig evaluate sau mọi user @Configuration@ConditionalOnBean với bean user khai sẽ thấy đúng.
  • Debug: /actuator/conditions (production) hoặc --debug (dev). Đừng đoán.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao autoconfig dùng @ConditionalOnClass(name = "com.example.Foo") thay vì @ConditionalOnClass(Foo.class)? Điều gì xảy ra nếu dùng class literal khi class không có classpath?
  2. Q2
    Mô tả back-off pattern bằng ví dụ cụ thể. Tại sao đây là cơ chế "override được" mà không cần exclude autoconfig toàn bộ?
  3. Q3
    Có 2 autoconfig AB. B khai bean Foo với @ConditionalOnMissingBean. A khai bean Foo không có điều kiện. Không có ordering. Điều gì xảy ra? Fix ra sao?
  4. Q4
    Tại sao @ConditionalOnBean(DataSource.class) trong autoconfig thấy được DataSource do user khai trong @Configuration? Cơ chế kỹ thuật nào đảm bảo điều này?
  5. Q5
    Bạn thêm spring-boot-starter-data-redis nhưng bean RedisTemplate không tạo. Quy trình debug chính xác bằng /actuator/conditions?

Bài tiếp theo: Mini-challenge — trace 1 request

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

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

Ôn phỏng vấn

Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

Mini-challenge: trace 1 request từ main() và log mọi bean tạo