Spring Core & Boot/@EnableAutoConfiguration — cơ chế đọc AutoConfiguration.imports và đăng ký BeanDefinition
30/41
Bài 30 / 41~12 phútSpring Boot & Auto-configurationMiễn phí lượt xem

@EnableAutoConfiguration — cơ chế đọc AutoConfiguration.imports và đăng ký BeanDefinition

Bài này bóc một mục tiêu duy nhất: @EnableAutoConfiguration hoạt động ra sao bên dưới — từ annotation đến AutoConfigurationImportSelector, đến đọc file AutoConfiguration.imports từ classpath* mọi jar, đến register BeanDefinition qua BFPP pipeline. Giải thích tại sao Spring Boot dùng file text imports thay @Import thủ công, và tại sao ASM bytecode reader để filter không load class thừa.

TL;DR: @EnableAutoConfiguration không tự làm gì — nó chỉ là entry point @Import(AutoConfigurationImportSelector.class). Selector đó implement DeferredImportSelector, được Spring gọi sau khi mọi @Configuration thông thường đã process. Tại đó, selector đọc tất cả file META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports từ mọi jar trên classpath (classpath*: scan) — mỗi dòng là một fully-qualified class name. Danh sách đó được nạp vào ConfigurationClassPostProcessor như BeanDefinition — chính là BeanFactoryPostProcessor đã giải thích ở bài 03 — BeanDefinition & BFPP. Để filter sớm mà không load class, Spring Boot dùng ASM bytecode reader — đọc annotation trực tiếp từ bytecode, không qua Class.forName(). Hai quyết định thiết kế này (file imports + ASM) có lý do rõ ràng và giải quyết vấn đề cụ thể.

Sau khi starter kéo dependency vào classpath (Starter & BOM), câu hỏi tiếp theo: Spring Boot tự tạo bean từ những dependency đó bằng cách nào? Bài này bóc cơ chế cốt lõi — đúng một mảnh hẹp: @EnableAutoConfigurationAutoConfigurationImportSelector → file AutoConfiguration.imports → register BeanDefinition. Phần @ConditionalOn* và ordering sẽ là bài 05 — @ConditionalOn* family & ordering.

1. Vấn đề mà @EnableAutoConfiguration giải quyết

Trước Spring Boot, mỗi project phải tự viết — hoặc copy — đoạn config này:

// Spring 4 — phai viet trong moi project
@Configuration
@Import({
    DataSourceConfiguration.class,
    JpaConfiguration.class,
    TransactionManagerConfiguration.class,
    HibernateConfiguration.class,
    WebMvcConfiguration.class
})
public class AppConfig { }

Vấn đề: 5 class đó phải được import đúng — đúng thứ tự, đúng điều kiện, đúng phiên bản. Copy-paste nhầm một dòng là app lỗi. Mỗi team viết lại từ đầu.

@EnableAutoConfiguration giải quyết bằng cách đẩy trách nhiệm khám phá config ra khỏi code app: thay vì app khai "tôi cần những class này", Boot nói "tôi tự tìm những class nào phù hợp với classpath của bạn". Đây là inversion of control ở cấp cấu hình, không phải cấp bean.

Tại sao cần DeferredImportSelector?

Spring xử lý @Import theo 2 loại: ImportSelector thông thường chạy cùng lúc với @Configuration của app. DeferredImportSelector chạy sau tất cả — đảm bảo autoconfig "đến sau" và nhường chỗ cho config của user. Nếu dùng ImportSelector thông thường, autoconfig có thể register bean trước user config, phá vỡ cơ chế "user override" qua @ConditionalOnMissingBean.

2. @EnableAutoConfiguration — entry point và cấu trúc

@SpringBootApplication meta-annotate @EnableAutoConfiguration:

// File: org/springframework/boot/autoconfigure/EnableAutoConfiguration.java (rut gon)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
    Class<?>[] exclude() default {};
    String[] excludeName() default {};
}

Hai phần quan trọng:

  • @Import(AutoConfigurationImportSelector.class) — entry point thực sự. AutoConfigurationImportSelector là class thực hiện toàn bộ công việc khám phá autoconfig.
  • @AutoConfigurationPackage — đánh dấu package chứa class app làm root cho @EntityScan@ComponentScan. Đây là lý do @SpringBootApplication phải đặt ở root package: nếu đặt ở sub-package, các class ngoài sub-package đó sẽ không được scan.

Lưu ý: excludeexcludeName cho phép user loại trừ autoconfig cụ thể — vd khi muốn tự config DataSource thay vì để Boot tự động:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class App { }

3. AutoConfigurationImportSelector — luồng selectImports

AutoConfigurationImportSelector implement DeferredImportSelector. Spring gọi method selectImports() khi xử lý @Configuration class — sau khi mọi @Configuration app đã parse xong:

// File: AutoConfigurationImportSelector.java (rut gon va giai thich)
public class AutoConfigurationImportSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        // Buoc 1: doc file AutoConfiguration.imports tu moi jar classpath
        AutoConfigurationEntry entry = getAutoConfigurationEntry(annotationMetadata);
        return StringUtils.toStringArray(entry.getConfigurations());
    }

    protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata metadata) {
        // 1. Doc danh sach tu file imports — classpath* scan tat ca jar
        List<String> configurations = getCandidateConfigurations(metadata, attributes);

        // 2. Bo class user exclude (qua annotation exclude/excludeName)
        configurations = removeExclusions(configurations, exclusions);

        // 3. Filter qua @Conditional* — chi giu class pass dieu kien
        configurations = getConfigurationClassFilter().filter(configurations);

        return new AutoConfigurationEntry(configurations, exclusions);
    }

    protected List<String> getCandidateConfigurations(...) {
        // Dung ImportCandidates de doc file imports tu classpath*
        return ImportCandidates
            .load(AutoConfiguration.class, getBeanClassLoader())
            .getCandidates();
    }
}

Ba bước: load → exclude → filter. Kết quả là danh sách fully-qualified class name của autoconfig được pass vào Spring để register.

flowchart TB
  EAC["@EnableAutoConfiguration<br/>@Import(AutoConfigurationImportSelector)"]
  ACIS["AutoConfigurationImportSelector<br/>selectImports()"]
  Load["getCandidateConfigurations()<br/>doc file AutoConfiguration.imports<br/>tu moi jar classpath*"]
  Exclude["removeExclusions()<br/>bo class user exclude"]
  Filter["getConfigurationClassFilter().filter()<br/>ASM bytecode reader + @ConditionalOn*"]
  BDReg["ConfigurationClassPostProcessor<br/>register tung class nhu BeanDefinition"]

  EAC --> ACIS
  ACIS --> Load
  Load --> Exclude
  Exclude --> Filter
  Filter --> BDReg

4. Cơ chế bên dưới — file AutoConfiguration.imports và classpath* scan

4.1 File imports là file text đơn giản

Mỗi jar autoconfig chứa một file text tại đường dẫn cố định:

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

Nội dung là danh sách fully-qualified class name — mỗi dòng một class:

org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration
... (143 dong trong Boot 3.4)

Không có logic, không có condition — chỉ là danh sách. Logic condition nằm trong annotation trên từng class autoconfig.

4.2 Tại sao dùng file text thay @Import thủ công?

Câu hỏi hợp lý: tại sao không dùng @Import(DataSourceAutoConfiguration.class, KafkaAutoConfiguration.class, ...) trực tiếp trong @EnableAutoConfiguration?

Ba lý do:

Lý do 1 — Extensibility. Nếu dùng @Import trực tiếp, danh sách 143 class phải nằm cứng trong annotation của Spring Boot core. Khi viết starter của riêng bạn (acme-tracing-starter), bạn không thể thêm autoconfig vào @EnableAutoConfiguration — bạn không sở hữu annotation đó. Với file imports, mỗi jar tự đóng góp file của mình — Boot scan tất cả và merge. Không cần sửa core Boot.

Lý do 2 — Lazy discovery qua classpath.* ImportCandidates.load() dùng ClassLoader.getResources() với prefix classpath*: — nghĩa là tìm tất cả file có cùng tên trong toàn bộ classpath, kể cả trong jar. Đây là cơ chế classpath* scan đã giải thích ở bài Resource & @SpringBootApplication. Bạn chỉ cần đặt file đúng vị trí trong jar, Boot tìm thấy tự động.

Lý do 3 — Compile independence. @Import annotation yêu cầu class được import phải có trên classpath lúc compile annotation đó. Nếu @EnableAutoConfiguration import 143 class trực tiếp, toàn bộ 143 class đó phải compile được cùng lúc — không thể tách module. File text chỉ là string, không compile-depend vào class.

flowchart LR
  subgraph JAR1["spring-boot-autoconfigure.jar"]
    F1["META-INF/spring/<br/>AutoConfiguration.imports<br/>(143 class)"]
  end
  subgraph JAR2["acme-tracing-starter.jar"]
    F2["META-INF/spring/<br/>AutoConfiguration.imports<br/>(1 class)"]
  end
  subgraph JAR3["spring-cloud-config.jar"]
    F3["META-INF/spring/<br/>AutoConfiguration.imports<br/>(N class)"]
  end
  Merge["ImportCandidates.load()<br/>classpath* merge tat ca"]
  JAR1 --> Merge
  JAR2 --> Merge
  JAR3 --> Merge

4.3 Từ danh sách class đến BeanDefinition

Kết quả selectImports() là mảng string class name. Spring xử lý mảng này trong ConfigurationClassPostProcessor — đây là BeanFactoryPostProcessor (BFPP) quan trọng nhất của Spring, đã được giải thích kỹ ở bài 03 — BeanDefinition & BFPP.

Mỗi class trong danh sách được parse như @Configuration class — Spring đọc annotation @Bean, @Import, @ConditionalOn* trên đó, rồi register BeanDefinition cho từng @Bean method vào DefaultListableBeanFactory. Tại thời điểm này, chỉ có metadata (BeanDefinition) — object thật chưa tồn tại. Object được tạo sau, ở pha instantiate bean.

// Minh hoa: BFPP register BeanDefinition cho autoconfig class
// (day la pseudo-code mo ta logic trong ConfigurationClassPostProcessor)
for (String autoConfigClass : selectedImports) {
    ConfigurationClass configClass = parse(autoConfigClass);
    for (BeanMethod beanMethod : configClass.getBeanMethods()) {
        // Chi register neu @Conditional* pass
        if (conditionEvaluator.shouldSkip(beanMethod)) continue;
        BeanDefinition bd = createBeanDefinition(beanMethod);
        registry.registerBeanDefinition(bd.getBeanName(), bd);
    }
}

5. Cơ chế bên dưới — ASM bytecode reader và tại sao không Class.forName()

Bước filter trong getConfigurationClassFilter().filter(configurations) cần đọc annotation @ConditionalOnClass của từng autoconfig class để biết có bỏ qua không. Cách naive nhất là Class.forName(className) — load class rồi đọc annotation. Boot không làm vậy. Thay vào đó, Boot dùng ASM bytecode reader.

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

Khi JVM load một class qua Class.forName(), ba điều xảy ra:

  1. Bytecode được load vào metaspace.
  2. Static field được khởi tạo.
  3. Static initializer block (static { ... }) chạy.

Với 143 autoconfig class, nếu load tất cả qua Class.forName():

  • 143 class + dependency transitive của chúng vào metaspace — tiêu tốn vài chục MB bộ nhớ cho class không bao giờ cần.
  • Static initializer của những class như KafkaAutoConfiguration có thể trigger connection check, log init, hoặc side effect không mong muốn.
  • Nếu KafkaAutoConfiguration import KafkaProducer trong annotation @ConditionalOnClass(KafkaProducer.class)KafkaProducer không có classpath → ClassNotFoundException — autoconfig fail thay vì graceful skip.

5.2 Cách ASM hoạt động

ASM là thư viện đọc bytecode Java .class file. Spring Boot dùng ASM trong AnnotationMetadataReadingVisitor để đọc annotation mà không load class:

Autoconfig .class file (bytecode)
  → ASM visitor parse annotation table
  → extract @ConditionalOnClass value = "org.apache.kafka.clients.producer.KafkaProducer"
  → check ClassLoader.getResource("org/apache/kafka/.../KafkaProducer.class") trả null
  → class khong co classpath → skip autoconfig nay
  (KafkaAutoConfiguration chua bao gio duoc Class.forName())

Lưu ý: ClassLoader.getResource() chỉ kiểm tra xem file .class có tồn tại trong classpath không — không load, không init. Đây là cách Spring check class presence mà không trigger side effect.

flowchart TB
  BC["KafkaAutoConfiguration.class (bytecode)"]
  ASM["ASM AnnotationVisitor<br/>doc annotation table"]
  Extract["extract @ConditionalOnClass<br/>= 'org.apache.kafka.clients.producer.KafkaProducer'"]
  Check["ClassLoader.getResource(<br/>'org/apache/.../KafkaProducer.class')"]
  Skip["null -> skip<br/>KafkaAutoConfiguration khong load"]
  Load["not null -> load class<br/>qua Class.forName()"]

  BC --> ASM
  ASM --> Extract
  Extract --> Check
  Check -->|"null"| Skip
  Check -->|"not null"| Load

5.3 Thêm một lớp tối ưu: spring-autoconfigure-metadata.properties

Boot không cần đọc bytecode của 143 class mỗi lần startup. Trong quá trình build, annotation processor tạo file:

META-INF/spring-autoconfigure-metadata.properties

File này chứa condition đã pre-compute từ bytecode:

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

org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration.ConditionalOnClass=\
  org.springframework.data.jpa.repository.JpaRepository

Khi startup, Boot đọc file properties này trước — nếu class trong ConditionalOnClass không có classpath, skip autoconfig mà không cần đọc bytecode của nó. ASM chỉ được dùng khi metadata không có hoặc cần check condition phức tạp hơn.

Kết quả: 143 candidate → metadata pre-filter loại ~80 → ASM filter loại thêm ~30 → ~30-50 autoconfig thực sự được Class.forName() load và register BeanDefinition.

6. Pitfall

Nhầm 1 — Tưởng @EnableAutoConfiguration tự register bean:

// NGUOI HOC NGHI: annotation nay truc tiep tao DataSource, EntityManager...
@EnableAutoConfiguration
public class App { }

Thực tế: @EnableAutoConfiguration chỉ import AutoConfigurationImportSelector. Selector đọc file, trả về danh sách class name. Spring mới parse các class đó và register BeanDefinition. Bean object thật chỉ xuất hiện ở pha instantiate sau đó. Annotation = entry point vào pipeline, không phải nơi bean ra đời.

Nhầm 2 — Quên file AutoConfiguration.imports khi viết starter:

// SAI: chi co class Java, khong co file
src/main/java/com/acme/TracingAutoConfiguration.java

// DUNG: phai co ca file
src/main/java/com/acme/TracingAutoConfiguration.java
src/main/resources/META-INF/spring/
  org.springframework.boot.autoconfigure.AutoConfiguration.imports

Nếu thiếu file imports, ImportCandidates.load() không tìm thấy class của bạn — autoconfig không bao giờ chạy dù class Java đúng hoàn toàn. Đây là bug im lặng nhất của custom starter.

✅ Verify: sau khi build jar, chạy jar tf acme-starter.jar | grep AutoConfiguration.imports — phải thấy file trong output.

Nhầm 3 — Hiểu sai scope của @AutoConfigurationPackage:

// SAI: dat @SpringBootApplication o sub-package
package com.olhub.web;    // sub-package
@SpringBootApplication
public class WebApp { }

// Class nay se khong duoc scan:
package com.olhub.service;
@Service
public class OrderService { }

@AutoConfigurationPackage đăng ký com.olhub.web làm root — com.olhub.service nằm ngoài root, không được scan. @SpringBootApplication phải đặt ở package bao chứa toàn bộ code (com.olhub).

📚 Deep Dive

Source code đọc để bỏ chữ 'magic'

Chỉ cần đọc tên method và xem flow call, không cần hiểu hết implementation:

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

  • Bài 03 — BeanDefinition & BeanFactoryPostProcessor: ConfigurationClassPostProcessor là BFPP xử lý output của AutoConfigurationImportSelector. Hiểu BFPP là hiểu vì sao autoconfig register như BeanDefinition chứ không phải object — và vì sao condition được evaluate tại pha metadata, không phải pha instantiate.
  • Bài 05 — Resource & @SpringBootApplication: classpath*: scan — cơ chế ClassLoader.getResources() tìm file trong nhiều jar — là nền tảng của ImportCandidates.load(). Bài đó giải thích tại sao classpath*: (có dấu *) scan được cả trong jar, còn classpath: (không *) chỉ thấy file đầu tiên.
  • Bài 05-autoconfig — @ConditionalOn* family & ordering: phần filter trong pipeline này — evaluate @ConditionalOnClass, @ConditionalOnMissingBean, và ordering @AutoConfigureBefore/After. Bài hiện tại dừng ở "selector trả về danh sách"; bài sau giải thích danh sách đó được filter ra sao.
  • @ConditionalOn* family & ordering: bước tiếp theo — sau khi autoconfig class được register, @ConditionalOn* quyết định bean nào thực sự được tạo (back-off pattern).

Tóm tắt

  • @EnableAutoConfiguration chỉ là entry point: @Import(AutoConfigurationImportSelector.class). Mọi công việc thực tế nằm trong selector.
  • AutoConfigurationImportSelector implement DeferredImportSelector — chạy sau tất cả @Configuration app để đảm bảo user config được ưu tiên.
  • Selector đọc file META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports từ mọi jar trên classpath qua classpath*: scan — mỗi jar đóng góp danh sách autoconfig của riêng mình.
  • File text thay @Import trực tiếp vì: extensibility (jar thứ ba có thể đóng góp), lazy discovery (classpath*), compile independence (chỉ là string).
  • Kết quả được đẩy vào ConfigurationClassPostProcessor (BFPP) — parse annotation, register BeanDefinition cho từng @Bean method pass điều kiện.
  • Boot dùng ASM bytecode reader thay Class.forName() để đọc @ConditionalOnClass: không load class, không trigger static init, không side effect, tiết kiệm metaspace.
  • spring-autoconfigure-metadata.properties là lớp tối ưu thứ hai: condition pre-computed tại build time — filter trước khi cần đọc bytecode runtime.

Tự kiểm tra

Tự kiểm tra
Q1
@EnableAutoConfiguration tự register bean DataSource không? Giải thích đúng pipeline từ annotation đến bean object.

Không — @EnableAutoConfiguration không trực tiếp tạo bean nào. Đây là pipeline đầy đủ:

  1. @EnableAutoConfiguration meta-annotate @Import(AutoConfigurationImportSelector.class) — khai báo một DeferredImportSelector sẽ được Spring gọi sau.
  2. Khi ConfigurationClassPostProcessor (BFPP) parse @SpringBootApplication, nó gọi selectImports() trên selector — trả về danh sách class name.
  3. BFPP parse từng class trong danh sách: đọc @Bean method, evaluate @Conditional*, register BeanDefinition vào DefaultListableBeanFactory.
  4. Đây vẫn là metadata — object DataSource thật chưa tồn tại.
  5. Sau khi BFPP phase xong, container mới instantiate bean — gọi constructor/factory method, inject dependency. Lúc này DataSource object ra đời.

Nói gọn: annotation = entry point → selector = file reader → BFPP = metadata register → instantiate = object ra đời. Không bước nào là "magic" tự phát.

Q2
Tại sao Spring Boot dùng file AutoConfiguration.imports (file text) thay vì đặt toàn bộ 143 class vào @Import(...) trực tiếp trong annotation? Cho 3 lý do cụ thể.

3 lý do thiết kế:

  1. Extensibility — jar thứ ba đóng góp được. Nếu danh sách nằm trong @Import của Spring Boot core, chỉ Spring Boot team mới sửa được. Với file text, mỗi jar (spring-cloud, spring-ai, custom starter của bạn) tự đặt file trong META-INF/spring/ và Boot merge tất cả. Không cần fork core.
  2. Classpath* discovery — tự động tìm. ImportCandidates.load() dùng ClassLoader.getResources() với prefix classpath*: — tìm tất cả file cùng tên trong toàn bộ classpath, kể cả trong jar. Chỉ cần đặt file đúng vị trí, Boot tìm thấy tự động khi app start.
  3. Compile independence. @Import(SomeClass.class) yêu cầu SomeClass phải compile được tại thời điểm viết annotation. Nếu @EnableAutoConfiguration import 143 class trực tiếp, toàn bộ 143 class phải có trên classpath khi compile annotation. File text chỉ là string — không có compile dependency, cho phép tách module hoàn toàn.

Ba lý do này cùng phục vụ mục tiêu "mở rộng mà không sửa core" — triết lý Open/Closed Principle ở cấp ecosystem.

Q3
Tại sao Spring Boot dùng ASM bytecode reader thay Class.forName() để kiểm tra @ConditionalOnClass? Liệt kê 3 vấn đề cụ thể nếu dùng Class.forName().

3 vấn đề nếu dùng Class.forName():

  1. Side effect từ static initializer. Load class trigger static block và static field init. Với class như KafkaAutoConfiguration, static init có thể cố connect đến broker, tạo thread pool, log cấu hình. Những side effect này không mong muốn khi chỉ cần check "class có tồn tại không".
  2. ClassNotFoundException khi class không có. Nếu KafkaProducer không có classpath và boot gọi Class.forName("...KafkaAutoConfiguration") — class đó có thể import KafkaProducer trong annotation value (@ConditionalOnClass(KafkaProducer.class)) → JVM throw ClassNotFoundException tại load time. Autoconfig fail thay vì graceful skip.
  3. Memory và performance. Load 143 autoconfig class + dependency transitive của chúng tiêu tốn metaspace và thời gian. ASM đọc bytecode trực tiếp — không vào metaspace, không trigger class loading chain. Kết hợp với spring-autoconfigure-metadata.properties (pre-computed condition), phần lớn autoconfig không cần đọc bytecode runtime.

ASM thay thế bằng cách đọc annotation table trong bytecode, extract string class name, rồi dùng ClassLoader.getResource("org/apache/.../KafkaProducer.class") — chỉ check file tồn tại, không load.

Q4
Bạn viết custom starter acme-tracing-starter, có class TracingAutoConfiguration. App thêm dependency vào starter nhưng bean Tracer không tạo. Debug step-by-step.

Quy trình debug theo pipeline:

  1. Kiểm tra file imports trong jar. Chạy jar tf acme-tracing-starter.jar | grep AutoConfiguration.imports. Nếu không thấy file → đây là lỗi — file phải ở META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports trong jar. Thêm file vào src/main/resources/META-INF/spring/.
  2. Kiểm tra nội dung file. jar xf acme-tracing-starter.jar META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports rồi đọc — phải có dòng com.acme.TracingAutoConfiguration. Class name sai (typo, wrong package) → selector không tìm được.
  3. Bật /actuator/conditions. Search TracingAutoConfiguration trong response. Nếu trong negativeMatches: đọc reason — vd OnClassCondition: Tracer class not found → thiếu dependency. Nếu không có trong response: file imports chưa đúng (bước 1-2).
  4. Kiểm tra @ConditionalOn* trong class. Nếu @ConditionalOnClass(Tracer.class) dùng value thay vì name — starter phải pull jar chứa Tracer. Nếu không pull → compile fail hoặc condition fail. Fix: dùng @ConditionalOnClass(name = "com.acme.Tracer").
  5. Kiểm tra @ConditionalOnMissingBean. Nếu app đã có bean Tracer từ nguồn khác và autoconfig có @ConditionalOnMissingBean → Boot skip, đó là hành vi đúng. Nếu không có @ConditionalOnMissingBean và có conflict → BeanDefinitionOverrideException.

Quy tắc debug: luôn bắt đầu từ file imports → /actuator/conditions → annotation condition. Không đoán.

Q5
DeferredImportSelector khác ImportSelector như thế nào? Tại sao AutoConfigurationImportSelector phải implement DeferredImportSelector chứ không phải ImportSelector?

Sự khác nhau:

  • ImportSelector thông thường: selectImports() được gọi ngay khi ConfigurationClassPostProcessor parse class chứa @Import — tức là cùng lúc với các @Configuration khác của app.
  • DeferredImportSelector: selectImports() bị defer — bị đẩy vào một nhóm riêng, chỉ chạy sau khi tất cả @Configuration app đã được parse xong trong cùng một BFPP cycle.

Tại sao autoconfig cần Deferred:

Autoconfig muốn "nhường chỗ" cho config của user. Cơ chế @ConditionalOnMissingBean chỉ hoạt động đúng nếu autoconfig được evaluate sau khi user config đã register bean của họ. Nếu dùng ImportSelector thông thường, thứ tự evaluate giữa autoconfig và user @Configuration không xác định — có thể autoconfig evaluate trước khi user register bean, khiến @ConditionalOnMissingBean luôn pass và tạo bean default dù user đã khai.

DeferredImportSelector đảm bảo autoconfig luôn đến sau — đây là nền tảng của hợp đồng "opinionated default, nhưng user có thể override bất kỳ lúc nào".

Thực tế: nếu bạn viết custom ImportSelector muốn hoạt động như autoconfig (nhường chỗ cho app config), luôn implement DeferredImportSelector.

Bài tiếp theo: @ConditionalOn* family & ordering

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