OLHub

Câu hỏi phỏng vấn Spring IoC/DI & vòng đời bean

Bộ đề này harvest từ 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.

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

    IoC là gì — nó đảo ngược cái gì, và IoC có đồng nghĩa với DI không?

    Junior

    IoC đảo ngược QUYỀN KIỂM SOÁT việc tạo object và nối dependency: trước đây class tự new thứ nó cần, giờ container quyết định tạo gì, khi nào, nối với ai — code của bạn chỉ khai báo. IoC và DI không đồng nghĩa: theo Fowler (2004), IoC là khái niệm rộng gồm nhiều kỹ thuật — Dependency Injection, Service Locator (ctx.getBean()), Template Method (JdbcTemplate), Event/Callback (@EventListener) — DI chỉ là MỘT hình thức. Thiếu IoC thì ba thứ vỡ: testability bằng không, coupling chặt, và lifecycle không ai quản lý.

    Inject ApplicationContext rồi gọi getBean() khắp nơi vẫn là IoC nhưng là Service Locator, không phải DI — dependency bị giấu khỏi chữ ký class, test khó hơn hẳn.
  2. 02

    DI có mấy kiểu? Vì sao constructor injection được khuyến nghị thay vì field injection?

    Junior

    Ba kiểu: constructor (khuyến nghị), setter (chỉ cho dependency optional), field (anti-pattern). Bốn lý do cơ chế cho constructor injection: field được final nên Java Memory Model đảm bảo visibility đa luồng; dependency bắt buộc fail ngay tại startup (UnsatisfiedDependencyException) thay vì NPE lúc runtime; test chỉ cần new OrderService(mock) dưới 1ms, không cần Spring context; và circular dependency bị phát hiện tại startup thay vì âm thầm. Từ Spring 4.3, class một constructor không cần @Autowired — bản thân annotation không phải anti-pattern, chỉ đặt trên field mới là.

    Constructor 8 tham số trông 'xấu' thực ra là tín hiệu God class — field injection chỉ che triệu chứng đó đi chứ không chữa; tách class mới là chữa.
  3. 03

    Spring xử lý circular dependency thế nào — và khi nào nó chịu thua?

    Mid

    Với field/setter injection, Spring tự giải bằng three-level cache trong DefaultSingletonBeanRegistry: singletonObjects (instance hoàn chỉnh), earlySingletonObjects (early reference), singletonFactories (factory tạo early reference) — cần đủ ba cấp vì early reference phải là chính AOP proxy nếu bean bị @Transactional wrap. Nó chịu thua với constructor-constructor: constructor Java là nguyên tử, không có điểm giữa chừng để expose this trước khi hoàn tất, nên không có gì đặt vào cache — app fail startup với BeanCurrentlyInCreationException, và đó là hành vi TỐT (fail-fast). Từ Boot 2.6, mặc định allow-circular-references=false — Spring chính thức coi circular là design smell.

    Fix đúng trong ~90% case là tách logic chung sang class thứ ba; @Lazy trên constructor param chỉ là band-aid — inject một CGLIB proxy để trì hoãn resolve, vòng lặp thiết kế vẫn còn nguyên.
  4. 04

    BeanFactory khác ApplicationContext thế nào? 'Singleton' của Spring thực chất là gì?

    Junior

    BeanFactory là hợp đồng tối thiểu (getBean), mặc định tạo bean LAZY; ApplicationContext không kế thừa suông mà COMPOSE một DefaultListableBeanFactory bên trong rồi delegate, cộng thêm Environment, MessageSource, event publisher, resource resolver — và tạo toàn bộ singleton EAGER lúc khởi động để fail-fast. Bên trong là hai map: beanDefinitionMap (metadata) và singletonObjects (instance cache); getBean() tra cache trước, chưa có mới tạo rồi cache lại. Vì thế "singleton" của Spring nghĩa là MỘT ENTRY trong map của MỘT container — không phải singleton pattern kiểu static getInstance().

    Hai context tạo từ cùng một config KHÔNG chia sẻ singleton — hai map riêng biệt; 'singleton per container' là câu trả lời phân biệt junior/mid.
  5. 05

    Bean lifecycle của Spring gồm những phase chính nào?

    Mid

    Trình tự cố định: constructor → populate dependency (@Autowired) → các *Aware callback → BeanPostProcessor before-init → @PostConstructafterPropertiesSet → custom init-method → BeanPostProcessor after-init — và AOP proxy được wrap ở đúng bước CUỐI này, nên mọi annotation aspect chỉ có hiệu lực sau khi bean init xong. Hai chi tiết hay bị hỏi: @PostConstruct không phải phase tách rời mà do một BeanPostProcessor (InitDestroyAnnotationBeanPostProcessor) thực thi; và exception trong @PostConstruct làm app không khởi động được — fail-fast có chủ đích. Destroy callback chỉ chạy khi shutdown êm (SIGTERM), không chạy khi SIGKILL hay crash.

    Bẫy kinh điển: constructor chạy TRƯỚC khi field @Autowired được set — đụng dependency field-inject trong constructor là NPE; constructor injection miễn nhiễm bẫy này.
  6. 06

    Bean singleton của Spring có thread-safe không?

    Junior

    Không — Spring chỉ đảm bảo MỘT instance per container, không đảm bảo gì về đồng bộ. Singleton là default vì đa số bean (service, repository) stateless: không có state thì share giữa bao nhiêu thread cũng an toàn. Vấn đề chỉ xuất hiện khi bean singleton giữ field mutable — một int requestCount++ là race condition ngay, vì hàng trăm request cùng chạy trên đúng một instance. Cách xử lý: AtomicInteger/synchronized cho counter đơn giản, hoặc đẩy state ra ngoài bean (database, Redis) — giữ bean stateless là nguyên tắc mặc định.

    Trả lời 'singleton nên thread-safe' là nhầm chiều: chính vì Spring KHÔNG lo đồng bộ nên bạn phải giữ bean stateless — thread-safety là trách nhiệm của người viết bean.
  7. 07

    Inject bean prototype vào bean singleton thì chuyện gì xảy ra?

    Mid

    Prototype semantics âm thầm biến mất: Spring chỉ gọi getBean() đúng MỘT LẦN lúc khởi tạo singleton, field từ đó giữ mãi một instance — mọi request dùng chung "prototype" đó, và Spring hoàn toàn im lặng vì về kỹ thuật đây là cách dùng hợp lệ. Fix khuyến nghị là ObjectProvider<T>: gọi provider.getObject() tường minh mỗi lần cần instance mới. @Lookup cũng được (CGLIB override method) nhưng hiếm trong code mới. Còn scoped proxy đúng cho request/session scope nhưng SAI cho prototype: mỗi method call qua proxy lấy một instance khác nhau, state không tích luỹ được — builder.add() rồi builder.build() chạy trên hai object khác nhau.

    Nhớ thêm: @PreDestroy không bao giờ chạy cho prototype — Spring không track lifecycle sau khi trao instance, caller tự cleanup.
  8. 08

    AOP proxy JDK khác CGLIB thế nào — và vì sao self-invocation làm @Transactional mất tác dụng?

    Mid

    JDK Dynamic Proxy tạo anonymous class IMPLEMENT interface (tên dạng $Proxy42, instanceof class gốc là false, bắt buộc bean có interface); CGLIB sinh SUBCLASS bytecode (OrderService$$SpringCGLIB$$0, instanceof class gốc là true, không cần interface nhưng bó tay với class/method final). Spring Boot từ 2.0 mặc định CGLIB cho mọi bean. Vì aspect sống trên proxy, this.method() trong cùng class đi thẳng vào object gốc, bỏ qua proxy — @Transactional, @Async, @PreAuthorize trên method đó mất tác dụng mà không có lỗi nào báo; ví dụ kinh điển: importAll() gọi this.importOne()REQUIRES_NEW, một dòng lỗi rollback cả 10.000 dòng vì tất cả thực chất chung một transaction.

    Fix xếp hạng: tách hai class (tốt nhất) → self-inject với @Lazy (Boot 2.6+ coi self-inject là circular reference nên bắt buộc @Lazy) → getBean() tại chỗ (service locator, tệ nhất). Kotlin còn dính thêm: class mặc định final, cần plugin allopen.
  9. 09

    BeanDefinition là gì? BeanFactoryPostProcessor khác BeanPostProcessor thế nào?

    Mid

    BeanDefinition là METADATA thuần — class, scope, lazy hay không, init/destroy method — chưa phải object; object chỉ ra đời ở bước instantiate gần cuối refresh(). Hai điểm can thiệp khác nhau về tầng: BeanFactoryPostProcessor chạy sớm, sửa được BeanDefinition (metadata) nhưng không thay object; BeanPostProcessor chạy quanh lúc init từng bean, nhận instance thật và CÓ THỂ trả về proxy thay thế — AOP, @Autowired, @PostConstruct đều là BPP. Spring tách hai pha metadata/instance chính là để Boot autoconfig và Spring Cloud tweak bean mà không đụng vào code người dùng.

    Gọi getBean() bên trong một BFPP ép Spring tạo bean quá sớm — bean đó bỏ lỡ toàn bộ BPP (không @Autowired, không @PostConstruct, không AOP proxy) và không có exception nào báo, chỉ sai hành vi âm thầm.

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.