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.
01
IoC là gì — nó đảo ngược cái gì, và IoC có đồng nghĩa với DI không?
JuniorIoC đảo ngược QUYỀN KIỂM SOÁT việc tạo object và nối dependency: trước đây class tự
newthứ 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ý.InjectApplicationContextrồi gọigetBean()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.02
DI có mấy kiểu? Vì sao constructor injection được khuyến nghị thay vì field injection?
JuniorBa 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
finalnê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ầnnew 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.03
Spring xử lý circular dependency thế nào — và khi nào nó chịu thua?
MidVớ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ị@Transactionalwrap. Nó chịu thua với constructor-constructor: constructor Java là nguyên tử, không có điểm giữa chừng để exposethistrước khi hoàn tất, nên không có gì đặt vào cache — app fail startup vớiBeanCurrentlyInCreationException, và đó là hành vi TỐT (fail-fast). Từ Boot 2.6, mặc địnhallow-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;@Lazytrê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.04
BeanFactory khác ApplicationContext thế nào? 'Singleton' của Spring thực chất là gì?
JuniorBeanFactorylà hợp đồng tối thiểu (getBean), mặc định tạo bean LAZY;ApplicationContextkhông kế thừa suông mà COMPOSE mộtDefaultListableBeanFactorybê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ểustatic 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.05
Bean lifecycle của Spring gồm những phase chính nào?
MidTrình tự cố định: constructor → populate dependency (
@Autowired) → các*Awarecallback → BeanPostProcessor before-init →@PostConstruct→afterPropertiesSet→ 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:@PostConstructkhông phải phase tách rời mà do một BeanPostProcessor (InitDestroyAnnotationBeanPostProcessor) thực thi; và exception trong@PostConstructlà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.06
Bean singleton của Spring có thread-safe không?
JuniorKhô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/synchronizedcho 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.07
Inject bean prototype vào bean singleton thì chuyện gì xảy ra?
MidPrototype 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ọiprovider.getObject()tường minh mỗi lần cần instance mới.@Lookupcũ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ồibuilder.build()chạy trên hai object khác nhau.Nhớ thêm:@PreDestroykhông bao giờ chạy cho prototype — Spring không track lifecycle sau khi trao instance, caller tự cleanup.08
AOP proxy JDK khác CGLIB thế nào — và vì sao self-invocation làm @Transactional mất tác dụng?
MidJDK Dynamic Proxy tạo anonymous class IMPLEMENT interface (tên dạng
$Proxy42,instanceofclass gốc là false, bắt buộc bean có interface); CGLIB sinh SUBCLASS bytecode (OrderService$$SpringCGLIB$$0,instanceofclass gốc là true, không cần interface nhưng bó tay với class/methodfinal). 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,@PreAuthorizetrê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ọithis.importOne()có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.09
BeanDefinition là gì? BeanFactoryPostProcessor khác BeanPostProcessor thế nào?
MidBeanDefinitionlà 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ốirefresh(). 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ọigetBean()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.