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:
flowchart TB
subgraph DLBF["DefaultListableBeanFactory"]
direction TB
BDM["beanDefinitionMap<br/>Map name to BeanDefinition<br/>(METADATA: class, scope, dependencies)"]
SO["singletonObjects<br/>Map name to Object<br/>(INSTANCE: object da tao)"]
end
Scan["@Component scan / @Bean"] -->|"dang ky metadata"| BDM
BDM -->|"createBean tu metadata"| SObeanDefinitionMap(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:
flowchart TB
G["getBean(name)"] --> DG["doGetBean(name)"]
DG --> GS{"getSingleton(name)<br/>co trong singletonObjects?"}
GS -->|"CO"| Ret["return instance da cache<br/>(O(1), khong tao moi)"]
GS -->|"CHUA"| CB["createBean(name, definition)<br/>chon constructor, resolve dependencies, inject"]
CB --> Add["dat vao singletonObjects[name]"]
Add --> RetDiễ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.
flowchart TB
Caller["ctx.getBean(OrderService.class)"] --> AC
subgraph AC["AnnotationConfigApplicationContext"]
direction TB
Cap["4 capability:<br/>Environment, MessageSource,<br/>EventMulticaster, ResourceLoader"]
BF["beanFactory:<br/>DefaultListableBeanFactory"]
end
AC -->|"delegate getBean"| BF
BF --> Maps["singletonObjects + beanDefinitionMap"]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 04 — 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 02 — 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.▸
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.Lần đầu: singletonObjects chưa có entry cho bean này, nên doGetBean phải gọi createBean — chọn constructor, resolve và inject từng dependency (đệ quy getBean), gọi @PostConstruct. Đây là phần tốn thời gian.
Object vừa tạo được đặt vào singletonObjects (một ConcurrentHashMap).
Lần hai trở đi: getSingleton(name) tra map thấy có ngay nên trả về object cached. Đây là một phép tra HashMap cỡ O(1), không tạo mới gì cả. Đó cũng là định nghĩa thật của singleton scope: "một instance được cache trong map của container".
Q2Câu nói "ApplicationContext là một BeanFactory" đúng hay sai? Giải thích sự khác nhau giữa quan hệ implements và composition ở đây.▸
ApplicationContext là một BeanFactory" đúng hay sai? Giải thích sự khác nhau giữa quan hệ implements và composition ở đây.Đúng về hợp đồng (implements): ApplicationContext implements interface BeanFactory, nên bạn gọi getBean trên nó được.
Nhưng về cài đặt thì là composition: implementation (vd GenericApplicationContext) giữ một field beanFactory kiểu DefaultListableBeanFactory, và mỗi getBean chỉ uỷ quyền (delegate) xuống field đó: return this.beanFactory.getBean(name).
Tức nó "là một" BeanFactory về mặt kiểu, nhưng "có một" BeanFactory về mặt object. Hệ quả thực tế: getBeanFactory() trả về chính cái DefaultListableBeanFactory nội bộ đó — nơi bean thật sự sống.
Q3Bạn tạo 2 AnnotationConfigApplicationContext từ cùng một AppConfig trong một process. Bean scope singleton có được chia sẻ giữa 2 context không? Vì sao?▸
AnnotationConfigApplicationContext từ cùng một AppConfig trong một process. Bean scope singleton có được chia sẻ giữa 2 context không? Vì sao?Không. Mỗi ApplicationContext bọc một DefaultListableBeanFactory riêng, nghĩa là có một singletonObjects map riêng.
Singleton scope chỉ đảm bảo "một instance trong phạm vi một container". Hai container = hai map = hai instance khác nhau cho cùng một định nghĩa bean.
Đây là lý do bug "tại sao state không đồng bộ" xuất hiện khi vô tình tạo 2 context: hai bên thao tác trên hai bộ singleton độc lập. Mỗi process nên chỉ có một root context.
Q4Vì sao ApplicationContext tạo sẵn (eager) toàn bộ singleton non-lazy lúc khởi động, trong khi BeanFactory trần thì lazy? Lợi ích thiết kế là gì?▸
ApplicationContext tạo sẵn (eager) toàn bộ singleton non-lazy lúc khởi động, trong khi BeanFactory trần thì lazy? Lợi ích thiết kế là gì?ApplicationContext chủ động instantiate mọi singleton non-lazy ngay khi khởi tạo (trong pha tạo container), thay vì đợi getBean đầu tiên.
Lợi ích là fail fast: nếu một bean thiếu dependency, sai cấu hình, hoặc @PostConstruct ném lỗi, app không khởi động được — lỗi lộ ra ngay lúc deploy, không phải lúc người dùng gửi request đầu tiên vào lúc 2 giờ sáng.
Đánh đổi: thời gian startup lâu hơn (phải tạo hết bean trước). Nếu cần hoãn tạo một bean nặng, đánh dấu @Lazy để quay lại hành vi lazy cho riêng bean đó.
Q5Trong DefaultListableBeanFactory, vì sao beanDefinitionMap và singletonObjects phải là hai map tách biệt thay vì gộp một?▸
DefaultListableBeanFactory, vì sao beanDefinitionMap và singletonObjects phải là hai map tách biệt thay vì gộp một?Vì chúng giữ hai loại dữ liệu ở hai pha khác nhau: beanDefinitionMap giữ metadata (class, scope, dependency cần) có ngay sau khi scan/parse config; singletonObjects giữ instance đã tạo, chỉ xuất hiện sau khi createBean.
Tách rời cho phép container làm việc theo 2 pha: đăng ký toàn bộ definition trước (biết tất cả bean sẽ có), rồi mới instantiate. Nhờ đó các BeanFactoryPostProcessor có thể chỉnh sửa metadata (vd đổi scope, set lazy) trước khi bất kỳ object nào ra đời.
Nếu gộp một map, sẽ không phân biệt được "đã biết về bean" với "đã tạo bean" — mất luôn pha can thiệp metadata, vốn là nền của autoconfig và rất nhiều cơ chế Spring.
Bài tiếp theo: refresh() — 12 bước khởi tạo container
Bài này có giúp bạn hiểu bản chất không?
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