Spring Boot/ApplicationContext và BeanFactory — container hoạt động ra sao bên dưới
~28 phútSpring là gì & nền tảng IoCMiễn phí

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()
    }
InterfaceGì cho ai?
BeanFactoryContainer "tối giản" — chỉ biết tạo bean theo definition, resolve dependency, return bean qua getBean(). Đủ cho app embedded nhỏ.
ApplicationContextContainer "đầ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.
ConfigurableApplicationContextSub-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.

💡 Quy tắc đơn giản

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:

CapabilityInterfaceThấy ở đâu trong Spring
EnvironmentEnvironmentCapable@Value("${...}"), @Profile, application.properties
MessageSourceMessageSourcemessages.properties i18n, MessageSource.getMessage()
Event publisherApplicationEventPublisher@EventListener, ApplicationListener, custom event
Resource resolverResourcePatternResolver@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:

ImplementationKhi nào dùng
ClassPathXmlApplicationContextCấu hình bằng XML trên classpath. Legacy (Spring 1-3 era).
FileSystemXmlApplicationContextCấu hình XML trên file system. Cũng legacy.
AnnotationConfigApplicationContextCấu hình bằng class Java có @Configuration + @ComponentScan. Standard cho Spring không Boot.
AnnotationConfigServletWebServerApplicationContextSpring Boot web servlet (default). Tự động khởi embedded Tomcat/Jetty.
AnnotationConfigReactiveWebServerApplicationContextSpring Boot WebFlux reactive.
GenericApplicationContextProgrammatic — bạn tự register bean qua API. Dùng cho test phức tạp.
StaticApplicationContextTest 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:

  • spring-boot-starter-web → web servlet context.
  • 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:

  1. AnnotationConfigApplicationContext(AppConfig.class) — tạo context và đọc class config.
  2. Constructor tự động gọi refresh() — biến config thành runtime container.
  3. ctx.getBean(OrderService.class) — lookup bean qua type (Spring 3+ hỗ trợ generic-aware).
  4. try-with-resources — tự gọi close() 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 implement ApplicationContextAware để 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ụ: PropertySourcesPlaceholderConfigurer resolve ${...} trong @Value. ConfigurationClassPostProcessor xử lý @Configuration (parse @Bean method).
  • registerBeanPostProcessors — register BeanPostProcessor (BPP). BPP can thiệp trên bean instance (sau bean được tạo, trước/sau init). Ví dụ: AutowiredAnnotationBeanPostProcessor xử lý @Autowired. CommonAnnotationBeanPostProcessor xử 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.
  • finishBeanFactoryInitializationinstantiate 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:#fef3c7

99% 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ự:

EventKhi nàoUse case
ApplicationStartingEventBắt đầu run(), trước khi xử lý argsLog/metric "app starting"
ApplicationEnvironmentPreparedEventEnvironment đã build, chưa tạo ApplicationContextModify env trước khi context build
ApplicationContextInitializedEventContext đã tạo nhưng chưa load bean definitionInject programmatic bean definition
ApplicationPreparedEventContext refresh xong nhưng chưa publishedCustom init trước listener khác
ContextRefreshedEventBước 12 của refresh()Phổ biến nhất — bean ready, có thể start side-effect
ApplicationReadyEventApp hoàn toàn ready (sau cả CommandLineRunner/ApplicationRunner)Mark "app live" cho health check, smoke test
ContextClosedEventclose() được gọiPre-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đạ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, ...
  • Profilesdev/prod/test, kiểm tra qua env.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_ACTIVEspring.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:

PrefixResource typeVí dụ
classpath:Trên classpath (jar hoặc target/classes)classpath:templates/index.html
file:File system absolutefile:/etc/app/config.json
https: / http:URL remotehttps://example.com/data.json
Không prefixTuỳ context — thường file relativedata.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:#d1fae5

Trong 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:

AnnotationTác dụng
@SpringBootConfigurationMarker 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.
@EnableAutoConfigurationTrigger 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.
@ComponentScanScan 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: implement TypeFilter interface.

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.javaroot 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

📚 Tài liệu chính chủ

Reference docs nên đọc:

Source chính chủ để đọc khi muốn đi sâu:

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

  • BeanFactory là interface gốc tối giản; ApplicationContext extend với i18n, event, environment, resource — đây là interface dùng 99% thời gian.
  • ConfigurableApplicationContext thê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ự — ApplicationReadyEvent là điểm tốt cho "after start" logic.
  • BeanDefinition là metadata, không phải bean instance. BFPP có thể modify trước instantiate.
  • Environment quản properties + profiles. PropertySource có thứ tự ưu tiên: command line > env var > application.properties. Spring Boot relax binding: DB_URLdb.url.
  • Resource abstraction: 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.
  • @ComponentScan mặ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

Tự kiểm tra
Q1
Bạ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?

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ó @PostConstruct gọ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.

Q2
Khá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.

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 @Value bằng giá trị property thực tế. ConfigurationClassPostProcessor — parse @Configuration class, register @Bean method 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.

Q3
Tạ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ể.

@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. OrderServiceStripePayment bị miss → startup throw NoSuchBeanDefinitionException khi resolve dependency.

Fix 2 cách:

  1. Move App.java lên root com.olhub. Đây là chuẩn.
  2. Khai báo scope rộng hơn: @SpringBootApplication(scanBasePackages = "com.olhub") — nhưng workaround, không khuyến nghị.
Q4
App 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?

Win: (4) command line argumentjdbc:postgresql://prod2.

Thứ tự PropertySource trong Spring Boot (cao đến thấp):

  1. Command line args (--key=value)
  2. System properties (-Dkey=value JVM arg)
  3. OS environment variables (KEY=value)
  4. application-{profile}.properties
  5. application.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);

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()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;
  • 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 file META-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.

Q7
Vì 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ì?

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 @Bean method 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.

Q8
Bạ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?

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ước ApplicationReadyEvent. 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ủa refresh(), 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...