Welcome to MikeDaDev's Blog – notes on Java, backend & system design.

Explore
MikeDaDev's Blog

Spring Boot Auto-configuration

Tung Bui Thanh (TungDaDev)11 mins read

Auto configuration.webp

Có một câu phỏng vấn hay dùng để phân biệt Mid-level với Senior Spring: 'Điều gì thực sự xảy ra bên trong JVM khi bạn thêm spring-boot-starter-data-jpa vào pom.xml mà không viết dòng cấu hình DataSource hay EntityManager nào?'. Nhiều người trả lời chung chung: 'Do annotation @EnableAutoConfiguration'. Nhưng làm sao Spring quét qua hàng trăm thư viện mà không chậm lúc khởi động? Làm sao một class cấu hình nhắc tới thư viện không có trên classpath mà ứng dụng vẫn không văng lỗi?'. Câu trả lời nằm ở metadata sinh sẵn lúc build và cách Spring đọc bytecode.

Auto-Configuration là tính năng làm nên tên tuổi của Spring Boot. Nhưng nếu không hiểu nó chạy ra sao, bạn sẽ dễ bí khi gặp lỗi xung đột Bean (BeanDefinitionOverrideException), bean mặc định không được tạo vì sai thứ tự nạp, vòng phụ thuộc giữa các bean (circular dependency), hoặc khi cần đóng gói thư viện dùng chung (Shared Enterprise Starters) cho cả công ty.

Bài này đọc qua cách Auto-Configuration hoạt động trong mã nguồn Spring Boot 3.x, rồi hướng dẫn xây dựng một Custom Starter dùng được trên production.


# luồng xử lý bên trong (pipeline)

Auto-Configuration được nạp trong lúc ApplicationContext refresh, cụ thể là khi ConfigurationClassPostProcessor xử lý các class @Configuration:


# vì sao nhanh: ASM Bytecode scanning & DeferredImportSelector

Câu hỏi quan trọng ở đây: Làm sao Spring Boot kiểm tra được @ConditionalOnClass(name = "io.lettuce.core.RedisClient") khi dependency Redis KHÔNG có trong classpath mà không văng java.lang.ClassNotFoundException?

# cơ chế đọc Bytecode trực tiếp bằng ASM (không nạp class)

Thử hình dung RedisAutoConfiguration có @ConditionalOnClass(RedisOperations.class). Nếu Spring nạp thẳng class cấu hình này rồi đọc annotation bằng reflection, phần giá trị RedisOperations.class sẽ trỏ tới một class không tồn tại, và JVM ném TypeNotPresentException hoặc NoClassDefFoundError tuỳ lúc nào nó chạm tới. Vì vậy Spring tránh nạp class cấu hình cho tới khi chắc chắn nó dùng được. Có hai lớp làm việc này:

  • Lọc sớm bằng metadata sinh lúc build. Khi build spring-boot-autoconfigure (và các starter có dùng spring-boot-autoconfigure-processor), annotation processor ghi các điều kiện đơn giản ra file META-INF/spring-autoconfigure-metadata.properties, kiểu ...RedisAutoConfiguration.ConditionalOnClass=org.springframework.data.redis.core.RedisOperations. AutoConfigurationImportSelector chạy các AutoConfigurationImportFilter (OnClassCondition, OnBeanCondition, OnWebApplicationCondition) trên file này, nên phần lớn ứng viên bị loại mà chưa cần mở file .class nào.
  • Đọc annotation bằng ASM. Ứng viên còn lại được ConfigurationClassParser đọc qua MetadataReaderFactory, tức Spring ASM đọc thẳng byte của file .class. Ở mức bytecode, RedisOperations.class trong annotation chỉ là một chuỗi tên class, nên đọc được mà không cần class đó tồn tại.

Còn chuyện "class có trên classpath không" thì Spring kiểm tra bằng Class.forName(name, false, classLoader): có nạp class đó nếu nó tồn tại, nhưng không chạy static initializer, và bắt ClassNotFoundException để trả về "không có". Nói cách khác, cái được tránh nạp là class cấu hình nhắc tới thư viện thiếu, không phải mọi class. Nhờ vậy hơn trăm auto-configuration trong spring-boot-autoconfigure bị loại rất rẻ lúc khởi động.

# vì sao dùng DeferredImportSelector?

Trong Spring Core, có hai loại ImportSelector:

  • ImportSelector thông thường: Chạy ngay khi được phát hiện.
  • DeferredImportSelector (Trì hoãn): Chỉ chạy sau khi mọi class @Configuration do người dùng tự viết đã được parse xong. Vì thế bean definition của auto-configuration được đăng ký sau bean của người dùng, và lúc @ConditionalOnMissingBean được đánh giá thì bean của người dùng đã có mặt trong BeanFactory.

Nguyên tắc của Spring Boot: "User Configuration Always Wins" (cấu hình của người dùng luôn được ưu tiên). Chỉ khi người dùng chưa khai báo bean nào (kiểm tra qua @ConditionalOnMissingBean) thì bean mặc định của Auto-Configuration mới được kích hoạt.


# Spring Boot 3.x: tạm biệt spring.factories

Trong Spring Boot 2.x, mọi cấu hình tự động đều nằm chung trong một file:

# Spring Boot 2.x (deprecated từ 2.7, bị bỏ qua từ 3.0): META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
  org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration

Nhược điểm: File này vừa dùng để đăng ký Auto-Configuration, vừa đăng ký ApplicationListener, ApplicationContextInitializer... nên việc phân giải khá cồng kềnh.

# chuẩn mới từ Spring Boot 3.x

Spring Boot 2.7 giới thiệu file riêng cho Auto-Configuration (cùng annotation @AutoConfiguration), và từ 3.0 thì đây là cách duy nhất: auto-configuration khai trong spring.factories bị bỏ qua hoàn toàn, không báo lỗi gì, starter chỉ đơn giản là "không chạy". Spring Boot 4 vẫn giữ nguyên file này.

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

File này chỉ chứa danh sách class cấu hình, mỗi dòng một class, được phép có dòng comment bắt đầu bằng # (merge trên Git dễ hơn, không còn dấu gạch chéo \ gây xung đột). spring.factories vẫn còn dùng cho những thứ khác như ApplicationListener, EnvironmentPostProcessor, FailureAnalyzer.

com.enterprise.banking.security.BankingSecurityAutoConfiguration
com.enterprise.banking.audit.AuditTrailAutoConfiguration

# họ annotation @conditional

Auto-Configuration chạy được là nhờ các annotation @Conditional:

# phân biệt @conditionalonbean vs @conditionalonmissingbean

  • @ConditionalOnMissingBean: Dùng rất nhiều trong Auto-Configuration, để cung cấp một Bean mặc định (Default Implementation). Muốn tùy biến thì người dùng chỉ cần tự định nghĩa một @Bean cùng kiểu, Bean mặc định sẽ tự rút lui.
  • Cạm bẫy với @ConditionalOnBean: Cẩn thận thứ tự nạp. Điều kiện này chỉ nhìn thấy những bean definition đã được đăng ký tính tới lúc nó được đánh giá. Nếu bean mục tiêu đến từ một auto-configuration chạy sau, điều kiện trả về false và bean của bạn bị bỏ qua oan. Tài liệu Spring Boot khuyên chỉ dùng hai annotation này trong class auto-configuration, và khai rõ thứ tự bằng after như bên dưới.

# sắp xếp thứ tự nạp cấu hình

Dùng các annotation sau trên class @AutoConfiguration:

  • @AutoConfigureBefore(SecurityAutoConfiguration.class)
  • @AutoConfigureAfter(DataSourceAutoConfiguration.class)
  • @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)

Từ 2.7 có thể viết gọn trong chính annotation: @AutoConfiguration(after = DataSourceAutoConfiguration.class). Spring sắp danh sách theo bảng chữ cái trước, rồi theo @AutoConfigureOrder, cuối cùng mới áp ràng buộc before/after. Các annotation này chỉ có tác dụng giữa các auto-configuration với nhau, không ảnh hưởng tới class @Configuration của người dùng.


# xây dựng enterprise custom starter

Ở công ty lớn có hàng chục microservices, người phụ trách kiến trúc thường đóng gói các thư viện chuẩn hóa (như Logging MDC, Common Security JWT, Dynamic Multitenancy) thành Spring Boot Starters nội bộ.

# cấu trúc module

# viết class properties type-safe có validation

@Validated chỉ có tác dụng khi trên classpath có một implementation của Jakarta Bean Validation (thường là Hibernate Validator qua spring-boot-starter-validation). Nếu thiếu, ràng buộc @NotBlank bị lờ đi trong im lặng, nên starter nên kéo luôn dependency này.

@ConfigurationProperties(prefix = "enterprise.audit")
@Validated
public class EnterpriseAuditProperties {
 
    @NotBlank(message = "Tên service định danh audit không được để trống")
    private String serviceName;
 
    private boolean enabled = true;
 
    private Duration batchTimeout = Duration.ofSeconds(5);
 
    // Getters and Setters
    public String getServiceName() { return serviceName; }
    public void setServiceName(String serviceName) { this.serviceName = serviceName; }
    public boolean isEnabled() { return enabled; }
    public void setEnabled(boolean enabled) { this.enabled = enabled; }
    public Duration getBatchTimeout() { return batchTimeout; }
    public void setBatchTimeout(Duration batchTimeout) { this.batchTimeout = batchTimeout; }
}

# viết class auto-configuration có đủ điều kiện bảo vệ

@AutoConfiguration
@ConditionalOnClass(AuditClient.class) // Chỉ kích hoạt nếu có SDK trên classpath
@ConditionalOnProperty(prefix = "enterprise.audit", name = "enabled", havingValue = "true", matchIfMissing = true)
@EnableConfigurationProperties(EnterpriseAuditProperties.class)
public class EnterpriseAuditAutoConfiguration {
 
    private static final Logger log = LoggerFactory.getLogger(EnterpriseAuditAutoConfiguration.class);
 
    @Bean
    @ConditionalOnMissingBean(AuditClient.class) // Nhường quyền nếu dev tự tạo client riêng
    public AuditClient defaultAuditClient(EnterpriseAuditProperties properties,
                                          ObjectProvider<MeterRegistry> meterRegistry) {
        log.info("Khởi tạo Enterprise Audit Client tự động cho service: {}", properties.getServiceName());
        // App không có Actuator thì không có bean MeterRegistry: dùng registry tạm thay vì làm hỏng startup
        return new KafkaAuditClient(properties.getServiceName(), properties.getBatchTimeout(),
                meterRegistry.getIfAvailable(SimpleMeterRegistry::new));
    }
}

Hai chi tiết đáng để ý. @ConditionalOnClass(AuditClient.class) viết dạng class literal vẫn an toàn dù SDK vắng mặt, nhờ cơ chế đọc annotation bằng ASM ở trên. Còn MeterRegistry được nhận qua ObjectProvider vì bean này chỉ có khi app dùng Actuator; nếu inject thẳng, service nào không có Actuator sẽ chết lúc khởi động với NoSuchBeanDefinitionException.

# đăng ký vào file autoconfiguration.imports

Tạo file tại: src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:

com.enterprise.banking.audit.autoconfigure.EnterpriseAuditAutoConfiguration

Từ giờ, dev nào trong công ty cũng chỉ cần thêm dependency:

<dependency>
    <groupId>com.enterprise.banking</groupId>
    <artifactId>banking-spring-boot-starter</artifactId>
    <version>2.0.0</version>
</dependency>

Hệ thống Audit, Kafka Producer và Metrics tự chạy mà không cần viết thêm dòng cấu hình nào.


# debug auto-configuration trên production

Khi một Bean không chịu khởi tạo hoặc bị ghi đè ngoài ý muốn, điều tra thế nào?

# bật conditions evaluation report

Chạy ứng dụng với tham số:

java -Ddebug=true -jar app.jar

Spring Boot sẽ in một báo cáo ngay khi khởi động:

  • Positive matches: Các cấu hình đã thỏa mãn điều kiện và được nạp thành công.
  • Negative matches: Các cấu hình bị loại và lý do bị loại (ví dụ khi đặt enterprise.audit.enabled=false: @ConditionalOnProperty (enterprise.audit.enabled=true) found different value in property 'enabled').
  • Exclusions: Các cấu hình bị loại thủ công qua exclude.
  • Unconditional classes: Các cấu hình luôn chạy.

Dùng --debug làm tham số chương trình cũng cho kết quả tương tự.

# dùng actuator /actuator/conditions

Trên Production đang chạy, bạn có thể gọi API:

curl -X GET https://internal-api.company.com/actuator/conditions

Endpoint này trả về JSON chứa đúng báo cáo trên (được ghi lại lúc khởi động), nên xem được trên môi trường đang chạy mà không cần restart với --debug. Mặc định Spring Boot chỉ mở health qua HTTP, muốn gọi endpoint này phải thêm management.endpoints.web.exposure.include=health,conditions. Báo cáo lộ khá nhiều chi tiết về cấu trúc ứng dụng, nên chỉ mở ở cổng management nội bộ hoặc sau lớp xác thực.


# checklist

  • 1. Chuẩn Hóa Cấu Trúc Starter: Tách rõ module Starter (pom-only) và module Autoconfigure để không làm lộ dependency không cần thiết cho client.
  • 2. Luôn Dùng @ConditionalOnMissingBean: Mọi Bean trong Auto-Configuration nên có annotation này để người dùng ghi đè (override) khi cần.
  • 3. Chuyển Đổi Sang AutoConfiguration.imports: Khi nâng lên Spring Boot 3, nhớ chuyển thư viện từ spring.factories sang file imports mới.
  • 4. Khai Báo Configuration Metadata: Thêm annotation processor spring-boot-configuration-processor để IntelliJ IDEA và Eclipse tự gợi ý (Autocomplete) các thuộc tính trong file YAML. Thêm cả spring-boot-autoconfigure-processor để sinh spring-autoconfigure-metadata.properties, giúp bộ lọc sớm loại starter của bạn mà không phải đọc class.
  • 5. Thẩm Định Lỗi Khởi Động (FailureAnalyzer): Viết thêm class kế thừa AbstractFailureAnalyzer để báo lỗi dễ hiểu, có hướng dẫn sửa, khi người dùng thiếu cấu hình bắt buộc. FailureAnalyzer vẫn đăng ký trong META-INF/spring.factories với key org.springframework.boot.diagnostics.FailureAnalyzer.

# lời kết

Auto-Configuration không có phép thuật gì. Nó là một thiết kế khá gọn: danh sách ứng viên trong một file, bộ lọc rẻ chạy trước, điều kiện đầy đủ chạy sau, và bean của người dùng luôn được ưu tiên.

Muốn đọc tiếp thì mục "Creating Your Own Auto-configuration" trong tài liệu Spring Boot nói khá kỹ về điều kiện, thứ tự và cách test starter bằng ApplicationContextRunner. Mở mã nguồn AutoConfigurationImportSelector và OnClassCondition đọc song song với bài này cũng là một cách hay để thấy từng bước ở trên.


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.