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:

flowchart TB
    Q{"CacheManager bean<br/>da co trong context?"}
    Skip["Boot skip<br/>dung bean cua user"]
    Register["Boot tao ConcurrentMapCacheManager<br/>lam default"]

    Q -->|"Co roi"| Skip
    Q -->|"Chua co"| Register

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ộ.

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.

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:

flowchart LR
    DS["DataSourceAutoConfiguration<br/>tao DataSource"]
    HJpa["HibernateJpaAutoConfiguration<br/>after = DS<br/>tao EntityManagerFactory"]
    JpaRepo["JpaRepositoriesAutoConfiguration<br/>after = HJpa<br/>tao Repository proxy"]

    DS --> HJpa --> JpaRepo

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.

flowchart TB
    CPP["ConfigurationClassPostProcessor<br/>evaluate @Conditional*"]
    CER["ConditionEvaluationReport<br/>ghi result + reason"]
    AE["/actuator/conditions<br/>doc report, serialize JSON"]
    Log["--debug flag<br/>ConditionEvaluationReportLoggingListener<br/>in console"]

    CPP -->|"ghi"| CER
    CER --> AE
    CER --> Log

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
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?

Autoconfig phải compile được kể cả khi class đích không có classpath của dự án autoconfig. Nếu dùng @ConditionalOnClass(Foo.class), trình biên dịch cần import được Foo — nếu Foo không có dependency của module autoconfig thì compile fail, ngay cả khi ý định là "chỉ load khi Foo có".

Dùng name = "com.example.Foo" là string thuần — compile không phân giải gì. Runtime, Spring Boot dùng ClassLoader.getResource("com/example/Foo.class") để check sự tồn tại mà không cần load class. Nếu không tìm thấy → condition false → autoconfig skip.

Thực tế: mọi autoconfig core của Boot dùng name attribute cho @ConditionalOnClass khi class đến từ thư viện ngoài. Chỉ dùng class literal khi class đó chắc chắn có trong classpath vô điều kiện (vd class của chính module autoconfig).

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ộ?

Back-off pattern: autoconfig khai một bean mặc định có @ConditionalOnMissingBean. Khi user khai bean cùng kiểu trong @Configuration của mình, condition OnMissingBean fail → autoconfig skip bean đó, dùng bean của user.

Ví dụ: CacheAutoConfiguration khai ConcurrentMapCacheManager với @ConditionalOnMissingBean. User khai RedisCacheManager trong AppConfig. Boot thấy CacheManager đã có → skip default. User không cần viết @SpringBootApplication(exclude = CacheAutoConfiguration.class).

Sức mạnh: override một bean cụ thể mà không mất phần còn lại của autoconfig. Nếu exclude toàn bộ CacheAutoConfiguration, bạn mất luôn binding @EnableConfigurationProperties, event listener, và các bean phụ trong autoconfig đó. Back-off chính xác hơn: chỉ nhường đúng bean user muốn override.

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?

Thứ tự evaluate không xác định — phụ thuộc vào classpath scan order. Hai tình huống:

Nếu B evaluate trước A: B thấy chưa có Foo → register Foo từ B. A evaluate sau, không có @ConditionalOnMissingBean → cố register thêm FooBeanDefinitionOverrideException (Boot 2.1+ mặc định cấm override).

Nếu A evaluate trước B: A register Foo. B thấy Foo đã có → skip. Ổn.

Fix: hai hướng:

1. Thêm @AutoConfiguration(after = A.class) trên B — đảm bảo A luôn chạy trước, B luôn skip.

2. Thêm @ConditionalOnMissingBean vào Foo bean của A — cả hai nhường nhau, chỉ bean nào đến trước win.

Quy tắc: nếu hai autoconfig có thể produce cùng bean type, ít nhất một phải có ordering rõ ràng, và bean default phải có @ConditionalOnMissingBean.

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?

Autoconfig được load qua AutoConfigurationImportSelector, implement DeferredImportSelector — interface Spring dùng để delay import đến sau khi mọi @Configuration thông thường đã được process.

Thứ tự trong ConfigurationClassPostProcessor:

1. Parse và process tất cả @Configuration user (khai trong app).

2. Sau khi user config xong, mới gọi DeferredImportSelector.selectImports() để load autoconfig.

3. Autoconfig evaluate condition — lúc này bean definition của user đã có trong beanDefinitionMap.

Nếu autoconfig dùng ImportSelector thường (không Deferred) — evaluate cùng lúc user config — có thể thấy chưa đủ bean. DeferredImportSelector là lý do autoconfig luôn thua user config về thứ tự — thiết kế có chủ đích.

Hệ quả thực tế: bạn luôn có thể override autoconfig bằng cách khai bean trong @Configuration thông thường — không cần @Order, không cần @Primary. Autoconfig tự nhận ra và nhường.

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ước 1 — Bật endpoint: thêm management.endpoints.web.exposure.include: health,conditions vào application.yml. Restart app.

Bước 2 — Fetch và đọc: GET /actuator/conditions. Tìm RedisAutoConfiguration.

Bước 3 — Đọc negativeMatches: nếu RedisAutoConfiguration nằm trong negativeMatches, đọc trường notMatched[].message. Ba lý do phổ biến:

- did not find required class RedisTemplate → starter chưa kéo đúng jar (kiểm tra mvn dependency:tree).

- spring.data.redis.host not set → thiếu property host.

- RedisTemplate bean already defined → đang ở positiveMatches nhưng bean của bạn là custom — check positiveMatches lần nữa.

Bước 4 — Nếu trong positiveMatches nhưng vẫn không work: GET /actuator/beans search redisTemplate — verify bean tồn tại. Nếu có, vấn đề không phải autoconfig mà là connection (Redis chưa chạy, host sai).

Quy tắc: /actuator/conditions chỉ answer câu hỏi "bean có được register không và tại sao". Câu hỏi runtime (connection, data) cần log và /actuator/health.

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

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

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