Spring Core & Boot/Spring Boot Auto-Configuration hoạt động thế nào?
30/41
Bài 30 / 41~12 phútCó videoSpring Boot & Auto-configurationMiễn phí lượt xem

Spring Boot Auto-Configuration hoạt động thế nào?

Mổ xẻ @EnableAutoConfiguration và AutoConfiguration.imports: Boot quét classpath, áp @Conditional, đăng ký bean — và vì sao config của bạn luôn thắng autoconfig.

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.

Pipeline dọc từ annotation tới metadata: @SpringBootApplication kích hoạt @EnableAutoConfiguration vốn chỉ là @Import của AutoConfigurationImportSelector; selector là DeferredImportSelector nên chạy sau khi mọi @Configuration của user parse xong; getCandidateConfigurations đọc mọi file AutoConfiguration.imports qua classpath sao ra 143 tên class; removeExclusions và filter thu còn khoảng 30 tới 50 tên; cuối cùng ConfigurationClassPostProcessor đăng ký BeanDefinition mà tới đó vẫn chưa có object nào

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.

Đối chiếu hai phương án khai danh sách autoconfig: cột trái nếu @Import thẳng 143 class thì danh sách nằm cứng trong annotation của Boot core, jar của bạn không có chỗ nào chen tên vào, và cả 143 class phải compile được cùng một lúc; cột phải mỗi jar mang một file text riêng gồm spring-boot-autoconfigure 143 dòng, acme-tracing-starter 1 dòng, spring-cloud-config N dòng, rồi classpath sao gom hết thành một danh sách

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.

Đường đi tránh Class.forName: KafkaAutoConfiguration.class mới chỉ là bytecode trên đĩa, ASM đọc bảng annotation trong bytecode để lấy chuỗi trong @ConditionalOnClass, rồi hỏi có file KafkaProducer.class trên classpath không; nhánh có thì giờ mới Class.forName và đăng ký BeanDefinition, nhánh không thì bỏ qua và KafkaAutoConfiguration chưa từng được nạp vào JVM

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
0/5 câu đã trả lời
  1. Q1
    @EnableAutoConfiguration tự register bean DataSource không? Giải thích đúng pipeline từ annotation đến bean object.
  2. 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. 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().
  4. 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.
  5. Q5
    DeferredImportSelector khác ImportSelector như thế nào? Tại sao AutoConfigurationImportSelector phải implement DeferredImportSelector chứ không phải ImportSelector?

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

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

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