ApplicationContext và BeanFactory — container hoạt động ra sao bên dưới
BeanFactory là interface gốc, ApplicationContext extend với enterprise features (event, i18n, environment, resource). Bài này bóc 2 interface, refresh() 12 bước, BeanDefinition format, Environment + PropertySource ordering, Resource abstraction, parent-child context, và vì sao @SpringBootApplication chỉ là 3 annotation.
Bài 02 đã giải thích DI làm gì khi gặp @Autowired. Câu hỏi tiếp theo: cái thực thể đứng sau làm việc đó là gì? Khi bạn gọi SpringApplication.run(App.class, args), một object trở về tay bạn. Object đó tên ConfigurableApplicationContext. Trong nó là toàn bộ "hồ chứa" bean và logic resolve dependency.
Bài này bóc 7 thứ: (1) BeanFactory — interface gốc đơn giản, (2) ApplicationContext — extend BeanFactory với enterprise features, (3) refresh() — method 12 bước biến cấu hình thành runtime container, (4) BeanDefinition — dạng metadata bean, (5) Environment + PropertySource — externalized config, (6) Resource abstraction — đọc file/classpath/URL thống nhất, (7) parent-child context. Hiểu xong, bạn đọc stack trace Spring lúc startup không còn hoang mang.
1. Hai interface — BeanFactory và ApplicationContext
Spring có 2 interface chính cho IoC container, xếp theo độ phức tạp:
classDiagram
BeanFactory <|-- HierarchicalBeanFactory
BeanFactory <|-- ListableBeanFactory
HierarchicalBeanFactory <|-- ApplicationContext
ListableBeanFactory <|-- ApplicationContext
EnvironmentCapable <|-- ApplicationContext
MessageSource <|-- ApplicationContext
ApplicationEventPublisher <|-- ApplicationContext
ResourcePatternResolver <|-- ApplicationContext
ApplicationContext <|-- ConfigurableApplicationContext
class BeanFactory {
+getBean(name)
+containsBean(name)
+isSingleton(name)
+getType(name)
}
class ApplicationContext {
<<interface>>
}
class ConfigurableApplicationContext {
+refresh()
+close()
+registerShutdownHook()
}| Interface | Gì cho ai? |
|---|---|
BeanFactory | Container "tối giản" — chỉ biết tạo bean theo definition, resolve dependency, return bean qua getBean(). Đủ cho app embedded nhỏ. |
ApplicationContext | Container "đầy đủ" — BeanFactory + 4 capability mở rộng: environment, message source (i18n), event publisher, resource resolver. Đây là interface bạn dùng 99% thời gian. |
ConfigurableApplicationContext | Sub-interface bạn dùng để gọi refresh(), close(), registerShutdownHook() — controller-level. |
Trong code Spring Boot, SpringApplication.run() trả về ConfigurableApplicationContext. Trong test bạn thường nhận ApplicationContext. Plain old BeanFactory hiếm khi gặp trừ khi đọc source Spring.
Cần "Spring Container"? → dùng ApplicationContext. Cần lifecycle method (refresh/close)? → dùng ConfigurableApplicationContext. Plain BeanFactory chỉ gặp khi đọc source Spring hoặc tối ưu memory cực đoan.
1.1 4 capability mà ApplicationContext thêm vào BeanFactory
Đây là điểm quan trọng — vì mọi feature enterprise của Spring đều xây trên 1 trong 4 capability này:
| Capability | Interface | Thấy ở đâu trong Spring |
|---|---|---|
| Environment | EnvironmentCapable | @Value("${...}"), @Profile, application.properties |
| MessageSource | MessageSource | messages.properties i18n, MessageSource.getMessage() |
| Event publisher | ApplicationEventPublisher | @EventListener, ApplicationListener, custom event |
| Resource resolver | ResourcePatternResolver | @Value("classpath:data.json"), ClassPathResource, glob pattern classpath*:META-INF/*.properties |
Khi bạn đọc Spring source và thấy ApplicationContextAware được inject vào 1 class, class đó dùng 1 trong 4 capability này — không phải để gọi getBean().
2. Implementation cụ thể của ApplicationContext
ApplicationContext là interface — bạn không tự new nó. Spring có 4-5 implementation chính, chọn theo cách bạn config bean:
| Implementation | Khi nào dùng |
|---|---|
ClassPathXmlApplicationContext | Cấu hình bằng XML trên classpath. Legacy (Spring 1-3 era). |
FileSystemXmlApplicationContext | Cấu hình XML trên file system. Cũng legacy. |
AnnotationConfigApplicationContext | Cấu hình bằng class Java có @Configuration + @ComponentScan. Standard cho Spring không Boot. |
AnnotationConfigServletWebServerApplicationContext | Spring Boot web servlet (default). Tự động khởi embedded Tomcat/Jetty. |
AnnotationConfigReactiveWebServerApplicationContext | Spring Boot WebFlux reactive. |
GenericApplicationContext | Programmatic — bạn tự register bean qua API. Dùng cho test phức tạp. |
StaticApplicationContext | Test thuần, programmatic register. Legacy. |
Spring Boot 3 default là AnnotationConfigServletWebServerApplicationContext (web app) hoặc AnnotationConfigApplicationContext (CLI app). Bạn không chọn trực tiếp — SpringApplication.run() tự deduce qua classpath:
- Có
spring-boot-starter-web→ web servlet context. - Có
spring-boot-starter-webflux→ reactive context. - Không có cả 2 → plain context (CLI app).
3. Tạo container thủ công — không Boot
Để hiểu container, đôi khi tạo nó không qua Spring Boot giúp thấy rõ. Đoạn code sau là "Spring tối thiểu":
@Configuration
@ComponentScan(basePackages = "com.olhub.demo")
public class AppConfig {
@Bean
public DataSource dataSource() {
return new HikariDataSource(/* ... */);
}
}
public class Manual {
public static void main(String[] args) {
try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
var orderService = ctx.getBean(OrderService.class);
orderService.placeOrder(/* ... */);
}
}
}
Phân tích:
AnnotationConfigApplicationContext(AppConfig.class)— tạo context và đọc class config.- Constructor tự động gọi
refresh()— biến config thành runtime container. ctx.getBean(OrderService.class)— lookup bean qua type (Spring 3+ hỗ trợ generic-aware).try-with-resources— tự gọiclose()cuối block.close()trigger destroy callback (@PreDestroy), shutdown bean.
Spring Boot làm chính xác 3 thứ này, chỉ khác: thay class config bằng class có @SpringBootApplication, và gói toàn bộ vào SpringApplication.run() để thêm logging, banner, environment setup, embedded server.
4. refresh() — 12 bước biến config thành container
refresh() là method quan trọng nhất của container. Source ở AbstractApplicationContext.refresh() — nếu cmd-click vào nó trong IDE, bạn thấy đoạn này:
// File: org/springframework/context/support/AbstractApplicationContext.java
public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
prepareRefresh(); // 1
ConfigurableListableBeanFactory bf = obtainFreshBeanFactory(); // 2
prepareBeanFactory(bf); // 3
try {
postProcessBeanFactory(bf); // 4
invokeBeanFactoryPostProcessors(bf); // 5
registerBeanPostProcessors(bf); // 6
initMessageSource(); // 7
initApplicationEventMulticaster(); // 8
onRefresh(); // 9 — start web server o day (Boot)
registerListeners(); // 10
finishBeanFactoryInitialization(bf); // 11 — instantiate non-lazy singletons
finishRefresh(); // 12 — publish ContextRefreshedEvent
} catch (BeansException ex) {
destroyBeans();
cancelRefresh(ex);
throw ex;
}
}
}
12 bước, phân thành 3 giai đoạn:
4.1 Giai đoạn 1 — chuẩn bị BeanFactory (bước 1-4)
- prepareRefresh — clear cache, validate environment property bắt buộc (qua
getEnvironment().validateRequiredProperties()). - obtainFreshBeanFactory — load bean definition từ config class / XML / scanner. Bean definition là metadata, chưa instantiate.
- prepareBeanFactory — set ClassLoader, register
ApplicationContextAwareProcessor(cho phép bean implementApplicationContextAwaređể nhận context). - postProcessBeanFactory — hook cho subclass (web context register
ServletContextAwareProcessorở đây).
Sau 4 bước, container đã có danh sách tất cả bean cần tạo, dạng definition. Ví dụ: "có bean tên orderService, class com.olhub.OrderService, scope singleton, dependencies [paymentGateway, emailService]".
4.2 Giai đoạn 2 — biến đổi definition (bước 5-6)
- invokeBeanFactoryPostProcessors — chạy
BeanFactoryPostProcessor(BFPP). BFPP có thể modify bean definition trước khi instantiate. Ví dụ:PropertySourcesPlaceholderConfigurerresolve${...}trong@Value.ConfigurationClassPostProcessorxử lý@Configuration(parse@Beanmethod). - registerBeanPostProcessors — register
BeanPostProcessor(BPP). BPP can thiệp trên bean instance (sau bean được tạo, trước/sau init). Ví dụ:AutowiredAnnotationBeanPostProcessorxử lý@Autowired.CommonAnnotationBeanPostProcessorxử lý@PostConstruct/@PreDestroy.
Phân biệt quan trọng: BFPP modify metadata, BPP modify instance. Tên giống nhau nhưng vai trò khác.
4.3 Giai đoạn 3 — instantiate (bước 7-12)
- initMessageSource — i18n bean cho
MessageSource. - initApplicationEventMulticaster — event bus cho
ApplicationEventPublisher. - onRefresh — hook subclass. Spring Boot start embedded Tomcat ở đây.
- registerListeners — đăng ký
ApplicationListenerđã khai báo. - finishBeanFactoryInitialization — instantiate tất cả singleton non-lazy. Đây là bước nặng nhất. Mỗi bean: chọn constructor → resolve dep → call constructor → BPP wrap → call
@PostConstruct→ lưu cache. - finishRefresh — clear cache, publish
ContextRefreshedEvent. Giai đoạn này coi như xong.
flowchart TB
Start["SpringApplication.run()"]
P1["1-4: Prepare BeanFactory<br/>(load definitions)"]
P2["5-6: BFPP modify defs<br/>BPP register"]
P3a["7-10: i18n, events, listeners<br/>+ start Tomcat (Boot)"]
P3b["11: Instantiate singletons<br/>(99% thoi gian)"]
P3c["12: Publish ContextRefreshedEvent"]
Done["Container ready"]
Start --> P1 --> P2 --> P3a --> P3b --> P3c --> Done
style P3b fill:#fef3c799% thời gian startup nằm ở bước 11 — instantiate hàng trăm singleton. Đây là lý do app Spring khởi động trong 3-15 giây, không phải milisecond. Boot 3.3+ có CDS (Class Data Sharing) giúp giảm bước 11 xuống ~50%.
4.4 Application events trong vòng đời container
Spring publish 6 event chính trong vòng đời container, theo thứ tự:
| Event | Khi nào | Use case |
|---|---|---|
ApplicationStartingEvent | Bắt đầu run(), trước khi xử lý args | Log/metric "app starting" |
ApplicationEnvironmentPreparedEvent | Environment đã build, chưa tạo ApplicationContext | Modify env trước khi context build |
ApplicationContextInitializedEvent | Context đã tạo nhưng chưa load bean definition | Inject programmatic bean definition |
ApplicationPreparedEvent | Context refresh xong nhưng chưa published | Custom init trước listener khác |
ContextRefreshedEvent | Bước 12 của refresh() | Phổ biến nhất — bean ready, có thể start side-effect |
ApplicationReadyEvent | App hoàn toàn ready (sau cả CommandLineRunner/ApplicationRunner) | Mark "app live" cho health check, smoke test |
ContextClosedEvent | close() được gọi | Pre-shutdown cleanup ngoài bean |
Đăng ký listener:
@Component
public class StartupListener {
@EventListener(ApplicationReadyEvent.class)
public void onReady(ApplicationReadyEvent event) {
log.info("App fully ready at {}", Instant.now());
// warm up cache, ping downstream...
}
}
ApplicationReadyEvent là điểm tốt để chạy logic "after app start" — bean đã init, server đã bind port, runner đã chạy.
5. BeanDefinition — metadata của bean
BeanDefinition là đại diện metadata của bean, không phải bean instance. Nó giữ:
public interface BeanDefinition {
String getBeanClassName(); // class name
String getScope(); // singleton, prototype, ...
boolean isLazyInit(); // @Lazy
String[] getDependsOn(); // @DependsOn
String getInitMethodName(); // init-method
String getDestroyMethodName(); // destroy-method
int getRole(); // ROLE_APPLICATION, ROLE_SUPPORT, ROLE_INFRASTRUCTURE
ConstructorArgumentValues getConstructorArgumentValues();
MutablePropertyValues getPropertyValues();
// ... 30+ method khac
}
Khi container scan thấy @Component, nó tạo 1 BeanDefinition với beanClassName, scope, lazyInit từ annotation, lưu vào BeanDefinitionRegistry. Chưa instantiate — đó là bước 11.
BFPP có thể modify BeanDefinition trước khi instantiate:
@Component
public class CustomizeDefinitions implements BeanFactoryPostProcessor {
public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
BeanDefinition def = bf.getBeanDefinition("orderService");
def.setLazyInit(true); // bien orderService thanh lazy
}
}
Đây là cách Spring Cloud, Boot autoconfig "tweak" definition mà không phải sửa annotation. Hiểu khái niệm này giúp bạn debug khi 1 bean có behavior bất ngờ — ai đó đã modify definition qua BFPP.
6. Environment và PropertySource — externalized config
Environment là 1 trong 4 capability của ApplicationContext. Nó manage 2 thứ:
- Properties (
PropertySource) — value từapplication.properties, env var, command line, ... - Profiles —
dev/prod/test, kiểm tra quaenv.acceptsProfiles("prod").
6.1 PropertySource ordering — ưu tiên từ cao đến thấp
Khi resolve \${db.url}, Spring Boot tra theo thứ tự:
flowchart TB
A["1. Default properties<br/>(SpringApplication.setDefaultProperties)"]
B["2. @TestPropertySource<br/>(only in test)"]
C["3. Devtools properties<br/>(only when devtools active)"]
D["4. Command line args<br/>--db.url=..."]
E["5. SPRING_APPLICATION_JSON<br/>env var"]
F["6. ServletConfig init params"]
G["7. ServletContext init params"]
H["8. JNDI"]
I["9. Java System Properties<br/>-Ddb.url=..."]
J["10. OS Environment Variables<br/>DB_URL=..."]
K["11. application-{profile}.properties<br/>(profile-specific)"]
L["12. application.properties<br/>(default)"]
M["13. @PropertySource on @Configuration"]
N["14. Default properties"]
D -->|higher priority| L
style D fill:#fef3c7
style J fill:#fef3c7
style L fill:#d1fae5Ý nghĩa: command line override mọi thứ, env var override file, profile override default. Đây là contract chuẩn để config behavior khác nhau giữa dev/staging/prod mà không phải build lại.
Pitfall thực tế:
# application.properties:
db.url=jdbc:postgresql://localhost/dev
# Run:
java -jar app.jar --db.url=jdbc:postgresql://prod-db/app
# CLI override -> dung prod-db, KHONG phai localhost
# K8s config:
env:
- name: DB_URL
value: jdbc:postgresql://prod/app
# DB_URL env var → spring resolve thanh db.url (relax binding)
Spring Boot có relax binding: DB_URL env var → match với property db.url (uppercase + underscore → lowercase + dot). Cùng với prefix SPRING_PROFILES_ACTIVE → spring.profiles.active. Đây là cơ chế chuẩn cho deployment K8s/Docker.
6.2 @Value và SpEL trên property
@Service
public class OrderService {
@Value("${db.url}") private String dbUrl;
@Value("${db.poolSize:10}") private int poolSize; // default 10 neu khong co
@Value("#{systemProperties['user.home']}") private String userHome; // SpEL
@Value("${app.allowedOrigins}") private List<String> allowedOrigins; // CSV
}
${...} resolve qua Environment. #{...} là SpEL — expression mạnh hơn (truy cập system properties, gọi method, evaluate expression).
@ConfigurationProperties là cách hiện đại hơn — bind nhóm property thành object:
@ConfigurationProperties(prefix = "app")
public record AppProps(String name, int maxConnections, List<String> allowedOrigins) {}
// Trong application.properties:
// app.name=OLHub
// app.max-connections=100
// app.allowed-origins[0]=https://olhub.org
Module 02 sẽ đào sâu @ConfigurationProperties — đây chỉ là preview.
7. Resource abstraction — đọc file thống nhất
Resource interface đại diện cho file/classpath/URL/in-memory. Lý do: bạn không phải nhớ FileInputStream vs getResourceAsStream vs URL.openStream — Spring trừu tượng hoá.
@Service
public class TemplateLoader {
@Value("classpath:templates/welcome.html")
private Resource welcomeTemplate;
@Value("file:/etc/app/config.json")
private Resource configFile;
@Value("https://example.com/data.json")
private Resource remoteData;
public String load(Resource r) throws IOException {
try (var reader = new BufferedReader(new InputStreamReader(r.getInputStream(), UTF_8))) {
return reader.lines().collect(joining("\n"));
}
}
}
3 prefix chính:
| Prefix | Resource type | Ví dụ |
|---|---|---|
classpath: | Trên classpath (jar hoặc target/classes) | classpath:templates/index.html |
file: | File system absolute | file:/etc/app/config.json |
https: / http: | URL remote | https://example.com/data.json |
| Không prefix | Tuỳ context — thường file relative | data.json |
Pattern matching với classpath*::
@Component
public class PluginLoader {
@Autowired
private ResourcePatternResolver resolver;
public List<Resource> findPlugins() throws IOException {
return Arrays.asList(resolver.getResources("classpath*:META-INF/plugins/*.json"));
}
}
classpath*: (có dấu sao) tìm trên toàn bộ classpath, kể cả nhiều jar — đây là cơ chế Spring Boot dùng để gom file spring.factories từ mọi starter jar.
8. Parent-child context (hierarchical context)
Spring cho phép lồng container: 1 child context có thể tham chiếu bean của parent context. Use case chính: Spring MVC.
flowchart TB
Root["Root Context<br/>(spring.xml hoac SpringApplication)"]
MVC["DispatcherServlet Context<br/>(controller, view resolver)"]
BeanS["@Service / @Repository<br/>OrderService, UserRepository"]
BeanC["@Controller<br/>OrderController"]
Root --> BeanS
MVC --> BeanC
MVC -.parent.-> Root
style Root fill:#fef3c7
style MVC fill:#d1fae5Trong Spring MVC truyền thống (không Boot):
- Root context: load bởi
ContextLoaderListener— chứa service, repository, datasource. - DispatcherServlet context (child): chứa controller, view resolver. Có thể inject bean của parent.
Spring Boot đơn giản hoá — chỉ 1 root context cho cả service + controller. Pattern parent-child chỉ gặp trong:
- Code Spring không Boot (legacy).
- Spring Cloud Bootstrap context (load config server trước app).
- Test phức tạp với multiple context.
Bạn có thể bỏ qua parent-child cho ứng dụng Boot thông thường — biết để khi gặp legacy không hoang mang.
9. @SpringBootApplication — chỉ là 3 annotation
Một sự thật ngạc nhiên: @SpringBootApplication không phải annotation đặc biệt. Source của nó:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration // = @Configuration + marker
@EnableAutoConfiguration // bat auto-config
@ComponentScan(...) // scan package chua class nay tro xuong
public @interface SpringBootApplication { ... }
3 annotation hợp lại:
| Annotation | Tác dụng |
|---|---|
@SpringBootConfiguration | Marker cho Spring Boot biết "đây là entry config class". Internally extend @Configuration — nghĩa là class này có thể chứa @Bean method. |
@EnableAutoConfiguration | Trigger Spring Boot autoconfig. Spring đọc file META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports từ tất cả starter, register conditional bean. Bài Module 02 sẽ đào sâu. |
@ComponentScan | Scan package chứa class này (và sub-package) để tìm @Component/@Service/@Repository/@Controller. Đây là lý do file App.java thường để ở root package. |
Nếu bạn tách 3 annotation ra, code chạy y hệt:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
Hệ quả: nhiều dự án legacy thấy 3 annotation tách rời, không phải lỗi — chỉ là phiên bản chưa rút gọn.
9.1 Tuỳ biến @ComponentScan
Default scan mọi class có stereotype trong package + sub-package. Cấu hình thêm:
@SpringBootApplication(
scanBasePackages = {"com.olhub", "com.partner.lib"}, // scan multiple package
exclude = {DataSourceAutoConfiguration.class} // tat 1 autoconfig cu the
)
public class App { ... }
// Hoac chi tiet hon:
@ComponentScan(
basePackages = "com.olhub",
includeFilters = @ComponentScan.Filter(MyMarker.class), // them annotation custom
excludeFilters = @ComponentScan.Filter(Deprecated.class) // bo class @Deprecated
)
Filter mode:
ANNOTATION(default): match annotation type.ASSIGNABLE_TYPE: match class hoặc subclass.ASPECTJ: match AspectJ pointcut expression.REGEX: match regex tên class.CUSTOM: implementTypeFilterinterface.
Hiếm khi cần custom filter — default đủ.
10. Lookup bean — cách hiện đại
API getBean() có 4 overload, ưu tiên dùng theo thứ tự:
// 1. Theo type (KHUYEN NGHI):
OrderService os = ctx.getBean(OrderService.class);
// 2. Theo type + name (khi co nhieu bean cung type):
PaymentGateway p = ctx.getBean("stripePayment", PaymentGateway.class);
// 3. Theo name (yeu - mat type safety):
Object obj = ctx.getBean("orderService");
// 4. Generic-aware (Spring 4+):
ObjectProvider<OrderService> provider = ctx.getBeanProvider(OrderService.class);
provider.ifAvailable(os -> os.placeOrder(...)); // null-safe
Trong code business không nên gọi getBean() — đó là service locator (anti-pattern, bài 02). Chỉ dùng getBean trong:
- Test code khi cần verify container trạng thái.
- Bootstrap code (init script chạy 1 lần lúc startup).
- Plugin/extension framework cần dynamic lookup.
11. Pitfall tổng hợp
❌ Nhầm 1: New ApplicationContext 2 lần cho cùng app.
var ctx1 = new AnnotationConfigApplicationContext(AppConfig.class);
var ctx2 = new AnnotationConfigApplicationContext(AppConfig.class);
✅ Mỗi process Spring chỉ 1 root context. 2 context = 2 bộ singleton độc lập, dễ gây bug "tại sao state không sync".
❌ Nhầm 2: Inject ApplicationContext vào service business để gọi getBean.
✅ Inject thẳng dependency cần thiết qua constructor. Service locator anti-pattern.
❌ Nhầm 3: Quên try-with-resources khi tự new context.
var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
// app chay nhung khong bao gio close → @PreDestroy khong chay → connection leak
✅ Dùng try (var ctx = ...) hoặc gọi ctx.close() cuối lifecycle.
❌ Nhầm 4: Cho rằng bean tạo lazy by default.
✅ Singleton non-lazy by default → tạo tại bước 11 của refresh(). Lazy phải khai báo @Lazy (bean lazy chỉ tạo khi lookup lần đầu).
❌ Nhầm 5: Đặt @SpringBootApplication ở sub-package, không scan được bean ở root.
com.olhub
├── domain/
│ └── OrderService.java // @Service
├── api/
│ └── App.java // @SpringBootApplication (sai vi tri)
✅ Đặt App.java ở root package com.olhub. @ComponentScan mặc định scan từ package của class chứa nó tro xuống — đặt sub-package sẽ miss bean ở root và package song song.
❌ Nhầm 6: Set property qua System property nhưng kỳ vọng ghi đè env var.
DB_URL=env-value
java -Ddb.url=sysprop-value -jar app.jar
# resolve thanh sysprop-value (system prop ưu tiên hơn env var)
✅ Đọc thứ tự PropertySource — command line > system prop > env var > application.properties. Nếu cần env var win, set trực tiếp env var, không set system prop.
❌ Nhầm 7: Đọc file qua new File("config.json") thay vì Resource abstraction.
String content = Files.readString(Path.of("config.json")); // chi work voi file system, KHONG voi classpath
✅ Dùng @Value("classpath:config.json") Resource r hoặc ResourceLoader. Code chạy được cả khi đóng gói trong jar (resource trong jar không phải file thông thường).
12. 📚 Deep Dive Spring Reference
Reference docs nên đọc:
- Spring Framework Reference — The IoC Container — section đầu giới thiệu BeanFactory vs ApplicationContext.
- Spring Framework Reference — Container Extension Points — chi tiết về BeanFactoryPostProcessor và BeanPostProcessor — 2 extension point quan trọng nhất của container.
- Spring Framework Reference — Annotation-based Configuration —
@Configuration,@ComponentScan. - Spring Framework Reference — Environment Abstraction — Environment, PropertySource, profile.
- Spring Framework Reference — Resources — Resource interface, classpath/file/URL prefix.
- Spring Boot Reference — Externalized Configuration — bảng PropertySource ordering chính thức.
Source chính chủ để đọc khi muốn đi sâu:
AbstractApplicationContext.refresh()— searchpublic void refresh()— chính là method 12 bước. Đọc 1 lần đủ thấy "Spring không phải magic".SpringApplication.run()— Spring Boot wrapper, gọirefresh()cuối cùng.ConfigurationClassPostProcessor— BFPP xử lý@Configuration, parse@Bean.
Ghi chú: đọc source chỉ cần đọc tên method và Javadoc, không cần hiểu implementation chi tiết. Mục tiêu là build mental model — biết nơi nào trong source xử lý cái gì.
13. Tóm tắt
BeanFactorylà interface gốc tối giản;ApplicationContextextend với i18n, event, environment, resource — đây là interface dùng 99% thời gian.ConfigurableApplicationContextthêm method lifecycle (refresh,close).- 4 capability ApplicationContext thêm:
Environment,MessageSource,ApplicationEventPublisher,ResourcePatternResolver— mọi feature enterprise xây trên 1 trong 4. - Spring chọn implementation cụ thể qua classpath: web servlet, reactive, hoặc plain.
refresh()có 12 bước chia 3 giai đoạn: prepare definitions → modify (BFPP) + register processors → instantiate singletons + publish event.- BFPP modify bean definition (metadata). BPP modify bean instance (object). Tên giống nhưng vai trò khác.
- 6+ application event xảy ra theo thứ tự —
ApplicationReadyEventlà điểm tốt cho "after start" logic. BeanDefinitionlà metadata, không phải bean instance. BFPP có thể modify trước instantiate.Environmentquản properties + profiles. PropertySource có thứ tự ưu tiên: command line > env var > application.properties. Spring Boot relax binding:DB_URL→db.url.Resourceabstraction:classpath:,file:,https:đều dùng cùng API.- Parent-child context: legacy hoặc Spring MVC truyền thống — Boot 1 context duy nhất.
@SpringBootApplication=@SpringBootConfiguration+@EnableAutoConfiguration+@ComponentScan. Không có magic.@ComponentScanmặc định scan từ package chứa class — đặt entry class ở root package.- Trong code business, không gọi
getBean()— service locator anti-pattern.
14. Tự kiểm tra
Q1Bạn debug app Spring Boot khởi động chậm 30 giây. Dựa vào 12 bước của refresh(), bạn nên nghi ngờ bước nào trước? Vì sao?▸
refresh(), bạn nên nghi ngờ bước nào trước? Vì sao?Nghi bước 11 — finishBeanFactoryInitialization (instantiate singleton non-lazy). 99% thời gian startup nằm ở đây vì mỗi bean phải: resolve dependency, gọi constructor, BPP wrap (có thể tạo proxy), gọi @PostConstruct (đôi khi connect DB/cache).
Cách verify: bật log logging.level.org.springframework=DEBUG hoặc dùng Spring Boot 3 spring.application.startup-tracking + Actuator /startup endpoint để thấy thời gian từng bean.
Nguyên nhân thường gặp tại bước 11:
- Bean có
@PostConstructgọi remote service chậm (DB ping, schema migration). - Hibernate validate schema lớn.
- JIT chưa warm up — không phải Spring vấn đề, nhưng đo nhằm vào bean.
Nghi cuối: bước 9 (onRefresh) — Tomcat embedded start. Hiếm khi chậm trừ khi config SSL phức tạp.
Q2Khác biệt giữa BeanFactoryPostProcessor (BFPP) và BeanPostProcessor (BPP) là gì? Cho ví dụ thực tế mỗi loại trong Spring core.▸
BeanFactoryPostProcessor (BFPP) và BeanPostProcessor (BPP) là gì? Cho ví dụ thực tế mỗi loại trong Spring core.Khác biệt cốt lõi: BFPP modify bean definition (metadata), BPP modify bean instance (object đã tạo).
Time: BFPP chạy trước bất kỳ bean nào instantiate (bước 5). BPP chạy quanh từng bean khi bean được instantiate (bước 11).
- BFPP ví dụ:
PropertySourcesPlaceholderConfigurer— replace${...}trong@Valuebằng giá trị property thực tế.ConfigurationClassPostProcessor— parse@Configurationclass, register@Beanmethod thành definition. - BPP ví dụ:
AutowiredAnnotationBeanPostProcessor— inject dependency vào field/method có@Autowired.CommonAnnotationBeanPostProcessor— gọi@PostConstruct/@PreDestroy.AnnotationAwareAspectJAutoProxyCreator— wrap bean với AOP proxy nếu có aspect match.
Memo: nếu bạn cần rewrite metadata (đổi class, scope, dep) → BFPP. Nếu bạn cần rewrite/wrap instance (proxy, inject extra) → BPP.
Q3Tại sao đặt class App với @SpringBootApplication ở sub-package thay vì root package có thể gây bean không được scan? Cho ví dụ cụ thể.▸
App với @SpringBootApplication ở sub-package thay vì root package có thể gây bean không được scan? Cho ví dụ cụ thể.@ComponentScan (bao trong @SpringBootApplication) mặc định scan package chứa class annotated và mọi sub-package. Đặt App ở sub-package thì root package và package song song bị bỏ qua.
Ví dụ:
com.olhub/
domain/
OrderService.java (@Service)
api/
App.java (@SpringBootApplication)
payment/
StripePayment.java (@Service)App scan com.olhub.api → trong đó không có bean nào. OrderService và StripePayment bị miss → startup throw NoSuchBeanDefinitionException khi resolve dependency.
Fix 2 cách:
- Move
App.javalên rootcom.olhub. Đây là chuẩn. - Khai báo scope rộng hơn:
@SpringBootApplication(scanBasePackages = "com.olhub")— nhưng workaround, không khuyến nghị.
Q4App của bạn đọc db.url. Bạn có 4 nguồn config đặt giá trị: (1) application.properties (jdbc:postgresql://localhost), (2) env var DB_URL=jdbc:postgresql://staging, (3) JVM arg -Ddb.url=jdbc:postgresql://prod1, (4) command line --db.url=jdbc:postgresql://prod2. Giá trị nào win? Vì sao?▸
db.url. Bạn có 4 nguồn config đặt giá trị: (1) application.properties (jdbc:postgresql://localhost), (2) env var DB_URL=jdbc:postgresql://staging, (3) JVM arg -Ddb.url=jdbc:postgresql://prod1, (4) command line --db.url=jdbc:postgresql://prod2. Giá trị nào win? Vì sao?Win: (4) command line argument → jdbc:postgresql://prod2.
Thứ tự PropertySource trong Spring Boot (cao đến thấp):
- Command line args (
--key=value) - System properties (
-Dkey=valueJVM arg) - OS environment variables (
KEY=value) application-{profile}.propertiesapplication.properties
Nên dù cả 4 nguồn cùng có giá trị, command line ưu tiên cao nhất → win.
Nguyên tắc thiết kế: command line override mọi thứ. Đó là contract chuẩn để config khác nhau giữa môi trường mà không build lại jar.
Pitfall thực tế: nếu CI/CD set env var DB_URL nhưng deploy script vô tình truyền command line --db.url=... hardcoded, command line win → bug "tại sao env var không có effect". Cách debug: log environment.getPropertySources() tại startup, in ra source nào active.
Q5Đoạn sau làm gì? Khi nào in gì? Có bug gì?@Configuration
public class AppConfig {
@Bean
public Thing a() { return new Thing("a"); }
@Bean
public Thing a() { return new Thing("a-dup"); }
}
var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(ctx.getBean("a", Thing.class).name);
▸
@Configuration
public class AppConfig {
@Bean
public Thing a() { return new Thing("a"); }
@Bean
public Thing a() { return new Thing("a-dup"); }
}
var ctx = new AnnotationConfigApplicationContext(AppConfig.class);
System.out.println(ctx.getBean("a", Thing.class).name);Code này không compile. Java cấm 2 method cùng signature trong 1 class — public Thing a() trùng nhau → compile error trước khi đến Spring.
Nếu đổi tên 2 method (vd a() và aDup()) thì cả 2 đều register với bean name = method name. Lookup "a" trả về bean từ method a(), lookup "aDup" trả về bean kia. In ra "a".
Nếu cố tình 2 bean cùng name (qua @Bean(name = "a") trên cả 2 method khác nhau): Spring throw BeanDefinitionOverrideException tại startup từ Spring Boot 2.1+ (default spring.main.allow-bean-definition-overriding=false).
Bài học: bean name unique trong context. Khi @Bean không khai báo name, Spring dùng method name.
Q6Đoạn sau khác đoạn nào? Đâu là kiểu Resource hay dùng nhất trong Spring Boot app, vì sao?@Value("classpath:templates/email.html") Resource a;
@Value("file:/etc/app/email.html") Resource b;
@Value("https://cdn.example.com/email.html") Resource c;
@Value("classpath*:META-INF/plugin/*.json") Resource[] d;
▸
@Value("classpath:templates/email.html") Resource a;
@Value("file:/etc/app/email.html") Resource b;
@Value("https://cdn.example.com/email.html") Resource c;
@Value("classpath*:META-INF/plugin/*.json") Resource[] d;a(classpath:): resource trong jar/classes của app. Đường dẫn relative đến classpath root. Hay dùng nhất — template, schema, default config — tất cả đóng gói cùng app.b(file:): file system absolute. Hay dùng cho config mounted từ Docker volume hoặc K8s ConfigMap.c(https:): URL remote. Hiếm dùng — phụ thuộc network startup.d(classpath*:): dấu sao = scan toàn bộ classpath kể cả nhiều jar. TrảResource[]. Hay dùng cho plugin architecture — mỗi jar plugin contribute fileMETA-INF/plugin/*.json, app gom tất cả.
Vì sao classpath: hay dùng nhất:
- Resource đóng gói trong jar — deploy 1 artifact, không phụ thuộc file system bên ngoài.
- Test thuận tiện — IDE chạy với target/classes, deploy chạy với jar — cùng API.
- Immutable — không bị edit ngoài ý muốn.
Khi nào dùng file:? Khi config có thể thay đổi runtime mà không rebuild jar — vd K8s mount ConfigMap vào /etc/app/. Spring Cloud Config thường dùng pattern này.
Q7Vì sao @SpringBootApplication phải bao gồm @ComponentScan? Giả sử Spring Boot bỏ @ComponentScan đi, code thay vào sẽ phải làm gì?▸
@SpringBootApplication phải bao gồm @ComponentScan? Giả sử Spring Boot bỏ @ComponentScan đi, code thay vào sẽ phải làm gì?Spring container không tự biết class nào trong project là bean. Container chỉ instantiate bean được khai báo — qua @Bean method trong class @Configuration, qua XML, hoặc qua component scan (tự động phát hiện @Component/@Service/...).
Nếu Spring Boot bỏ @ComponentScan:
- Mọi service phải được khai báo bằng
@Beanmethod trong class config. - Code thành:
@SpringBootConfiguration
@EnableAutoConfiguration
public class App {
@Bean OrderService orderService(PaymentGateway p) { return new OrderService(p); }
@Bean StripePayment stripePayment() { return new StripePayment(); }
// ... 200 bean khac
}Tốn công, dễ quên. Component scan + stereotype annotation (@Service, @Repository) là tradeoff: developer đánh dấu intent ngay tại class, container tự gom — convenience cao, type safety vẫn giữ qua interface/constructor.
Khi nào nên dùng @Bean thay @Component? Khi bean là class third-party không sở hữu (vd HikariDataSource, ObjectMapper) — không thể annotate. Hoặc khi cần config phức tạp tại creation time.
Q8Bạn cần chạy logic "warm cache" sau khi app fully ready (server đã listen port, mọi bean ready). Có 4 cách: (1) @PostConstruct trên 1 bean, (2) CommandLineRunner, (3) @EventListener(ContextRefreshedEvent.class), (4) @EventListener(ApplicationReadyEvent.class). Cách nào đúng nhất, vì sao?▸
@PostConstruct trên 1 bean, (2) CommandLineRunner, (3) @EventListener(ContextRefreshedEvent.class), (4) @EventListener(ApplicationReadyEvent.class). Cách nào đúng nhất, vì sao?Cách 4 — @EventListener(ApplicationReadyEvent.class) là đúng nhất.
Lý do từng cách:
- (1)
@PostConstruct: chạy khi bean init, có thể trước khi context refresh xong, trước khi server listen. Nếu logic warm cache fail, app start fail (intended) — nhưng nếu cần "app live thì warm", quá sớm. - (2)
CommandLineRunner/ApplicationRunner: chạy trướcApplicationReadyEvent. Server đã listen nhưng readiness probe chưa "UP". Hợp khi muốn warm trước khi expose health check. - (3)
ContextRefreshedEvent: publish ở bước 12 củarefresh(), trước Spring Boot start server. Quá sớm. Lưu ý: trong test với@DirtiesContext, event này có thể fire nhiều lần — không idempotent. - (4)
ApplicationReadyEvent: publish sau mọi runner, sau khi server đã listen. Đây là điểm "app fully alive". Idempotent — fire 1 lần / app instance.
Quy tắc:
- Bean init logic →
@PostConstruct. - Pre-ready setup (chạy 1 lần, có thể fail app) →
CommandLineRunner. - Post-ready logic (warm cache, ping downstream, register service) →
@EventListener(ApplicationReadyEvent.class).
Bài tiếp theo: Bean lifecycle — từ instantiate đến destroy, callback tại mỗi giai đoạn
Bài này có giúp bạn hiểu bản chất không?
Bình luận (0)
Đang tải...