Auto-configuration
Xem thửBạn thêm spring-boot-starter-actuator và vài property, và ứng dụng bỗng có hàng chục endpoint, bộ đo, kiểm tra sức khỏe mà bạn chưa viết dòng cấu hình nào. Ở chiều ngược lại, đồng nghiệp viết một thư viện nội bộ với auto-configuration cho AuditClient, thêm vào dự án, và không có bean nào xuất hiện: không lỗi, không cảnh báo, chỉ là NoSuchBeanDefinitionException ở nơi dùng nó. Hai chiều đó có chung một cơ chế: auto-configuration là cấu hình có điều kiện, Boot đoán các bean cần thiết từ classpath và property, và lùi lại khi bạn tự khai báo. Biết cách hỏi Boot "vì sao bean này có hoặc không có" là kỹ năng quan trọng nhất của người dùng Boot.
Bài này đo cơ chế bằng thí nghiệm trên Spring Boot 4.1.1: có bao nhiêu auto-configuration được xét và áp dụng trong một ứng dụng web nhỏ, báo cáo --debug trông thế nào, và một auto-configuration tự viết với bốn hành vi (không bật, bật, lùi lại trước bean của người dùng, bị loại trừ) cùng vị trí khai báo trong Boot 4 (nhiều tệp, theo module).
Sau bài này, bạn có thể
- Giải thích auto-configuration được tìm thấy và áp dụng thế nào, và nó nằm ở đâu trong Boot 4.
- Đọc báo cáo
--debugđể biết vì sao một cấu hình có hoặc không áp dụng. - Viết auto-configuration riêng với điều kiện đúng và đăng ký qua tệp
AutoConfiguration.imports. - Ghi đè hoặc loại trừ một auto-configuration.
Các đầu vào của auto-configuration
Cách đọc hình
Có ba đầu vào. Classpath cho biết bạn có thư viện nào (có driver JDBC hay không). Property cho biết bạn muốn gì (spring.datasource.url). Bean đã có cho biết bạn đã tự cấu hình gì. Boot tổng hợp ba thứ này qua các điều kiện @ConditionalOn... để ra tập bean cuối cùng.
Auto-configuration nằm ở đâu trong Boot 4
Trong Spring Boot 4, các auto-configuration được chia theo module, ví dụ DataSourceAutoConfiguration nằm trong module JDBC (package org.springframework.boot.jdbc.autoconfigure). Danh sách ứng viên nằm trong tệp META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports của mỗi jar; tệp spring.factories không còn dùng cho mục đích này. Lab đếm các tệp đó trên classpath (webmvc, validation, actuator, aspectj):
AUTOCONFIG imports files on classpath=12 candidate classes=92
AUTOCONFIG sample jars: [spring-boot-autoconfigure-4.1.1.jar, spring-boot-jackson-4.1.1.jar, spring-boot-tomcat-4.1.1.jar, spring-boot-http-converter-4.1.1.jar, spring-boot-webmvc-4.1.1.jar, spring-boot-servlet-4.1.1.jar, spring-boot-validation-4.1.1.jar, spring-boot-micrometer-metrics-4.1.1.jar]
Mười hai tệp trong mười hai jar, tổng cộng 92 lớp ứng viên (thêm tệp của lab ở thí nghiệm dưới đây là 13 tệp và 93 lớp): mỗi module (jackson, tomcat, webmvc, validation...) mang auto-configuration của riêng nó. Đó là điều khác với Boot 3, nơi gần như tất cả nằm trong một jar spring-boot-autoconfigure. Hệ quả thực tế: thêm một starter thường thêm một module và các auto-configuration của nó; và tên package auto-configuration đã đổi (ví dụ org.springframework.boot.jdbc.autoconfigure), nên chuỗi tên đầy đủ trong spring.autoconfigure.exclude hay @SpringBootApplication(exclude = ...) cần được kiểm tra khi nâng cấp.
Luồng đánh giá điều kiện
Đang tải sơ đồ…
Hình trên là luồng đơn giản hóa; trong thực tế mỗi cấu hình dùng tổ hợp điều kiện riêng của nó. Điều đáng nhớ: auto-configuration luôn đứng sau cấu hình của bạn. Spring xử lý các cấu hình của người dùng trước, rồi mới đến auto-configuration, nên điều kiện "chưa có bean" (@ConditionalOnMissingBean) thấy được bean bạn đã khai báo.
Thí nghiệm 1: báo cáo điều kiện
Boot ghi mọi quyết định vào ConditionEvaluationReport. Lab đọc nó từ một ứng dụng servlet:
AUTOCONFIG evaluated=81 applied=35 skipped=46
AUTOCONFIG some applied: [DispatcherServletAutoConfiguration, HealthEndpointAutoConfiguration, JacksonAutoConfiguration, JacksonEndpointAutoConfiguration, ValidationAutoConfiguration, WebMvcAutoConfiguration, WebMvcHealthEndpointExtensionAutoConfiguration, WebMvcObservationAutoConfiguration]
AUTOCONFIG some skipped: [AuditAutoConfiguration, AuditEventsEndpointAutoConfiguration]
Trong một ứng dụng web nhỏ (đã có auto-configuration của lab, 93 ứng viên), 81 lớp auto-configuration cấp cao nhất được xét, 35 được áp dụng và 46 bị bỏ qua. Số "ứng viên" lớn hơn số "được xét" vì một số bị lọc từ sớm. Con số này cho thấy nửa số cấu hình có sẵn không áp dụng cho ứng dụng của bạn: đó là điều khiến Boot "tự cấu hình" mà không tạo bean thừa. Khi chạy với --debug, báo cáo được in ra theo hai nhóm:
CONDITIONS EVALUATION REPORT
Positive matches:
-----------------
AopAutoConfiguration matched:
- @ConditionalOnBooleanProperty (spring.aop.auto=true) matched (OnPropertyCondition)
AopAutoConfiguration.AspectJAutoProxyingConfiguration matched:
- @ConditionalOnClass found required class 'org.aspectj.weaver.Advice' (OnClassCondition)
Negative matches:
-----------------
AopAutoConfiguration.AspectJAutoProxyingConfiguration.JdkDynamicAutoProxyConfiguration:
Did not match:
- @ConditionalOnBooleanProperty (spring.aop.proxy-target-class=false) did not find property 'spring.aop.proxy-target-class' (OnPropertyCondition)
(Hai dòng gạch dưới tiêu đề nhóm trong báo cáo gốc là dấu gạch ngang.) Cách đọc: Positive matches liệt kê cấu hình đã áp dụng cùng điều kiện đã thỏa; Negative matches liệt kê cấu hình bị bỏ qua cùng điều kiện không thỏa, thường là một dòng nói rõ thiếu lớp nào hay thiếu property nào. Ví dụ trên cũng giải thích một điều đã đo ở bài giới thiệu AOP: proxy JDK không bật vì không có spring.aop.proxy-target-class=false, còn proxy lớp (CGLIB) là mặc định. Khi một bean "không có", đừng đoán: chạy --debug, tìm tên cấu hình trong Negative matches và đọc dòng điều kiện không thỏa.
Thí nghiệm 2: viết auto-configuration riêng
Lab dựng một auto-configuration cho AuditClient, bật bằng property audit.enabled=true, lùi lại nếu người dùng tự khai báo bean:
@AutoConfiguration
@ConditionalOnClass(AuditClient.class)
@ConditionalOnProperty(prefix = "audit", name = "enabled", havingValue = "true")
@EnableConfigurationProperties(AuditProperties.class)
public class AuditAutoConfiguration {
@Bean
@ConditionalOnMissingBean
AuditClient auditClient(AuditProperties properties) {
return new AuditClient(properties.endpoint());
}
}
Lớp được khai báo trong tệp META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports (một tên lớp đầy đủ mỗi dòng): lab.l20.AuditAutoConfiguration. Auto-configuration không được component scan quét; nó chỉ được nạp qua tệp imports. Bốn kịch bản:
AUTOCONFIG audit.enabled not set: audit client beans=0 (OnPropertyCondition -> NO match: @ConditionalOnProperty (audit.enabled=true) did not find property 'enabled')
AUTOCONFIG audit.enabled=true -> beans=1 endpoint=http://localhost:9000/audit
AUTOCONFIG audit.enabled=true and user-defined bean -> beans=1 endpoint=user-defined
AUTOCONFIG property override -> endpoint=https://audit.internal/v1
AUTOCONFIG excluded via property -> beans=0
- Không đặt property: 0 bean, và báo cáo nêu lý do chính xác (
did not find property 'enabled'): đây là chuyện "thư viện thêm vào mà không có bean" ở đầu bài. audit.enabled=true: 1 bean với giá trị mặc định củaAuditProperties.- Người dùng tự khai báo
AuditClient: vẫn đúng 1 bean và là bean của người dùng (endpoint=user-defined):@ConditionalOnMissingBeanlàm auto-configuration lùi lại. - Ghi đè bằng property:
audit.endpointđổi giá trị mặc định mà không sửa mã. - Loại trừ:
spring.autoconfigure.exclude=lab.l20.AuditAutoConfigurationtắt cấu hình dù property bật.
Đang tải Animation…
Thử và quan sát
Dự đoán trước: classpath có driver cơ sở dữ liệu và bạn tự khai báo một bean DataSource; Boot tạo một DataSource nữa hay dùng bean của bạn? Chạy hai biến thể rồi so sánh chỉ số "DataSource được dùng". Quy tắc ghi nhớ: auto-configuration luôn đứng sau cấu hình của bạn.
Điều khiển auto-configuration
java -jar app.jar --debug
Để loại trừ một cấu hình trong mã:
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
public class ShopApplication { }
Hoặc bằng property, hữu ích khi lớp không có trên classpath (chuỗi tên không cần nhập khẩu lớp):
spring.autoconfigure.exclude=org.springframework.boot.jdbc.autoconfigure.DataSourceAutoConfiguration
Có những quy tắc khi viết auto-configuration cho thư viện của bạn:
- Dùng
@AutoConfigurationthay cho@Configuration, đặt tên lớp vào tệp imports, đừng đặt nó trong package được component scan của ứng dụng (nó sẽ bị nạp sớm và không điều kiện). - Luôn gắn điều kiện:
@ConditionalOnClass(lớp cần thiết),@ConditionalOnProperty(công tắc),@ConditionalOnMissingBean(lùi lại). - Gom cấu hình vào
@ConfigurationPropertiescó kiểu thay vì đọc từng property (bài Application Properties). - Dùng
@AutoConfiguration(after = ..., before = ...)khi thứ tự giữa các auto-configuration quan trọng; lab này không đo thứ tự nên hãy đọc tài liệu và kiểm thử riêng. - Tách thành module
-autoconfigurevà starter khi phát hành thư viện cho nhiều ứng dụng dùng.
Kiểm thử auto-configuration với ApplicationContextRunner
Bài này kiểm tra auto-configuration bằng cách khởi động cả ứng dụng, tốn thời gian và khó cô lập. Spring Boot có công cụ riêng: ApplicationContextRunner dựng một context nhỏ chỉ với những cấu hình bạn nêu, nhanh và có thể thử từng tổ hợp. Lab chạy năm kịch bản trên AuditAutoConfiguration:
final ApplicationContextRunner runner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AuditAutoConfiguration.class));
runner.run(ctx -> ...);
runner.withPropertyValues("audit.enabled=true").run(ctx -> ...);
runner.withPropertyValues("audit.enabled=true").withUserConfiguration(UserDefined.class).run(ctx -> ...);
runner.withPropertyValues("audit.enabled=true").withClassLoader(new FilteredClassLoader(AuditClient.class)).run(ctx -> ...);
default: beans=0
audit.enabled=true: beans=1 endpoint=http://localhost:9000/audit
override endpoint: endpoint=https://x/v1
user-defined bean wins: beans=1 endpoint=user-defined name=mine
class missing (FilteredClassLoader): context started=true failure=null
Năm kịch bản tương ứng đúng năm điều kiện của cấu hình: công tắc tắt, công tắc bật, ghi đè thuộc tính, lùi lại trước bean người dùng (@ConditionalOnMissingBean), và thiếu lớp (@ConditionalOnClass). FilteredClassLoader che một lớp khỏi classpath để mô phỏng thư viện không có mặt: kết quả là context vẫn khởi động (failure=null) mà không có bean, đúng ý đồ "cấu hình lùi lại khi thiếu thư viện" (lab dùng ctx.getBeansOfType thay vì khẳng định cụ thể vì phần này chỉ in số liệu; trong dự án thật dùng AssertJ assertThat(ctx).hasSingleBean(AuditClient.class)). Mỗi kịch bản chạy trong vài chục mili giây, nên bạn có thể thử mọi tổ hợp mà không tốn một lần khởi động ứng dụng đầy đủ.
Thứ tự auto-configuration và cái bẫy của @ConditionalOnBean
@ConditionalOnMissingBean và @ConditionalOnBean nhìn vào các bean đã được đăng ký tại thời điểm đánh giá, nên kết quả phụ thuộc thứ tự xử lý các cấu hình. Lab có hai trường hợp:
AfterFirst (after = First) liệt kê ngược thứ tự: secondMarker present = true
AaaNeedsLate (@ConditionalOnBean tên lateMarker) và ZzzLate (cung cấp lateMarker), không khai báo thứ tự:
needsLate present = false (cả khi liệt kê ZzzLate trước)
Với @AutoConfiguration(after = First.class), dù bạn liệt kê AfterFirst trước First, Spring sắp lại: First được xử lý trước nên AfterFirst thấy bean firstMarker (secondMarker có mặt). Còn khi không khai báo thứ tự, bộ sắp xếp mặc định theo bảng chữ cái của tên lớp: AaaNeedsLate được xử lý trước ZzzLate, lúc đó lateMarker chưa tồn tại, điều kiện @ConditionalOnBean sai và needsLate không bao giờ được tạo, ngay cả khi liệt kê ngược. Đổi tên lớp làm đổi hành vi: một lỗi thật sự khó tìm. Quy tắc: bất kỳ cấu hình nào dùng @ConditionalOnBean (hoặc phụ thuộc bean của cấu hình khác) phải khai báo @AutoConfiguration(after = ...) rõ ràng; chỉ dùng @ConditionalOnMissingBean trong chính cấu hình đó để lùi lại trước người dùng thì an toàn vì cấu hình của người dùng luôn được xử lý trước auto-configuration.
@ConditionalOnProperty khớp khi nào
Công tắc property là điều kiện hay dùng nhất, và cũng là nơi có hành vi dễ nhầm nhất. Lab thử ba khai báo với mười giá trị:
@ConditionalOnProperty("f.plain")
@ConditionalOnProperty(name = "f.value", havingValue = "yes")
@ConditionalOnProperty(name = "f.optout", matchIfMissing = true)
(không đặt gì) : plain=false havingYes=false optOut=true
f.plain=true : plain=true
f.plain=false : plain=false
f.plain=anything : plain=true
f.plain= : plain=true (giá trị rỗng vẫn tính là khớp)
f.value=yes / YES : havingYes=true (so sánh không phân biệt hoa thường)
f.optout=false : optOut=false
f.optout=0 / true : optOut=true
Quy tắc đã đo: (1) không có havingValue, điều kiện khớp với mọi giá trị trừ false (không phân biệt hoa thường): anything, giá trị rỗng, 0 đều khớp; chỉ chữ false mới tắt. Vì vậy f.optout=0 không tắt công tắc, một cạm bẫy cho người nghĩ 0 là sai. (2) havingValue so sánh chuỗi không phân biệt hoa thường (YES khớp yes). (3) matchIfMissing = true biến công tắc thành "bật mặc định, chỉ tắt khi đặt false" (optOut=true khi không có thuộc tính). Quy tắc thiết kế: tính năng nhạy cảm (gửi dữ liệu ra ngoài) mặc định tắt (không có matchIfMissing, như audit.enabled ở bài này); tính năng an toàn mặc định bật dùng matchIfMissing = true; luôn tài liệu hóa giá trị hợp lệ vì 0 và rỗng khớp bất ngờ. Dùng @ConditionalOnBooleanProperty (Boot 4) khi muốn khớp chặt chỉ true/false; hãy đọc tài liệu về hành vi chính xác của nó, lab này chưa đo.
Quyết định thiết kế và đánh đổi
| Quyết định | Khuyến nghị | Đánh đổi |
|---|---|---|
| Chẩn đoán bean thiếu | --debug, ConditionEvaluationReport | Báo cáo dài, cần lọc |
| Thay thế mặc định | Tự khai báo bean cùng kiểu | Phải hiểu bean thay thế gì |
| Tắt một cấu hình | spring.autoconfigure.exclude | Bean phụ thuộc cũng mất |
| Cấu hình thư viện | @AutoConfiguration + điều kiện + imports | Phải viết tệp imports |
| Công tắc | @ConditionalOnProperty | Mặc định tắt dễ bị quên |
| Lùi lại | @ConditionalOnMissingBean | Phụ thuộc kiểu bean |
| Tên đầy đủ khi loại trừ | Kiểm tra khi nâng cấp | Tên package đổi ở Boot 4 |
| Quét thành phần | Không đặt auto-config trong cây quét | Cần tách package |
Sổ tay xử lý sự cố
- Triệu chứng: bean thư viện không tồn tại. Nguyên nhân: điều kiện không thỏa (thiếu property, thiếu lớp). Sửa:
--debug, đọc Negative matches. - Triệu chứng: tự khai báo bean mà vẫn thấy hành vi cũ. Nguyên nhân: bean khác tên nhưng khác kiểu, hoặc điều kiện kiểu bean. Sửa: đúng kiểu mà
@ConditionalOnMissingBeankiểm tra. - Triệu chứng: loại trừ rồi ứng dụng không khởi động. Nguyên nhân: bean khác phụ thuộc cấu hình bị loại. Sửa: cung cấp bean thay thế.
- Triệu chứng: tên loại trừ không còn hiệu lực sau nâng cấp. Nguyên nhân: package đổi (module hóa Boot 4). Sửa: kiểm tra tên đầy đủ.
- Triệu chứng: auto-configuration áp dụng quá sớm không điều kiện. Nguyên nhân: đặt trong cây component scan. Sửa: chuyển sang package ngoài cây quét, đăng ký bằng imports.
- Triệu chứng: hai bean cùng kiểu. Nguyên nhân: auto-config không lùi vì thiếu
@ConditionalOnMissingBean. Sửa: thêm điều kiện. - Triệu chứng: khởi động chậm do nhiều cấu hình. Nguyên nhân: starter thừa. Sửa: bỏ starter không dùng.
- Triệu chứng: đoán vì sao bean thiếu. Nguyên nhân: thiếu công cụ. Sửa:
--debugtrước mọi suy đoán.
Hiệu năng, bảo mật và vận hành
- Boot đánh giá khoảng trăm cấu hình mỗi lần khởi động (81 trong lab); chi phí chủ yếu là kiểm tra lớp trên classpath, nhỏ so với tạo bean.
- Auto-configuration kéo theo endpoint, bộ đo và bộ lọc mà bạn không thấy trong mã: kiểm tra những gì được bật (bài Actuator) trước khi ra môi trường thật.
- Báo cáo
--debuglộ tên thư viện và cấu hình nội bộ; không bật ở môi trường thật và đừng đăng log chưa lọc lên kênh công khai. - Đừng bật công tắc auto-configuration mặc định mở cho tính năng nhạy cảm (gửi dữ liệu ra ngoài); mặc định tắt, bật rõ ràng.
- Thêm test
ApplicationContextRunnercho auto-configuration của bạn: bốn kịch bản như lab.
Bài thực hành
- Chạy ứng dụng với
--debugvà tìm một cấu hình trong Positive matches và một trong Negative matches; ghi điều kiện của từng cái. - Đếm số tệp
AutoConfiguration.importsvà số lớp ứng viên trên classpath của bạn. - Viết
AuditAutoConfigurationvới@ConditionalOnClass,@ConditionalOnProperty,@ConditionalOnMissingBeanvà khai báo trong tệp imports. - Kiểm tra bốn kịch bản: không bật, bật, người dùng tự khai báo bean, loại trừ bằng property.
- Thêm
@ConfigurationPropertiescó giá trị mặc định và ghi đè bằng property. - Xác nhận báo cáo ghi đúng lý do khi
audit.enabledthiếu.
Tiêu chí hoàn thành: bước 2 cho vài chục đến một trăm lớp; bước 4 cho 0, 1, 1 (bean của người dùng), 0; bước 5 cho endpoint mặc định rồi giá trị ghi đè; bước 6 có thông điệp did not find property 'enabled'. Gợi ý nếu bước 3 không áp dụng: kiểm tra tệp imports nằm đúng META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports và lớp ghi đủ tên package.
Tự kiểm tra
- Boot tìm danh sách auto-configuration ở đâu, và Boot 4 khác Boot 3 thế nào?
@ConditionalOnMissingBeanlàm gì trong một starter?- Làm sao biết một auto-configuration cụ thể có được áp dụng không?
- Vì sao không đặt auto-configuration trong package được quét?
@ConditionalOnPropertykhông thấy property thì kết quả là gì?- Loại trừ một auto-configuration có thể gây hậu quả gì?
- Vì sao nên có
@ConfigurationPropertiestrong auto-configuration? - Vì sao báo cáo
--debugkhông nên bật ở môi trường thật?
Gợi ý đáp án. (1) Các tệp AutoConfiguration.imports trong từng jar; Boot 4 chia thành nhiều module nên có nhiều tệp (12 trong lab). (2) Cho phép bean của người dùng thay bean mặc định. (3) Chạy với --debug và đọc condition evaluation report, hoặc ConditionEvaluationReport. (4) Nó sẽ được nạp như cấu hình thường, sớm và không điều kiện. (5) Điều kiện không thỏa nên cấu hình bị bỏ qua, không có lỗi. (6) Bean khác phụ thuộc vào nó có thể thiếu và ứng dụng không khởi động. (7) Cho phép cấu hình có kiểu, giá trị mặc định và ghi đè bằng property. (8) Nó lộ tên thư viện và cấu hình nội bộ.
Điều cần nhớ
- Auto-configuration là cấu hình có điều kiện, nạp qua
AutoConfiguration.imports(nhiều tệp trong Boot 4). - Bean của bạn luôn thắng bean mặc định nhờ
@ConditionalOnMissingBean. --debuglà công cụ chẩn đoán đầu tiên; đọc dòng điều kiện không thỏa trong Negative matches.- Thư viện tự viết auto-configuration với công tắc property mặc định tắt cho tính năng nhạy cảm.
- Kiểm tra tên đầy đủ khi loại trừ sau nâng cấp.
Học liệu chọn lọc
- Spring Boot Reference: Auto-configuration
- Spring Boot Reference: Creating Your Own Auto-configuration
- Spring Boot Reference: Condition Annotations
- Spring Boot Reference: Testing Auto-configuration
- Spring Boot Reference: Troubleshooting (Condition Evaluation Report)
- Spring Boot API: ConditionEvaluationReport
- Java SE API (Oracle): javax.sql.DataSource
- Sách: Spring Boot in Action (Craig Walls).