OLHub

Câu hỏi phỏng vấn Spring Boot Auto-Configuration

Bộ đề này harvest từ module Spring Boot & Auto-Configuration của khoá Spring Core — mỗi câu link bài học đào sâu cơ chế, không dừng ở đáp án thuộc lòng.

Đề Spring15 câuJunior–Midmỗi câu ≥1 bài học
  1. 01

    @SpringBootApplication gồm những annotation nào, mỗi cái làm gì?

    Junior

    Đây là composite của ba annotation: @SpringBootConfiguration (chính là @Configuration — đánh dấu class này là nguồn bean definition), @EnableAutoConfiguration (bật cơ chế autoconfig) và @ComponentScan (quét @Component/@Service/@Repository từ package hiện tại xuống). Chỉ @EnableAutoConfiguration là thứ nghe có vẻ "magic", mà bản thân nó cũng chỉ là @Import(AutoConfigurationImportSelector.class) cộng @AutoConfigurationPackage. Ba annotation cộng lại vừa đủ để SpringApplication.run() dựng một container đầy đủ. Hệ quả thực tế hay bị hỏi tiếp: vì package của class chứa annotation được lấy làm gốc scan, đặt class main ở sub-package sẽ khiến mọi bean nằm ngoài sub-package đó không được đăng ký.

    Đọc thuộc ba cái tên nhưng không nói được @EnableAutoConfiguration thực chất chỉ import một selector — và không biết vị trí package của class main quyết định phạm vi component scan.
  2. 02

    Spring Boot có phải một framework mới không? Nó khác Spring Framework ở đâu?

    Junior

    Không phải framework mới — Spring Boot là lớp đóng gói nằm trên Spring Framework, gồm năm trụ cột: starter dependency (BOM curate version của hơn 200 thư viện), auto-configuration, embedded server, production-ready feature (Actuator, externalized config) và build plugin đóng gói executable fat jar. Nó ra đời 2014 để xoá 60-90 phút boilerplate mỗi project Spring 4 — web.xml, applicationContext.xml, Tomcat cài rời, log4j.xml. Bằng chứng rõ nhất cho "chỉ là lớp bọc": SpringApplication.run() chạy 14 bước và bước 12 chính là refresh() của Spring core, Boot chỉ wrap thêm 11 bước trước cùng 2 bước sau. Vì thế bỏ Boot không có nghĩa viết lại app: code Spring core vẫn chạy, chỉ mất phần default và đóng gói.

    Trả lời "Boot là Spring bản mới, dễ dùng hơn" mà không nêu được Boot chỉ đóng gói Spring core — bước 12 của SpringApplication.run() vẫn là refresh() quen thuộc.
  3. 03

    Spring Boot starter thực chất là gì? Nó có chứa code không?

    Junior

    Starter là một jar RỖNG CLASS — giải nén ra không có file .class nào, thứ duy nhất có ý nghĩa là pom.xml bên trong. Vai trò duy nhất của nó là liệt kê transitive dependency cho một tính năng, để bạn khai một dòng thay vì mười lăm dòng: spring-boot-starter-web kéo theo khoảng 30 jar gồm Tomcat embedded, Jackson, Spring MVC và logging stack. Version không nằm trong starter mà đến từ BOM, nên pom bên trong starter không khai version cho bất kỳ dependency nào. Điểm hay bị nhầm: starter là cơ chế Maven (gom dependency), còn auto-configuration là cơ chế Spring (tạo bean) — hai thứ độc lập, có cái này mà không có cái kia là chuyện bình thường.

    Nói "starter tự cấu hình giúp mình" — starter chỉ kéo jar về classpath, còn việc tạo bean là do autoconfig đọc classpath đó rồi quyết định.
  4. 04

    spring-boot-starter-parent khác spring-boot-dependencies thế nào?

    Junior

    spring-boot-dependencies là BOM thuần — một pom chỉ chứa dependencyManagement định nghĩa version cho hơn 200 thư viện, không kéo dependency nào vào classpath. spring-boot-starter-parent kế thừa BOM đó rồi cộng thêm Java version mặc định, UTF-8 encoding, plugin default (Surefire, Failsafe) và resource filtering — khai một dòng parent là xong. Khi project đã có corporate parent thì buộc phải dùng cách thứ hai, vì Maven chỉ cho phép một parent: import BOM bằng scope=import cùng type=pom trong dependencyManagement. Đánh đổi là mất phần Java version và plugin default, phải tự cấu hình lấy.

    Không giải thích được vì sao BOM thắng thuật toán nearest-wins của Maven — version trong dependencyManagement được đọc TRƯỚC khi Maven resolve conflict trong cây transitive, nên nó luôn thắng.
  5. 05

    Họ annotation @ConditionalOn* dùng để làm gì? Đặt nhiều cái cùng lúc thì sao?

    Junior

    Boot có 12 annotation @ConditionalOn*, mỗi cái hỏi một câu về môi trường runtime: @ConditionalOnClass/@ConditionalOnMissingClass hỏi classpath, @ConditionalOnBean/@ConditionalOnMissingBean hỏi context, @ConditionalOnProperty@ConditionalOnExpression hỏi cấu hình, số còn lại hỏi web context, resource, Java version, cloud platform và JNDI. Đặt nhiều annotation trên cùng một class là AND logic — fail một điều kiện thì cả autoconfig bị skip, không có chuyện pass một phần. @ConditionalOnProperty có thêm matchIfMissing = true để bật tính năng theo mặc định và cho user opt-out bằng property. Cả family này xây trên @Conditional của Spring core, Boot chỉ đóng gói sẵn 12 câu hỏi hay dùng nhất.

    Kể tên annotation mà không nói được chúng kết hợp theo AND — và không biết matchIfMissing là thứ quyết định tính năng bật hay tắt khi property vắng mặt.
  6. 06

    Vì sao chỉ cần khai một @Bean là config của mình thắng autoconfig?

    Junior

    Đó là back-off pattern của @ConditionalOnMissingBean: autoconfig khai bean mặc định nhưng kiểm tra trước xem context đã có bean cùng kiểu chưa, có rồi thì nhường. Nhờ vậy user override chỉ bằng một @Bean bình thường — không cần annotation đặc biệt, không cần @Primary, không đụng gì tới source của Boot. Đây chính là ranh giới giữa "opinionated" và "lock-in": Boot có default cho phần lớn use case nhưng cơ chế override là tính năng thiết kế chứ không phải hack. So với việc exclude cả autoconfig, back-off chính xác hơn hẳn: chỉ nhường ĐÚNG một bean, phần còn lại của autoconfig class vẫn giữ nguyên.

    Nhầm @ConditionalOnMissingBean với @Primary@Primary vẫn tạo cả hai bean rồi mới chọn cái ưu tiên inject, còn @ConditionalOnMissingBean không tạo bean thừa ngay từ đầu.
  7. 07

    Muốn đổi embedded Tomcat sang Jetty thì làm gì, có phải sửa code không?

    Junior

    Exclude spring-boot-starter-tomcat khỏi spring-boot-starter-web rồi thêm spring-boot-starter-jetty — không sửa một dòng Java nào. Lý do không cần sửa code: auto-configuration đọc classpath, thấy JettyServletWebServerFactory thay cho TomcatServletWebServerFactory nên tạo embedded server tương ứng, còn business code chỉ nói chuyện qua Servlet API. Pattern y hệt áp cho logging — exclude spring-boot-starter-logging rồi thêm spring-boot-starter-log4j2. Bẫy đi kèm: exclude mà quên thêm replacement thì Maven build vẫn pass, nhưng lúc SpringApplication.run() chạy Boot không tìm thấy ServletWebServerFactory và ném NoSuchBeanDefinitionException.

    Trả lời chung chung "đổi dependency là xong" mà không nêu được vì sao code không đổi (autoconfig detect classpath) lẫn hậu quả khi exclude thiếu replacement.
  8. 08

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

    Mid

    @EnableAutoConfiguration chỉ là @Import(AutoConfigurationImportSelector.class) — selector đó mới làm việc thật. Nó gọi ImportCandidates.load() để đọc file META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports từ MỌI jar trên classpath qua cơ chế classpath*: scan; mỗi dòng trong file là một fully-qualified class name, bản Boot 3.4 có 143 dòng. Quy trình gồm ba bước gọn: load danh sách, bỏ class user exclude, rồi filter qua @ConditionalOn*. Danh sách còn lại được ConfigurationClassPostProcessor parse như @Configuration class và register thành BeanDefinition — tới đây mới chỉ có metadata, object thật ra đời ở pha instantiate sau đó. Từ Boot 2.7, file imports này thay cho spring.factories cũ.

    Nói "Boot quét classpath rồi tự tạo bean" mà không biết danh sách autoconfig đến từ một file text nằm trong từng jar — đó cũng là lý do starter bên thứ ba đóng góp được autoconfig mà không cần sửa core Boot.
  9. 09

    Vì sao AutoConfigurationImportSelector phải là DeferredImportSelector?

    Mid

    DeferredImportSelector khiến selectImports() bị đẩy vào nhóm chạy SAU khi mọi @Configuration của app đã parse xong, khác với ImportSelector thường chạy cùng lượt. Thứ tự này là điều kiện sống còn của back-off: @ConditionalOnMissingBean chỉ cho kết quả đúng nếu autoconfig được evaluate sau khi bean của user đã nằm trong beanDefinitionMap. Nếu dùng ImportSelector thường, thứ tự giữa autoconfig và user config không xác định — autoconfig có thể chạy trước rồi tạo bean default dù user đã khai. Hệ quả tích cực khác: @ConditionalOnBean(DataSource.class) trong autoconfig nhìn thấy được DataSource do user khai trong @Configuration bình thường.

    Chỉ nói "autoconfig chạy sau" mà không gọi tên DeferredImportSelector, và không nối được nó với lý do @ConditionalOnMissingBean hoạt động đúng.
  10. 10

    Vì sao Boot dùng ASM bytecode reader thay vì Class.forName() khi kiểm tra @ConditionalOnClass?

    Mid

    Bước filter cần đọc @ConditionalOnClass của từng candidate, và Boot đọc bằng ASM bytecode reader chứ không load class. Ba lý do: load class sẽ chạy static initializer nên gây side effect ngoài ý muốn; nếu class trong annotation value vắng mặt thì Class.forName() ném ClassNotFoundException thay vì skip êm; và nạp 143 class cùng dependency transitive vào metaspace là lãng phí cho thứ phần lớn sẽ không dùng. ASM chỉ parse annotation table trong bytecode để lấy chuỗi tên class, rồi kiểm tra tồn tại bằng ClassLoader.getResource(...) — không load, không init. Còn một lớp tối ưu nữa: META-INF/spring-autoconfigure-metadata.properties sinh lúc build chứa condition pre-compute, loại bớt candidate trước khi cần đụng tới bytecode.

    Nghĩ Boot phải load hết 143 autoconfig rồi mới lọc — thực tế phần lớn candidate bị loại từ file metadata hoặc từ ASM, chưa bao giờ đi qua Class.forName().
  11. 11

    Thứ tự giữa các auto-configuration được quyết định thế nào, và vì sao cần thứ tự?

    Mid

    Cần thứ tự vì @ConditionalOnMissingBean phụ thuộc vào bean đã có tại thời điểm evaluate — autoconfig đến trước sẽ đăng ký bean, autoconfig đến sau phải nhường; không có ordering thì kết quả là bug không tái hiện được. Khai bằng @AutoConfigureBefore/@AutoConfigureAfter, hoặc attribute before/after của @AutoConfiguration từ Boot 2.7, hoặc @AutoConfigureOrder khi chỉ cần đẩy rất sớm hay rất muộn. Chuỗi kinh điển là DataSourceAutoConfiguration rồi tới HibernateJpaAutoConfiguration rồi JpaRepositoriesAutoConfiguration, mỗi bước xây trên bean của bước trước. Cơ chế bên dưới không phải ép chạy tuần tự: AutoConfigurationSorter gom mọi quan hệ before/after thành DAG rồi topological sort ra danh sách an toàn — khai after một class không tồn tại thì quan hệ đó bị bỏ qua, còn quan hệ vòng làm Boot ném IllegalStateException lúc startup.

    Đặt @AutoConfigureAfter lên một @Configuration thường — annotation này chỉ có nghĩa trên class @AutoConfiguration, ở chỗ khác Spring bỏ qua hoàn toàn mà không báo gì.
  12. 12

    Auto-config không kích hoạt, bean không được tạo — bạn debug thế nào?

    Mid

    Đừng đoán: bật /actuator/conditions qua management.endpoints.web.exposure.include, hoặc chạy java -jar app.jar --debug để in condition evaluation report ra console. Ba mục cần đọc là positiveMatches (autoconfig đã register kèm lý do pass), negativeMatches (bị skip kèm lý do fail, ví dụ không tìm thấy class RedisTemplate) và exclusions (bị loại qua thuộc tính exclude). Đọc negativeMatches cho biết chính xác condition nào fail nên fix đúng gốc: thiếu jar thì soi mvn dependency:tree, thiếu property thì bổ sung cấu hình. Nếu autoconfig không xuất hiện trong report luôn thì vấn đề nằm sớm hơn — file AutoConfiguration.imports chưa đúng nên class chưa bao giờ vào danh sách candidate.

    Xử lý theo kiểu thử-sai (thêm bớt dependency, exclude bừa autoconfig) thay vì đọc negativeMatches — toàn bộ dữ liệu đó đã được Spring ghi sẵn vào ConditionEvaluationReport lúc evaluate.
  13. 13

    Viết một starter nội bộ cho team thì cấu trúc gồm những gì?

    Mid

    Starter chuẩn gồm hai module: X-spring-boot-autoconfigure chứa @AutoConfiguration class cùng @ConfigurationProperties, còn X-spring-boot-starter là jar rỗng chỉ pull autoconfigure và runtime lib. Tách hai vì module autoconfigure khai runtime lib dạng optional=true (không transitive) cho khớp với @ConditionalOnClass, còn starter pull tường minh cả hai để consumer chỉ cần khai một dependency. Bắt buộc phải có file META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports liệt kê class autoconfig — thiếu nó thì annotation đầy đủ tới đâu Boot cũng không biết class tồn tại, và đây là bug im lặng phổ biến nhất. Về đặt tên, starter chính chủ dùng spring-boot-starter-X còn third-party hay nội bộ đảo lại thành X-spring-boot-starter để không gây nhầm ai là người maintain.

    Viết đủ @AutoConfiguration rồi tin là "Boot tự quét package của tôi" — Boot chỉ đọc danh sách trong file imports; verify bằng jar tf acme-starter.jar | grep AutoConfiguration.imports.
  14. 14

    Có mấy cách tắt một auto-configuration, và nên chọn cách nào?

    Mid

    Có hai mức khác nhau: @SpringBootApplication(exclude = DataSourceAutoConfiguration.class) (hoặc excludeName khi chỉ có tên chuỗi) loại HẲN autoconfig class khỏi danh sách candidate, còn khai một @Bean cùng kiểu chỉ khiến autoconfig back-off đúng bean đó. Exclude thô hơn nhiều: bạn mất luôn phần còn lại của autoconfig class — binding @ConfigurationProperties, các bean phụ, listener đi kèm. Vì vậy mặc định nên override bằng bean, chỉ exclude khi thật sự muốn autoconfig đó biến mất hoàn toàn. Kiểm chứng bằng /actuator/conditions: class bị exclude nằm ở phần exclusions, khác hẳn negativeMatches vốn là do condition fail.

    Exclude cả autoconfig chỉ để đổi một bean, rồi ngạc nhiên vì mất luôn property binding và các bean phụ mà autoconfig đó vẫn đang cung cấp.
  15. 15

    Vì sao autoconfig hay viết @ConditionalOnClass(name = "...") thay vì class literal?

    Mid

    Vì module autoconfigure phải COMPILE được ngay cả khi thư viện đó không có trên classpath của chính nó. Dùng class literal thì trình biên dịch cần phân giải được class, không có là compile fail — trớ trêu ở chỗ ý định ban đầu chỉ là "chỉ chạy khi class này có mặt". Chuỗi tên thì không tạo ràng buộc compile nào; lúc runtime Boot dùng ClassLoader.getResource(...) kiểm tra file class có tồn tại hay không rồi mới quyết định. @ConditionalOnMissingBean cũng có thuộc tính type nhận chuỗi vì đúng lý do đó — chỉ dùng class literal khi class chắc chắn luôn có mặt, ví dụ class của chính module autoconfigure.

    Cho rằng dùng chuỗi là "code bẩn" — đây là ràng buộc kỹ thuật: class literal trong annotation tạo compile dependency, còn autoconfig phải build được khi thư viện đích vắng mặt.

Topic kế cùng track: Câu hỏi phỏng vấn JPA & bài toán N+1

Trả lời trôi chảy bắt đầu từ hiểu cơ chế

Mỗi câu ở trên đều có bài học đứng sau. Học tuần tự cả khoá Spring Core & Boot để không chỉ trả lời được, mà giải thích được vì sao.