Command Palette

Search for a command to run...

[Spring Boot Basics] Auto-configuration trong Spring Boot hoạt động ra sao: conditional, back-off và báo cáo --debug

Auto-configuration là phần của Spring Boot vẫn còn giống như phép thuật ngay cả khi bạn đã hiểu hết mọi thứ còn lại. Bạn thêm một starter, thế là có DispatcherServlet, có JSON mapper, có Tomcat — trong khi không dòng nào trong thư mục source của bạn yêu cầu những thứ đó.

Nhưng ở đây không có phép thuật nào cả. Một auto-configuration chỉ là một @Configuration class chứa đầy các @Bean method — đúng loại class bạn đã viết suốt năm bài vừa rồi — kèm các annotation nói rõ khi nào nó được áp dụng. Bài này mở hộp ra: Boot lấy danh sách từ đâu, một class như thế trông thế nào, condition được đánh giá ra sao, và cách đọc báo cáo ghi lại từng quyết định của nó.

Các candidate configuration đi qua một cổng conditional, chỉ một phần vào tới registry

Mọi thứ bên dưới chạy trên OpenJDK 21.0.6 với Spring Boot 4.1.1 (Spring Framework 7.0.9, embedded Tomcat 11.0.24) và Gradle 9.7.1, trên project sinh bởi Spring Initializr với dependencies=web. Mọi con số, nội dung file và đoạn báo cáo đều copy từ project đó ra, không viết theo trí nhớ.

Auto-configuration là @Configuration class biết tự rút lui

Bạn đã biết bean là gì, registry giữ nó ra sao, và một @Bean method trong @Configuration class làm gì. Auto-configuration chỉ thêm đúng một ý: bản thân class và từng @Bean method của nó mang theo condition, và chúng bị bỏ qua khi condition không thỏa.

Đó là toàn bộ cơ chế. Boot phát hành các class này rải khắp các module của nó, đưa cho container những cái khớp condition, và bỏ phần còn lại. Chúng là configuration Spring bình thường — không có hỗ trợ đặc biệt nào từ container, không có lifecycle riêng, framework không đối xử với chúng khác gì một @Configuration class do bạn tự viết.

Phần kỹ thuật đáng quan tâm nằm ở ba câu hỏi, và phần còn lại của bài trả lời lần lượt: Boot dựng danh sách candidate bằng cách nào, condition được đánh giá ra sao, và làm sao biết chuyện gì đã xảy ra.

Spring Boot tìm auto-configuration bằng cách nào

@SpringBootApplication là annotation ghép, và một trong ba thành phần của nó là @EnableAutoConfiguration. Annotation đó tự nó không làm gì; nó là một @Import:

Java
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

AutoConfigurationImportSelector mới là nơi danh sách được tạo ra. Method cốt lõi của nó, lấy nguyên văn từ source 4.1.1:

Java
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
configurations = removeDuplicates(configurations);
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
configurations = getConfigurationClassFilter().filter(configurations);
fireAutoConfigurationImportEvents(configurations, exclusions);

Năm bước đọc thẳng từ trên xuống: lấy candidate, bỏ trùng, loại phần bạn exclude, lọc nhanh, phát sự kiện kết quả. getCandidateConfigurations ủy quyền cho ImportCandidates.load, và đây là đoạn đáng thuộc lòng:

Java
private static final String LOCATION = "META-INF/spring/%s.imports";
...
String location = String.format(LOCATION, annotation.getName());
Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
List<String> importCandidates = new ArrayList<>();
while (urls.hasMoreElements()) {
    URL url = urls.nextElement();
    importCandidates.addAll(readCandidateConfigurations(url));
}

findUrlsInClasspath chính là classLoader.getResources(location). Nó trả về mọi file khớp trên classpath, và danh sách candidate là hợp của tất cả. Vậy file cần tìm trong bất kỳ jar nào là:

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

Năm bước từ @EnableAutoConfiguration tới registry, kèm sáu jar đóng góp file .imports

Đó là file text thuần, mỗi dòng một tên class đầy đủ. Đây là toàn bộ nội dung file nằm trong spring-boot-autoconfigure-4.1.1.jar, lấy ra bằng cách unzip jar trong Gradle cache:

Text
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration
org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration

Mười hai dòng. Con số này gây bất ngờ nếu bạn từng đọc bài hướng dẫn nào viết trước 2025, vì trên Spring Boot 3 file duy nhất này chứa gần như toàn bộ — cũng file đó trong spring-boot-autoconfigure-3.5.6.jar liệt kê 156 class. Boot 4 đã tách auto-configuration ra khỏi spring-boot-autoconfigure thành nhiều module spring-boot-* nhỏ, mỗi module mang file .imports riêng. Quét cả 39 jar trên runtime classpath của project này tìm thấy sáu file:

JarSố candidate đóng góp
spring-boot-autoconfigure-4.1.1.jar12
spring-boot-webmvc-4.1.1.jar6
spring-boot-servlet-4.1.1.jar5
spring-boot-tomcat-4.1.1.jar5
spring-boot-jackson-4.1.1.jar1
spring-boot-http-converter-4.1.1.jar1
Tổng30

Vậy danh sách candidate của một web application tối giản trên Boot 4.1.1 là 30 class, không phải mười hai class trong cái jar mà cái tên gợi ý rằng nó sở hữu tất cả. Chỉ trích dẫn mười hai dòng đó là cách nhanh nhất để tự hiểu sai cơ chế này.

Một điểm cần đính chính so với tài liệu cũ. Cơ chế ngày xưa là một key tên EnableAutoConfiguration nằm trong META-INF/spring.factories; không jar nào trên classpath này còn mang key đó, và Boot 4 chỉ đọc các file .imports. spring.factories chưa biến mất — spring-boot-autoconfigure vẫn còn một file — nhưng thứ còn sót trong đó là listener, failure analyzer và import filter, không phải danh sách candidate.

Bộ lọc chạy trước khi đánh giá bất kỳ condition nào

getConfigurationClassFilter().filter(configurations) ở đoạn code trên chạy trước khi Spring load bất kỳ candidate class nào. Việc của nó là loại candidate một cách rẻ tiền, vì load 30 class (hoặc vài trăm, với application lớn) chỉ để phát hiện phần lớn không áp dụng được là một chi phí startup có thật.

Các filter được đăng ký trong spring.factories:

Text
org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

Chúng trả lời dựa trên một file sinh tự động khác, META-INF/spring-autoconfigure-metadata.properties, do annotation processor của Boot ghi lúc build. Đây là chỉ mục phẳng của các condition mà mỗi class khai báo:

Properties
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration=
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration=
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration$AspectJAutoProxyingConfiguration.ConditionalOnClass=org.aspectj.weaver.Advice
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration=
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration.ConditionalOnClass=org.springframework.jmx.export.MBeanExporter

Nhờ đó filter kết luận được "org.aspectj.weaver.Advice không có trên classpath nên AspectJAutoProxyingConfiguration bị loại" chỉ bằng cách so một chuỗi, không cần đọc file class nào. Chỉ những candidate sống sót mới được load và đánh giá đầy đủ tập @ConditionalOn*.

Một auto-configuration thật trông thế nào

Lấy một class nhỏ trong spring-boot-servlet-4.1.1.jar. HttpEncodingAutoConfiguration gói trọn cả pattern trong bốn mươi dòng, đã bỏ phần header giấy phép:

Java
package org.springframework.boot.servlet.autoconfigure;
 
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBooleanProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication.Type;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.servlet.filter.OrderedCharacterEncodingFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.web.filter.CharacterEncodingFilter;
 
@AutoConfiguration
@EnableConfigurationProperties(ServletEncodingProperties.class)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass(CharacterEncodingFilter.class)
@ConditionalOnBooleanProperty(name = "spring.servlet.encoding.enabled", matchIfMissing = true)
public final class HttpEncodingAutoConfiguration {
 
    @Bean
    @ConditionalOnMissingBean
    CharacterEncodingFilter characterEncodingFilter(ServletEncodingProperties properties) {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(properties.getCharset().name());
        filter.setForceRequestEncoding(properties.shouldForce(ServletEncodingProperties.HttpMessageType.REQUEST));
        filter.setForceResponseEncoding(properties.shouldForce(ServletEncodingProperties.HttpMessageType.RESPONSE));
        return filter;
    }
 
}

Đọc các annotation theo thứ tự là thấy ngay thiết kế:

AnnotationQuyết định điều gì
@AutoConfigurationđây là một auto-configuration; nó được meta-annotate bằng @Configuration(proxyBeanMethods = false) và mang các thuộc tính sắp thứ tự
@EnableConfigurationPropertiesbind spring.servlet.encoding.* vào một properties object và đăng ký object đó thành bean
@ConditionalOnWebApplication(type = SERVLET)bỏ qua cả class trừ khi đây là servlet web application
@ConditionalOnClass(CharacterEncodingFilter.class)bỏ qua trừ khi class đó có trên classpath
@ConditionalOnBooleanProperty(...)bỏ qua nếu ai đó set property thành false; matchIfMissing = true nghĩa là mặc định bật
@ConditionalOnMissingBeanđặt trên @Bean method: bỏ qua riêng bean này nếu registry đã có một CharacterEncodingFilter

Ba condition ở mức class quyết định được ăn cả ngã về không cho toàn class. Condition ở mức method thì tính riêng từng bean, và chính điều đó cho phép một class có mười @Bean method vẫn đóng góp chín cái, để bạn tự cung cấp cái thứ mười.

Bản thân @Bean method chẳng có gì đặc biệt: tạo object, set vài field từ properties đã bind, rồi return. Nếu bạn tự viết nó trong @Configuration class của mình thì nó cũng y hệt như vậy.

Họ annotation @ConditionalOn*

Ở đây có hai tầng và nên tách bạch chúng.

@Conditional và interface Condition là tính năng của Spring Framework. Chúng không liên quan gì tới Boot: bất kỳ @Configuration class, @Bean method hay @Component nào cũng có thể mang @Conditional(SomeCondition.class), và Spring sẽ hỏi condition đó trước khi đăng ký bean definition.

Các annotation @ConditionalOn* là của Spring Boot. Mỗi cái là một @Conditional đi kèm sẵn một Condition implementation, đóng gói lại để bạn viết annotation thay vì viết class. Boot 4.1.1 có 21 annotation như vậy trong org.springframework.boot.autoconfigure.condition. Bảy cái dưới đây bao gần hết những gì bạn sẽ đọc thấy trong báo cáo:

AnnotationKhớp khi
@ConditionalOnClassmọi class được nêu đều có trên classpath
@ConditionalOnMissingClasskhông class nào trong số được nêu có trên classpath
@ConditionalOnBeanregistry đã có bean thuộc type hoặc tên đó
@ConditionalOnMissingBeanregistry chưa có bean thuộc type hoặc tên đó
@ConditionalOnPropertyproperty được set và khớp havingValue (có matchIfMissing cho trường hợp mặc định)
@ConditionalOnResourceresource được nêu tồn tại, ví dụ classpath:schema.sql
@ConditionalOnWebApplicationapplication là web application thuộc Type đã nêu — SERVLET, REACTIVE hoặc ANY

Mười bốn cái còn lại hẹp hơn: @ConditionalOnBooleanProperty@ConditionalOnProperties là biến thể chuyên biệt của cái về property, @ConditionalOnSingleCandidate khớp khi chỉ có đúng một bean ứng viên của một type, @ConditionalOnExpression nhận một SpEL expression, và còn các cái kiểm tra Java version, threading model, JNDI, WAR deployment và cloud platform.

Đây là chúng đặt trên một configuration class của riêng tôi trong project demo, để hành vi quan sát được thay vì chỉ mô tả:

ConditionsDemo.java
@Configuration
public class ConditionsDemo {
 
    @Bean
    @ConditionalOnClass(name = "org.apache.catalina.startup.Tomcat")
    String onClass() { return "onClass"; }
 
    @Bean
    @ConditionalOnMissingClass("com.google.gson.Gson")
    String onMissingClass() { return "onMissingClass"; }
 
    @Bean
    @ConditionalOnBean(DispatcherServlet.class)
    String onBean() { return "onBean"; }
 
    @Bean
    @ConditionalOnProperty(name = "demo.audit", havingValue = "on")
    String onProperty() { return "onProperty"; }
 
    @Bean
    @ConditionalOnResource(resources = "classpath:application.properties")
    String onResource() { return "onResource"; }
 
    @Bean
    @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
    String onWebApplication() { return "onWebApplication"; }
}

Chạy application rồi hỏi context xem bean nào tồn tại:

Text
  onClass = true
  onMissingClass = true
  onBean = false
  onProperty = false
  onResource = true
  onWebApplication = true

Năm trong sáu kết quả đúng như dự đoán: Tomcat có trên classpath, Gson thì không, application.properties tồn tại, đây là servlet application, và demo.audit chưa được set. Thêm --demo.audit=on vào command line thì cái thứ tư lật thành true. Kết quả thứ năm mới là chỗ thú vị.

Cái bẫy: @ConditionalOnBean trên configuration của chính bạn

onBeanfalse, trong khi context hoàn chỉnh chắc chắn có bean dispatcherServlet — application đang phục vụ HTTP request mà. Condition đã được đánh giá vào thời điểm bean đó chưa tồn tại.

Các @Configuration class của người dùng được parse trước, trong lúc component scanning. Auto-configuration được import bởi một DeferredImportSelector, mà theo thiết kế thì chạy sau cùng, khi mọi thứ bạn viết đã được biết. Nên lúc ConditionsDemo được parse, DispatcherServletAutoConfiguration chưa chạy và dispatcherServlet chưa được đăng ký.

Sự bất đối xứng đó là lý do tài liệu ghi rằng @ConditionalOnBean@ConditionalOnMissingBean chỉ nên dùng bên trong auto-configuration class. Trong auto-configuration chúng đáng tin, vì tới lúc đó mọi bean của người dùng đã đăng ký xong. Trong configuration của bạn, chúng phụ thuộc thứ tự parse mà bạn không kiểm soát được. Hãy dùng @ConditionalOnClass hoặc @ConditionalOnProperty trong code của mình, và để các condition về sự tồn tại bean lại cho auto-configuration.

Tự viết một Condition

Khi không annotation nào trong 21 cái kia vừa vặn, bạn xuống tầng interface bên dưới. Condition có đúng một method:

Java
boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata);

ConditionContext cho bạn Environment, BeanDefinitionRegistry, ConfigurableListableBeanFactory, ResourceLoaderClassLoader — đủ mọi thứ để soi application ngay lúc nó đang được dựng. AnnotatedTypeMetadata mô tả chính cái đang được đánh giá: một class, hoặc một @Bean method, cùng các annotation của nó. Đây là một condition dựa trên kiến trúc CPU, thứ mà không annotation dựng sẵn nào phủ:

OnArm64Condition.java
public class OnArm64Condition implements Condition {
 
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
        String arch = context.getEnvironment().getProperty("os.arch", "unknown");
        boolean match = arch.contains("aarch64") || arch.contains("arm64");
        String target = (metadata instanceof MethodMetadata m)
                ? m.getDeclaringClassName() + "#" + m.getMethodName()
                : metadata.toString();
        System.out.println("[OnArm64Condition] " + target + " os.arch=" + arch + " -> " + match);
        return match;
    }
}
ConditionDemoConfig.java
@Configuration
public class ConditionDemoConfig {
 
    @Bean
    @Conditional(OnArm64Condition.class)
    String cpuNote() { return "running on arm64"; }
}

Trên máy này, một chiếc Mac Apple Silicon:

Text
[OnArm64Condition] com.example.demo.ConditionDemoConfig#cpuNote os.arch=aarch64 -> true
...
cpuNote bean present: true

Ghi đè property trên command line bằng --os.arch=x86_64 chứng minh chính condition đang làm việc chứ bean không được đăng ký vô điều kiện:

Text
[OnArm64Condition] com.example.demo.ConditionDemoConfig#cpuNote os.arch=x86_64 -> false
...
cpuNote bean present: false

Một điều nên biết trước khi bạn đi tìm: một Condition thuần không xuất hiện trong báo cáo --debug. Báo cáo của Boot được điền bởi SpringBootCondition, base class mà mọi implementation @ConditionalOn* kế thừa. OnArm64Condition của tôi implement thẳng Condition, nên nó quyết định đúng mà không báo cáo gì — đó cũng là lý do nó tự in ra dòng của mình ở trên.

Back-off: Boot chỉ cấu hình phần bạn chưa làm

@ConditionalOnMissingBean là annotation làm cho toàn bộ thiết kế này chạy được. Nó cho phép Boot phát hành hàng trăm bean đầy quan điểm mà không bao giờ tranh chấp với bạn: ngay khi bạn khai báo một bean cùng type, bean của Boot ngừng được tạo. Không có gì để tắt, không property nào phải set, không cuộc đua ưu tiên nào phải thắng. Khai báo bean chính là cách override.

Đây là màn thật. Application là project sinh tự động, thêm một ApplicationRunner in ra những gì có trong registry. Trước tiên là bản nguyên vẹn:

Text
CharacterEncodingFilter beans: [characterEncodingFilter]
  characterEncodingFilter -> org.springframework.boot.servlet.filter.OrderedCharacterEncodingFilter
total beans: 146

Bean của Boot nằm đó, và nó là OrderedCharacterEncodingFilter — subclass riêng của Boot có kèm thứ tự filter. Báo cáo --debug liệt kê nó dưới Positive matches:

Text
   HttpEncodingAutoConfiguration matched:
      - @ConditionalOnClass found required class 'org.springframework.web.filter.CharacterEncodingFilter' (OnClassCondition)
      - found 'session' scope (OnWebApplicationCondition)
      - @ConditionalOnBooleanProperty (spring.servlet.encoding.enabled=true) matched (OnPropertyCondition)
 
   HttpEncodingAutoConfiguration#characterEncodingFilter matched:
      - @ConditionalOnMissingBean (types: org.springframework.web.filter.CharacterEncodingFilter; SearchStrategy: all) did not find any beans (OnBeanCondition)

Giờ thêm một configuration class của tôi và không đổi gì khác:

AppConfig.java
@Configuration
public class AppConfig {
 
    @Bean
    CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new CharacterEncodingFilter();
        filter.setEncoding(StandardCharsets.UTF_8.name());
        filter.setForceResponseEncoding(true);
        return filter;
    }
}

Cũng lần chạy đó giờ báo:

Text
CharacterEncodingFilter beans: [characterEncodingFilter]
  characterEncodingFilter -> org.springframework.web.filter.CharacterEncodingFilter
total beans: 147

Vẫn một bean filter, vẫn đúng tên bean đó, nhưng là của tôi. Và mục trong báo cáo đã chuyển từ Positive matches sang Negative matches:

Text
   HttpEncodingAutoConfiguration#characterEncodingFilter:
      Did not match:
         - @ConditionalOnMissingBean (types: org.springframework.web.filter.CharacterEncodingFilter; SearchStrategy: all) found beans of type 'org.springframework.web.filter.CharacterEncodingFilter' characterEncodingFilter (OnBeanCondition)

@ConditionalOnMissingBean chọn giữa hai thế giới: Boot tạo bean mặc định, hoặc Boot lùi lại vì bạn đã khai báo bean

Con số tổng cũng đổi theo: 52 positive và 39 negative trở thành 51 positive và 40 negative. Đúng một quyết định lật, và bản thân HttpEncodingAutoConfiguration vẫn là positive match — chỉ riêng @Bean method của nó lùi lại. Mức chi tiết này rất quan trọng: một auto-configuration có nhiều bean vẫn tiếp tục đóng góp những bean bạn không thay thế.

⚠️ Back-off xét theo type (hoặc theo tên bean, khi annotation chỉ định tên), không phải chỉ theo tên bean, và nó chỉ hiệu quả nếu bean của bạn được đăng ký trước khi auto-configuration được đánh giá — điều này tự động đúng với mọi thứ nằm trong component scan hoặc @Configuration class của bạn. Một bean đăng ký từ BeanFactoryPostProcessor sai thời điểm có thể lỡ mất cửa sổ đó.

Classpath quyết định thứ gì được cấu hình

Cách minh họa rõ nhất cho toàn bộ cơ chế chỉ tốn một dòng trong build.gradle. Vẫn application đó — không đổi một ký tự nào trong src/ — và thêm một dependency:

build.gradle
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-webmvc'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
}

Sáu jar mới xuất hiện: spring-boot-starter-thymeleaf, spring-boot-thymeleaf, thymeleaf, thymeleaf-spring6, attoparserunbescape. Một trong số đó, spring-boot-thymeleaf-4.1.1.jar, mang file .imports chỉ có đúng một dòng:

Text
org.springframework.boot.thymeleaf.autoconfigure.ThymeleafAutoConfiguration

Đó là thay đổi duy nhất đối với danh sách candidate — 30 thành 31 — và đây là hệ quả:

spring-boot-starter-webmvc+ spring-boot-starter-thymeleaf
Số jar trên runtime classpath3945
Số auto-configuration candidate3031
Positive matches5258
Negative matches3945
Số bean definition trong context146155
Bean mới có thể injectdefaultTemplateResolver, templateEngine, thymeleafViewResolver

Cùng một code application trên hai classpath, tạo ra hai tập bean khác nhau

Một candidate sinh ra sáu mục positive mới và sáu mục negative mới, vì ThymeleafAutoConfiguration là class chứa các configuration class lồng bên trong, mỗi cái lại có condition riêng:

Text
   ThymeleafAutoConfiguration matched:
      - @ConditionalOnClass found required classes 'org.thymeleaf.templatemode.TemplateMode', 'org.thymeleaf.spring6.SpringTemplateEngine' (OnClassCondition)
 
   ThymeleafAutoConfiguration.DefaultTemplateResolverConfiguration matched:
      - @ConditionalOnMissingBean (names: defaultTemplateResolver; SearchStrategy: all) did not find any beans (OnBeanCondition)
 
   ThymeleafAutoConfiguration.ThymeleafWebMvcConfiguration matched:
      - found 'session' scope (OnWebApplicationCondition)

Còn nửa negative của cùng class đó cho thấy Thymeleaf đã có thể cấu hình thêm gì nếu classpath đầy đủ hơn:

Text
   ThymeleafAutoConfiguration.ThymeleafSecurityDialectConfiguration:
      Did not match:
         - @ConditionalOnClass did not find required classes 'org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect', 'org.springframework.security.web.server.csrf.CsrfToken' (OnClassCondition)
 
   ThymeleafAutoConfiguration.ThymeleafWebFluxConfiguration:
      Did not match:
         - did not find reactive web application classes (OnWebApplicationCondition)

Thêm Spring Security thì cái đầu tiên lật thành positive match mà bạn không viết dòng nào. Đó là toàn bộ mô hình: classpath chính là configuration.

Đọc CONDITIONS EVALUATION REPORT

Khởi động application với --debug trên command line (hoặc debug=true trong application.properties) và Boot in báo cáo ngay khi context sẵn sàng:

Bash
java -jar build/libs/demo-0.0.1-SNAPSHOT.jar --server.port=8090 --debug
Text
============================
CONDITIONS EVALUATION REPORT
============================
 
 
Positive matches:
-----------------
 
   AopAutoConfiguration matched:
      - @ConditionalOnBooleanProperty (spring.aop.auto=true) matched (OnPropertyCondition)
 
   AopAutoConfiguration.ClassProxyingConfiguration matched:
      - @ConditionalOnMissingClass did not find unwanted class 'org.aspectj.weaver.Advice' (OnClassCondition)
      - @ConditionalOnBooleanProperty (spring.aop.proxy-target-class=true) matched (OnPropertyCondition)

Nó có bốn phần, luôn theo thứ tự này. Với project dependencies=web trần, báo cáo dài hơn 360 dòng một chút và phân bổ như sau:

PhầnNội dungSố lượng ở đây
Positive matchesmọi class, class lồng và @Bean method có toàn bộ condition thỏa52
Negative matchesmọi cái bị loại, kèm condition đã loại nó39
Exclusionscác class bạn tự loại bằng exclude hoặc spring.autoconfigure.exclude0
Unconditional classesauto-configuration không có condition nào nên luôn áp dụng6

Ba chi tiết khiến báo cáo trở nên dễ đọc thay vì đáng sợ.

Một mục không đồng nghĩa với một candidate. Ba mươi candidate tạo ra 91 mục, vì mỗi @Configuration class lồng có dòng riêng và mỗi @Bean method có condition cũng vậy. Dấu # trong tên nghĩa là một bean method: HttpEncodingAutoConfiguration#characterEncodingFilter là method, còn HttpEncodingAutoConfiguration là class.

Mỗi dòng đều nêu tên condition đã quyết định, đặt trong ngoặc ở cuối — (OnClassCondition), (OnBeanCondition), (OnPropertyCondition). Đó là tên class implementation, nên khi một dòng khiến bạn khó hiểu thì source chỉ cách một lần tìm kiếm.

Một mục negative vẫn có thể liệt kê phần đã khớp. Khi có nhiều condition và một cái thất bại, báo cáo hiện cả hai nửa. Chạy cùng project đó với --spring.servlet.encoding.enabled=false:

Text
   HttpEncodingAutoConfiguration:
      Did not match:
         - @ConditionalOnBooleanProperty (spring.servlet.encoding.enabled=true) found different value in property 'spring.servlet.encoding.enabled' (OnPropertyCondition)
      Matched:
         - @ConditionalOnClass found required class 'org.springframework.web.filter.CharacterEncodingFilter' (OnClassCondition)
         - found 'session' scope (OnWebApplicationCondition)

Hai condition thỏa, một cái trượt, và cả class bị bỏ — kèm luôn @Bean method của nó, vốn không được đánh giá một khi class đã bị loại. Số liệu của lần chạy đó: 50 positive, 40 negative, và CharacterEncodingFilter beans: [] trong registry.

Hai phần cuối ngắn nhưng đáng biết. Exclusions rỗng trừ khi bạn tự loại thứ gì đó, nên đây là cách nhanh nhất để xác nhận một exclusion đã thật sự có hiệu lực. Unconditional classes là số ít class không có condition nào nên luôn áp dụng; ở project này chúng là:

Text
Unconditional classes:
----------------------
 
    org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration
 
    org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration
 
    org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration
 
    org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration
 
    org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration
 
    org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration

Tìm ra vì sao một bean có mặt hoặc vắng mặt

Đây là phần mà phần lớn developer không bao giờ học, và nó biến "Spring Boot không cấu hình cái tôi cần" từ trò đoán mò thành một lần tra cứu hai phút.

  1. Chạy với --debug và ghi ra file. Báo cáo dài hàng trăm dòng và bạn cần tìm kiếm trong đó, không phải cuộn tay.
  2. Tìm auto-configuration theo tên. Nếu chưa biết tên, tìm theo type của bean — grep -i thymeleaf, grep -i datasource. Các mục trong báo cáo đều nêu tên class mà chúng canh giữ.
  3. Xem kết quả nằm ở phần nào. Nếu nằm dưới Positive matches thì configuration đã chạy và vấn đề nằm chỗ khác — một giá trị property, một bean bạn đã override, một URL mapping. Nếu nằm dưới Negative matches thì đọc dòng Did not match:; nó nói chính xác cái gì đang thiếu.
  4. Nếu tên đó không xuất hiện ở đâu cả, class này không phải candidate trên classpath hiện tại. Dependency mang nó đang thiếu nên không file .imports nào nêu tên nó. Đó là vấn đề của file build, không phải vấn đề cấu hình.
  5. Kiểm tra Exclusions khi một configuration bạn mong đợi lại vắng mặt mà classpath trông vẫn đúng.

Bốn kiểu thất bại ứng với bốn cách sửa: @ConditionalOnClass trượt nghĩa là thêm dependency; @ConditionalOnMissingBean trượt nghĩa là bạn (hoặc một thư viện khác) đã khai báo bean đó rồi; @ConditionalOnProperty trượt nghĩa là set property; và một cái tên không xuất hiện nghĩa là jar không có ở đó.

Còn một dòng nữa cần nhận ra. Khi startup hỏng trước lúc context sẵn sàng, Boot không in được báo cáo và nói thẳng ra:

Text
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.

Dòng đó nghĩa là báo cáo vẫn tồn tại và bạn chỉ cách nó đúng một flag.

Thứ tự áp dụng, và vì sao bean của bạn được biết trước

Các auto-configuration đôi khi phụ thuộc lẫn nhau, nên @AutoConfiguration mang theo thuộc tính sắp thứ tự. Nó được meta-annotate bằng @Configuration(proxyBeanMethods = false), @AutoConfigureBefore@AutoConfigureAfter, rồi alias chúng thành before, beforeName, afterafterName. WebMvcAutoConfiguration dùng cả hai dạng:

Java
@AutoConfiguration(after = { DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class },
        afterName = "org.springframework.boot.validation.autoconfigure.ValidationAutoConfiguration")
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@ImportRuntimeHints(WebResourcesRuntimeHints.class)
public final class WebMvcAutoConfiguration {

afterName nhận chuỗi vì class đó nằm trong một module có thể không có trên classpath — dùng class literal sẽ lỗi khi load. Thứ tự này cuối cùng nằm trong file metadata sinh tự động, cạnh các condition:

Properties
org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration.AutoConfigureAfter=org.springframework.boot.webmvc.autoconfigure.DispatcherServletAutoConfiguration,org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,org.springframework.boot.validation.autoconfigure.ValidationAutoConfiguration
org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration.AutoConfigureOrder=-2147483638

Nhưng thứ tự quan trọng nhất không phải giữa các auto-configuration với nhau — mà là giữa chúng và bạn. AutoConfigurationImportSelector là một DeferredImportSelector, và Spring xử lý deferred import sau khi mọi @Configuration class khác đã được parse. Component, @Bean method và @Import của bạn đều đã được biết trước khi condition đầu tiên của auto-configuration được đánh giá.

Đúng một sự thật đó biến @ConditionalOnMissingBean thành một lời hứa thay vì một cuộc đua. Boot nói thật khi bảo "chỉ khi bạn chưa làm", vì tới lúc nó hỏi thì câu trả lời đã an bài.

Tắt auto-configuration

Đôi khi bạn muốn loại hẳn một auto-configuration chứ không phải override nó. Có hai cách viết cho cùng một thao tác.

Trên annotation, nếu class có trên compile classpath:

DemoApplication.java
@SpringBootApplication(exclude = HttpEncodingAutoConfiguration.class)
public class DemoApplication {
 
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

Hoặc bằng property, nhận tên đầy đủ nên dùng được cho mọi trường hợp:

application.properties
spring.autoconfigure.exclude=org.springframework.boot.servlet.autoconfigure.HttpEncodingAutoConfiguration

Cả hai cho cùng kết quả, và báo cáo chứng minh điều đó — phần Exclusions không còn rỗng:

Text
Exclusions:
-----------
 
    org.springframework.boot.servlet.autoconfigure.HttpEncodingAutoConfiguration

Với exclusion đó, lần chạy báo 50 positive matches và CharacterEncodingFilter beans: []. @SpringBootApplication còn nhận excludeName cho dạng chuỗi.

Có hai kiểu hỏng đáng gặp chủ động. Loại một class không phải auto-configuration sẽ bị từ chối thẳng chứ không bị làm ngơ:

Text
java.lang.IllegalStateException: The following classes could not be excluded because they are not auto-configuration classes:
	- com.example.demo.ConditionsDemo

Còn loại bỏ thứ mà một starter thật sự cần thì hạ gục application ngay lúc khởi động. Bỏ auto-configuration của Tomcat servlet khỏi một web application:

Text
***************************
APPLICATION FAILED TO START
***************************
 
Description:
 
Web application could not be started as there was no org.springframework.boot.web.server.servlet.ServletWebServerFactory bean defined in the context.
 
Action:
 
Check your application's dependencies for a supported servlet web server.
Check the configured web application type.

Cũng thông báo đó xuất hiện nếu bạn tắt toàn bộ cơ chế bằng spring.boot.enableautoconfiguration=false, tức phương án hủy diệt và gần như không bao giờ là thứ bạn muốn. Exclude là con dao mổ; dùng nó khi auto-configuration của một thư viện thật sự sai với hoàn cảnh của bạn, còn khi chỉ muốn hành vi khác đi thì cứ khai báo bean của mình.

FAQ

Danh sách auto-configuration trong Spring Boot 4 nằm ở đâu?

Trong META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, mỗi dòng một class, và mỗi jar có đóng góp auto-configuration đều có một file như vậy. META-INF/spring.factories với key EnableAutoConfiguration là cơ chế của Boot 2; nó bị deprecated ở 2.7, gỡ bỏ ở 3.0, và không jar nào trên classpath Boot 4.1.1 còn dùng nó cho mục đích này.

Vì sao auto-configuration của tôi không được áp dụng?

Chạy với --debug và tìm class đó trong CONDITIONS EVALUATION REPORT. Dưới Negative matches, dòng Did not match: nêu tên condition đã trượt, gần như luôn là thiếu class (thêm dependency), một bean bạn đã khai báo (đó là back-off đang làm đúng việc), hoặc một property chưa set hay set sai giá trị. Nếu class không xuất hiện trong báo cáo, jar mang nó không có trên classpath.

Chạy với --debug trên production có an toàn không?

Bản thân báo cáo vô hại — nó in một lần lúc khởi động và sau đó không tốn gì. Vấn đề là --debug còn nâng một nhóm logger cốt lõi lên mức DEBUG, khiến application ồn hơn hẳn lúc chạy. Để điều tra một lần trên service đang chạy thì thường chấp nhận được; để bật vĩnh viễn thì không. Nếu chỉ cần báo cáo, debug=true cũng hành xử y hệt, nên hãy tính trước việc tắt lại.

Khác nhau giữa @Conditional và @ConditionalOn* là gì?

@Conditional là annotation của Spring Framework, nhận một Condition implementation do bạn cung cấp. Họ @ConditionalOn* là của Spring Boot: 21 annotation, mỗi cái là một @Conditional với condition viết sẵn cho một câu hỏi phổ biến. Dùng annotation của Boot khi có cái vừa vặn, và tự implement Condition khi không cái nào hợp.

Tôi có dùng được @ConditionalOnMissingBean trong @Configuration class của mình không?

Dùng được, nhưng kết quả phụ thuộc thứ tự parse nên không đáng tin. Auto-configuration được import sau cùng, đó là lý do condition về sự tồn tại bean hoạt động tốt ở đó. Trong configuration của bạn, hãy ưu tiên @ConditionalOnClass, @ConditionalOnProperty hoặc một Condition tự viết, vì chúng trả lời những câu hỏi không phụ thuộc vào thời điểm class được parse.

Làm sao tắt một auto-configuration mà không đụng tới classpath?

@SpringBootApplication(exclude = TheAutoConfiguration.class), hoặc spring.autoconfigure.exclude với tên đầy đủ. Kiểm tra phần Exclusions của báo cáo --debug để xác nhận nó có hiệu lực, và nhớ rằng loại bỏ một configuration mà cái khác phụ thuộc vào có thể khiến application không khởi động được.

Kết luận

Auto-configuration chỉ là bean, condition và registry — đúng những gì cả chương này nói tới. @EnableAutoConfiguration import một selector; selector đó hợp các file .imports từ mọi jar trên classpath thành danh sách candidate — 30 cái với một web app Boot 4.1.1 tối giản, rải trên sáu jar chứ không gom vào một; một bộ lọc metadata rẻ tiền loại ngay những cái hiển nhiên không áp dụng được; và những cái sống sót là các @Configuration class bình thường, với các annotation @ConditionalOn* lần lượt quyết định @Bean method nào được chạy. @ConditionalOnMissingBean là mảnh ghép khiến mọi thứ dễ sống chung: Boot cấu hình phần bạn chưa làm, và khai báo bean chính là cách override. Khi có chỗ nào khiến bạn bất ngờ, --debug in ra toàn bộ nhật ký quyết định kèm lý do cho từng dòng.

Vậy là khép lại Chương 1. Bạn đã biết container là gì, bean vào đó bằng cách nào, chúng được nối với nhau ra sao, sống bao lâu, và những bean bạn chưa từng viết đến từ đâu. Chương tiếp theo nói về việc chỉ dẫn cho application lúc chạy, và nó bắt đầu bằng hai file mà mọi project Spring Boot đều có: application.propertiesapplication.yml — cú pháp của chúng, khác nhau chỗ nào, và đọc giá trị ra bằng @Value.

Bài viết liên quan

[Spring Boot Basics] Validation trong Spring Boot: Bean Validation, @Valid và custom validator

Bean Validation trong Spring Boot 4.1.1 với Hibernate Validator 9.1.3, kiểm chứng bằng các lần chạy thật: spring-boot-starter-validation, @NotNull, @NotEmpty và @NotBlank khác nhau ra sao, @Size, @DecimalMin, @Digits, @Email và @Pattern trên DTO record, @Valid với @RequestBody và response 400 mặc định, object lồng nhau và list, validate @PathVariable và @RequestParam cùng cái bẫy 500 của @Validated, validation group, ValidationMessages.properties và Accept-Language, custom ConstraintValidator và constraint liên quan nhiều field, và validation ở service layer.

[Spring Boot Basics] Tài liệu API trong Spring Boot với springdoc-openapi và Swagger UI

springdoc-openapi 3.1.1 trên Spring Boot 4.1.1, kiểm chứng trên jar đang chạy: document OpenAPI 3.1 ở /v3/api-docs, Swagger UI và Try it out, những gì springdoc suy ra từ controller, DTO record và Bean Validation constraint, response nào của @RestControllerAdvice được thêm vào, @Tag, @Operation, @ApiResponse, @Parameter và @Schema trên record, bean OpenAPI và customizer toàn cục, GroupedOpenApi, property của springdoc và tắt tài liệu trong profile prod.

[Spring Boot Basics] @Configuration và @Bean trong Spring: khi nào dùng thay cho annotation stereotype

Vì sao @Component không đăng ký được một class nằm trong jar của thư viện khác, cách @Bean factory method trên @Configuration class làm thay, bảng quyết định chọn giữa hai cách, và proxyBeanMethods chứng minh bằng identity hash code thật trên Spring Boot 4.1.1 — full mode trả về một singleton dùng chung, lite mode tạo ba object riêng biệt — cùng @Import, static @Bean method và ba lỗi dựng lại kèm message thật.

[Spring Boot Basics] Logging trong Spring Boot: SLF4J, Logback, log level và ghi log ra file

Logging trong Spring Boot 4.1.1 kiểm chứng trên project thật: SLF4J là facade và Logback 1.5.38 là implementation, hai bridge jul-to-slf4j và log4j-to-slf4j, parameterised và fluent logging, ghi log exception, log level, cây logger và log group, --debug so với --trace, pattern dòng log mặc định, logging.file.name kèm rotation, logback-spring.xml với springProfile, MDC và chuyển sang Log4j2.