BeanFactory vs ApplicationContext — container thực sự là cái gì bên dưới
Container Spring không phải hộp đen. Bài này bóc đúng MỘT thứ: hai interface BeanFactory và ApplicationContext khác nhau ra sao, và bên dưới chúng được implement bằng data structure nào — singletonObjects map, beanDefinitionMap, và cơ chế delegation khiến ApplicationContext 'là' một BeanFactory.
TL;DR: BeanFactory là interface gốc định nghĩa hợp đồng tối thiểu của container: cho 1 tên/kiểu, trả về 1 bean (getBean). ApplicationContext không kế thừa suông mà bọc (compose) một DefaultListableBeanFactory bên trong rồi uỷ quyền (delegate) mọi thao tác bean cho nó — đồng thời cộng thêm 4 capability enterprise. Bên dưới, bean instance singleton sống trong một ConcurrentHashMap tên singletonObjects; metadata bean sống trong beanDefinitionMap. Hiểu 2 cấu trúc này là hiểu vì sao "singleton = một instance", vì sao tạo 2 context là 2 thế giới tách biệt, và vì sao getBean() lần hai luôn nhanh.
Bài Dependency Injection trả lời DI làm gì khi gặp @Autowired. Câu hỏi atomic của bài này hẹp hơn: khi SpringApplication.run() trả về một object cho bạn — object đó bên trong là cái gì? Nó tên ConfigurableApplicationContext. Ta sẽ bóc đúng nó, không lan sang refresh() hay Environment (mỗi thứ có bài riêng).
1. BeanFactory — hợp đồng tối thiểu của một container
BeanFactory là interface ở tận gốc cây phân cấp container. Nó nhỏ đến bất ngờ:
// File: org/springframework/beans/factory/BeanFactory.java (rút gọn)
public interface BeanFactory {
Object getBean(String name) throws BeansException;
<T> T getBean(Class<T> requiredType) throws BeansException;
boolean containsBean(String name);
boolean isSingleton(String name);
Class<?> getType(String name);
// ... vai method nua, tat ca deu xoay quanh "tra ve bean"
}
Toàn bộ ý nghĩa của BeanFactory nằm ở một câu: đưa cho tôi một định danh, tôi trả lại một object đã được khởi tạo và inject dependency đầy đủ. Nó che giấu việc object đó được tạo lúc nào, bằng constructor nào, dependency lấy từ đâu.
Điểm cốt lõi cần nhớ: BeanFactory mặc định lazy — bean chỉ thực sự được tạo khi getBean() gọi tới nó lần đầu. Đây là khác biệt quan trọng so với ApplicationContext (sẽ thấy ở phần 4).
BeanFactory = "máy tra cứu bean". Bạn đưa tên/kiểu, nó trả bean. Mọi thứ khác của Spring chỉ là lớp xây thêm trên hợp đồng này.
2. Implementation thật — DefaultListableBeanFactory
BeanFactory chỉ là interface. Object thật làm việc tên DefaultListableBeanFactory — đây là trái tim chứa bean của mọi app Spring (kể cả khi bạn dùng ApplicationContext, bên trong vẫn là nó). Nó giữ hai bản đồ (map) tách biệt:

beanDefinitionMap(Map<String, BeanDefinition>): bản đồ metadata — "có bean tênorderService, classOrderService, scope singleton, cầnpaymentGateway". Đây chưa phải object thật, chỉ là bản mô tả.singletonObjects(Map<String, Object>): bản đồ instance — object đã được tạo và cache lại.
Hai map này tách rời chính là lý do container chia làm 2 pha: đăng ký metadata trước, tạo instance sau. Khi bạn khai báo @Component, container mới chỉ thêm một entry vào beanDefinitionMap. Object thật chưa tồn tại.
3. getBean() bên dưới — vì sao lần hai luôn nhanh
Đây là phần cơ chế cốt lõi — và cũng là chỗ trả lời vì sao Spring chọn cache instance thay vì tạo mới mỗi lần. Khi bạn gọi getBean("orderService"), luồng thực tế trong source (AbstractBeanFactory.doGetBean) như sau:

Diễn giải:
getSingleton(name)trasingletonObjectstrước. Nếu đã có, nó trả về ngay, không tạo lại.- Nếu chưa có, container chạy
createBean(): đọcBeanDefinition, chọn constructor, resolve + inject dependency (đệ quygetBeancho từng dependency), gọi@PostConstruct. - Object vừa tạo được đặt vào
singletonObjectsrồi mới trả về.
Hệ quả trực tiếp — và đây là định nghĩa thật của "singleton scope":
Singleton trong Spring = "một instance cached trong
singletonObjectsmap", KHÔNG phải singleton pattern kiểugetInstance()static. Scope = một entry trong một map của một container cụ thể.
Vì lần hai trở đi chỉ là tra map (O(1)), nên getBean() lặp lại gần như miễn phí. Còn lần đầu đắt vì phải createBean.
// Minh hoa: 2 lan getBean tra ve CUNG object
var a = ctx.getBean(OrderService.class);
var b = ctx.getBean(OrderService.class);
System.out.println(a == b); // true -- cung 1 entry trong singletonObjects
4. ApplicationContext — bọc một BeanFactory, không phải kế thừa suông
Nhìn cây interface, bạn dễ tưởng ApplicationContext extends BeanFactory nghĩa là nó là một BeanFactory phình to. Thực tế tinh tế hơn: implementation của ApplicationContext (như AnnotationConfigApplicationContext) giữ một DefaultListableBeanFactory làm field bên trong và uỷ quyền mọi lời gọi bean cho field đó. Đây là composition + delegation, không phải kế thừa code.
// File: org/springframework/context/support/GenericApplicationContext.java (rut gon)
public class GenericApplicationContext extends AbstractApplicationContext {
private final DefaultListableBeanFactory beanFactory; // <-- BEAN THAT NAM O DAY
@Override
public Object getBean(String name) {
assertBeanFactoryActive();
return this.beanFactory.getBean(name); // <-- DELEGATE xuong field
}
public final ConfigurableListableBeanFactory getBeanFactory() {
return this.beanFactory;
}
}
Nói cách khác: ApplicationContext "là một" BeanFactory về mặt hợp đồng (implements), nhưng "có một" BeanFactory về mặt cài đặt (composition). Mọi getBean bạn gọi trên context thực ra chạy xuống DefaultListableBeanFactory bên trong.

Hai khác biệt hành vi quan trọng so với BeanFactory trần:
BeanFactory (trần) | ApplicationContext | |
|---|---|---|
| Tạo singleton | Lazy — khi getBean đầu tiên | Eager — tạo sẵn mọi singleton non-lazy lúc khởi động |
| Thêm gì | Không | 4 capability enterprise (phần 5) |
Việc ApplicationContext tạo sẵn (eager) toàn bộ singleton lúc startup chính là lý do lỗi cấu hình lộ ra ngay khi app khởi động thay vì lúc request đầu tiên — một lựa chọn thiết kế "fail fast" có chủ đích.
5. Bốn capability ApplicationContext cộng thêm
ApplicationContext bọc BeanFactory rồi cộng thêm 4 năng lực. Bài này chỉ điểm danh — mỗi cái có bài atomic riêng:
| Capability | Interface | Dùng cho |
|---|---|---|
| Environment | EnvironmentCapable | @Value("${...}"), @Profile |
| MessageSource | MessageSource | i18n messages.properties |
| Event publisher | ApplicationEventPublisher | @EventListener, custom event |
| Resource resolver | ResourcePatternResolver | @Value("classpath:...") |
Điều cần nắm ở bài này: 4 thứ trên không liên quan tới việc tra bean. Chúng là dịch vụ phụ trợ context cấp cho app. Khi đọc source thấy một class inject ApplicationContext (qua ApplicationContextAware), thường nó cần một trong 4 capability này — chứ không phải để gọi getBean.
Cơ chế bên dưới — data structure thật trong heap
Gộp lại, một ApplicationContext đang chạy, nhìn ở mức heap, là các cấu trúc sau:
AnnotationConfigApplicationContext
└─ beanFactory : DefaultListableBeanFactory
├─ beanDefinitionMap : LinkedHashMap<String, BeanDefinition> // metadata, giu thu tu khai bao
├─ singletonObjects : ConcurrentHashMap<String, Object> // instance da tao (cache singleton)
├─ earlySingletonObjects : ConcurrentHashMap<String, Object> // phuc vu circular dependency (bai sau)
└─ singletonFactories : HashMap<String, ObjectFactory> // phuc vu circular dependency (bai sau)
Ba điều rút ra từ chính các cấu trúc này:
singletonObjectslàConcurrentHashMap→ việc đọc bean an toàn đa luồng. Đó là lý do bạn có thểgetBeantừ nhiều thread mà không cần khoá.beanDefinitionMaplàLinkedHashMap→ giữ đúng thứ tự đăng ký, ảnh hưởng thứ tự tạo bean khi không có ràng buộc dependency.- Hai map
earlySingletonObjectsvàsingletonFactoriestồn tại chỉ để giải vòng lặp dependency (A cần B, B cần A) — cơ chế "three-level cache". Ta sẽ mổ nó ở bài 01 — Bean lifecycle; ở đây chỉ cần biết chúng nằm trong cùngDefaultListableBeanFactory.
Mental model gói gọn: một container = một DefaultListableBeanFactory = vài cái map. Bỏ đi lớp vỏ "magic", Spring container là một cặp Map<String, BeanDefinition> + Map<String, Object> cùng logic tra/tạo quanh chúng.
Pitfall của riêng concept này
❌ Nhầm 1 — tạo 2 context cho cùng một app, tưởng chúng chia sẻ bean:
var ctx1 = new AnnotationConfigApplicationContext(AppConfig.class);
var ctx2 = new AnnotationConfigApplicationContext(AppConfig.class);
// ctx1 va ctx2 co 2 DefaultListableBeanFactory RIENG -> 2 singletonObjects map RIENG
✅ Hai context = hai singletonObjects độc lập = hai bộ singleton khác nhau. Bean "singleton" của ctx1 và ctx2 là hai object khác nhau. Mỗi process Spring chỉ nên có một root context.
❌ Nhầm 2 — tưởng singleton Spring là singleton pattern toàn JVM:
✅ Singleton scope chỉ đảm bảo "một instance trong phạm vi một container". Hai container = hai instance. Nó là entry trong map của một BeanFactory cụ thể, không phải static getInstance().
❌ Nhầm 3 — inject ApplicationContext vào service để gọi getBean:
✅ Đó là service locator anti-pattern — xem bài 03 — IoC & DI để hiểu vì sao tránh. Inject thẳng dependency qua constructor. Chỉ dùng getBean ở test, bootstrap script, hoặc framework cần lookup động.
📚 Deep Dive
Chỉ cần đọc tên field + tên method, không cần hiểu hết implementation:
DefaultListableBeanFactory— tìm fieldbeanDefinitionMap. Đây là nơi metadata bean sống.DefaultSingletonBeanRegistry— tìm fieldsingletonObjects. Đây là cache instance singleton (và 2 map circular-dependency).AbstractBeanFactory.doGetBean— đọc đoạn đầu method: nó gọigetSingleton(beanName)trước khicreateBean. Chính là luồng "tra cache trước" ở phần 3.- Spring Framework Reference — Container Overview — phần giới thiệu BeanFactory vs ApplicationContext.
Liên hệ các bài khác
Bài này chỉ là một mảnh. Ghép với các bài khác trong course để thấy cả bức tranh container:
- Dependency Injection: vì sao không tự gọi
getBeantrong code business — bài này cho thấygetBeanthật ra làm gì bên dưới, nên hiểu rõ vì sao service locator là anti-pattern. - Bean lifecycle:
createBeanở phần 3 mở rộng thành các giai đoạn đầy đủ; 2 mapearlySingletonObjects/singletonFactoriesđược mổ ở đó (giải vòng lặp dependency). - Singleton & Prototype scope: "singleton = một entry trong
singletonObjects" — bài đó so sánh với prototype/request/session, nơi quy tắc cache thay đổi và vì sao chọn scope nào. - @Configuration, @Bean, @Component:
beanDefinitionMapđược lấp đầy từ đâu — scan stereotype so với@Beanmethod, và khi nào nên dùng cái nào.
Tóm tắt
BeanFactorylà interface gốc, hợp đồng tối thiểu: nhận định danh, trả về bean. Mặc định lazy.- Implementation thật là
DefaultListableBeanFactory, giữ 2 map:beanDefinitionMap(metadata) vàsingletonObjects(instance). getBean()trasingletonObjectstrước; có thì trả ngay (O(1)), chưa có thìcreateBeanrồi cache lại. ⇒ "singleton" = một entry cached, không phải static singleton pattern.ApplicationContextbọc mộtDefaultListableBeanFactory(composition + delegation), không kế thừa code; mọigetBeanchạy xuống field bên trong.- Khác hành vi:
ApplicationContexttạo eager mọi singleton non-lazy lúc startup (fail fast);BeanFactorytrần thì lazy. ApplicationContextcộng 4 capability (Environment, MessageSource, EventPublisher, ResourceLoader) — không liên quan việc tra bean.- Một container = một
DefaultListableBeanFactory= vài cái map. Hết magic.
Tự kiểm tra
- Q1Vì sao gọi
getBean(OrderService.class)lần thứ hai gần như miễn phí, trong khi lần đầu tốn vài mili-giây? Trả lời theo data structure bên dưới. - Q2Câu nói "
ApplicationContextlà mộtBeanFactory" đúng hay sai? Giải thích sự khác nhau giữa quan hệ implements và composition ở đây. - Q3Bạn tạo 2
AnnotationConfigApplicationContexttừ cùng mộtAppConfigtrong một process. Bean scope singleton có được chia sẻ giữa 2 context không? Vì sao? - Q4Vì sao
ApplicationContexttạo sẵn (eager) toàn bộ singleton non-lazy lúc khởi động, trong khiBeanFactorytrần thì lazy? Lợi ích thiết kế là gì? - Q5Trong
DefaultListableBeanFactory, vì saobeanDefinitionMapvàsingletonObjectsphải là hai map tách biệt thay vì gộp một?
Bài tiếp theo: refresh() — 12 bước khởi tạo container
Bài này đáng gửi cho bạn học cùng?
Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.
Bài này có giúp bạn hiểu bản chất không?
Ôn phỏng vấn
Bài này trả lời được các câu phỏng vấn sau — tự trả lời thử trước khi mở đáp án.
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên