Spring Core & Boot
9/41
Bài 9 / 41~12 phútContainer InternalsMiễn phí… lượt xem

Vòng Đời Khởi Động: Giải Mã 12 Bước Trong refresh()

Mổ xẻ 12 bước trong refresh() của AbstractApplicationContext, phân biệt BFPP và BPP, ánh xạ lỗi startup vào từng bước cụ thể.

TL;DR: Khi thực thi SpringApplication.run(), container nạp toàn bộ cấu hình thông qua phương thức cốt lõi refresh() gồm 12 bước tuần tự trong AbstractApplicationContext. Chu trình này được phân thành 3 giai đoạn: Chuẩn bị BeanFactory và nạp siêu dữ liệu (bước 1–4), Biến đổi siêu dữ liệu qua BFPP và đăng ký BPP (bước 5–6), Cấp phát toàn bộ bean Singleton non-lazy và phát sự kiện (bước 7–12). Bước 11 chiếm 99% thời gian khởi động; nắm vững 12 bước này giúp bạn định vị chính xác nguyên nhân của mọi ngoại lệ startup.

Trong bài BeanFactory vs ApplicationContext: Bản Chất Hai Container, chúng ta đã biết container duy trì hai bản đồ chính: beanDefinitionMap (siêu dữ liệu) và singletonObjects (instance thực tế). Bài học này sẽ giải mã câu hỏi then chốt: Quá trình lấp đầy hai bản đồ này diễn ra theo trình tự 12 bước nào?

Phương thức refresh() trong AbstractApplicationContext gồm những bước nào?

Toàn bộ quá trình khởi động của Spring ApplicationContext được đồng bộ hóa thông qua một phương thức duy nhất:

Java
// File: org/springframework/context/support/AbstractApplicationContext.java
public void refresh() throws BeansException, IllegalStateException {
    synchronized (this.startupShutdownMonitor) {
        // --- PHASE 1: Prepare BeanFactory and load metadata ---
        prepareRefresh();                                        // 1
        ConfigurableListableBeanFactory bf = obtainFreshBeanFactory(); // 2
        prepareBeanFactory(bf);                                  // 3
        postProcessBeanFactory(bf);                              // 4

        try {
            // --- PHASE 2: Transform metadata and register extensions ---
            invokeBeanFactoryPostProcessors(bf);                 // 5
            registerBeanPostProcessors(bf);                      // 6

            // --- PHASE 3: Instantiate singletons and publish events ---
            initMessageSource();                                 // 7
            initApplicationEventMulticaster();                   // 8
            onRefresh();                                         // 9
            registerListeners();                                 // 10
            finishBeanFactoryInitialization(bf);                 // 11 (takes 99% startup time)
            finishRefresh();                                     // 12
        } catch (BeansException ex) {
            destroyBeans();
            cancelRefresh(ex);
            throw ex;
        }
    }
}

Mười hai bước của refresh gom thành ba giai đoạn: chuẩn bị BeanFactory bước 1 đến 4, cửa sổ sửa metadata bước 5 và 6, tạo object và công bố bước 7 đến 12; bước 11 được mở rộng sang bên phải liệt kê toàn bộ việc làm cho mỗi singleton

Giai đoạn 1: Container chuẩn bị BeanFactory và nạp siêu dữ liệu ra sao?

Giai đoạn này tập trung vào việc đọc cấu hình để điền đầy bản đồ beanDefinitionMap (chưa có bất kỳ bean nghiệp vụ nào được tạo ra trên bộ nhớ):

  1. Bước 1 — prepareRefresh(): Khởi tạo trạng thái, xác thực các thuộc tính môi trường bắt buộc (validateRequiredProperties()). Nếu thiếu biến cấu hình trọng yếu, lỗi sẽ ném ra ngay lập tức.
  2. Bước 2 — obtainFreshBeanFactory(): Quét các class mang chú thích @Configuration, @Component, đọc XML/Java config và chuyển đổi thành các thực thể BeanDefinition lưu vào beanDefinitionMap.
  3. Bước 3 — prepareBeanFactory(bf): Cài đặt ClassLoader, SpEL resolver (#{...}), và đăng ký ApplicationContextAwareProcessor để hỗ trợ tiêm context vào bean.
  4. Bước 4 — postProcessBeanFactory(bf): Hook cho các lớp con mở rộng (ví dụ Spring Boot thêm các scope cho ứng dụng Web như request hay session).

Giai đoạn 2: Cơ chế biến đổi siêu dữ liệu của BFPP và BPP hoạt động như thế nào?

Đây là cửa sổ can thiệp duy nhất vào siêu dữ liệu trước khi các bean thực sự được sinh ra:

  • Bước 5 — invokeBeanFactoryPostProcessors(bf): Kích hoạt toàn bộ BeanFactoryPostProcessor (BFPP). Tiêu biểu là ConfigurationClassPostProcessor (phân tích sâu @ComponentScan, @Import) và PropertySourcesPlaceholderConfigurer (giải mã ${...} trong @Value). BFPP có toàn quyền sửa đổi cấu hình trong BeanDefinition.
  • Bước 6 — registerBeanPostProcessors(bf): Tìm kiếm và đăng ký các BeanPostProcessor (BPP) vào danh sách quản lý của container (như AutowiredAnnotationBeanPostProcessor xử lý @Autowired). Lưu ý: Các BPP chỉ được đăng ký chứ chưa chạy ở bước này.
Quy tắc phân biệt BFPP vs BPP

BFPP tác động lên siêu dữ liệu (bản vẽ kỹ thuật) ở bước 5 trước khi có object. BPP tác động lên instance thực tế (ngôi nhà đã xây) quanh thời điểm tạo bean ở bước 11.

Tại sao bước 11 chiếm tới 99% thời gian khởi động của ứng dụng?

Tại Bước 11 (finishBeanFactoryInitialization), container duyệt qua toàn bộ beanDefinitionMap, lọc ra tất cả các bean Singleton không đánh dấu @Lazy, và kích hoạt chuỗi xử lý tạo thực thể:

  1. Phân giải đệ quy và khởi tạo các bean phụ thuộc (Dependency Resolution).
  2. Gọi Constructor phù hợp thông qua Reflection.
  3. Chạy BPP tiền khởi tạo (postProcessBeforeInitialization).
  4. Kích hoạt callback khởi tạo @PostConstruct hoặc InitializingBean.afterPropertiesSet().
  5. Chạy BPP hậu khởi tạo (postProcessAfterInitialization) — đây là nơi Dynamic Proxy của AOP (@Transactional, @Async) được bọc ngoài bean.
  6. Lưu bean hoàn chỉnh vào bảng băm singletonObjects.

Với một ứng dụng Spring Boot doanh nghiệp chứa từ 300 đến 600 bean cùng nhiều thao tác kết nối cơ sở dữ liệu hay kiểm tra hạ tầng lúc khởi động, bước 11 chiếm gần như trọn vẹn thời gian khởi động ứng dụng.

Các sự cố khởi động (Startup Errors) ánh xạ tương ứng vào những bước nào?

Khi ứng dụng gặp sự cố không thể khởi động, việc đối chiếu lớp ngoại lệ đầu tiên trong stack trace với các bước của refresh() sẽ giúp khoanh vùng lỗi trong vài giây:

Ngoại lệ phát sinhBước tương ứngNguyên nhân cốt lõi
MissingRequiredPropertiesExceptionBước 1Biến môi trường bắt buộc chưa được cấu hình
BeanDefinitionParsingExceptionBước 2Lỗi cú pháp trong class @Configuration hoặc file XML
BeanDefinitionOverrideExceptionBước 2 hoặc 5Hai bean bị trùng tên định danh trong context
UnsatisfiedDependencyExceptionBước 11Thiếu bean phụ thuộc hoặc có nhiều bean gây xung đột
BeanCreationExceptionBước 11Hàm Constructor hoặc @PostConstruct ném lỗi logic
BeanCurrentlyInCreationExceptionBước 11Xảy ra phụ thuộc vòng giữa các constructor

Sự kiện Application Event nào an toàn nhất để thực thi tác vụ sau khởi động?

Spring phát đi nhiều sự kiện trong và sau chu trình refresh():

Plain Text
refresh() hoàn tất (Bước 12)
   └─ ContextRefreshedEvent (Container xong, nhưng server chưa chắc đã sẵn sàng)
        └─ ApplicationStartedEvent (Trước khi CommandLineRunner chạy)
             └─ ApplicationReadyEvent (Sau khi mọi Runner hoàn tất, Port đã mở)

Sử dụng ApplicationReadyEvent là giải pháp an toàn và đáng tin cậy nhất cho các tác vụ khởi chạy (như nạp bộ nhớ đệm cache hoặc gửi tín hiệu sẵn sàng):

Java
@Component
public class CacheWarmer {
    @EventListener(ApplicationReadyEvent.class)
    public void onReady() {
        // Safe: all beans initialized and server listening on port
        log.info("System is ready. Warming up cache...");
    }
}

Tại sao gọi getBean() bên trong BeanFactoryPostProcessor là cạm bẫy nguy hiểm?

Nếu trong phương thức postProcessBeanFactory của một BFPP tùy biến, bạn vô tình gọi bf.getBean("someService"):

Java
@Component
public class BadBFPP implements BeanFactoryPostProcessor {
    @Override
    public void postProcessBeanFactory(ConfigurableListableBeanFactory bf) {
        // Anti-pattern: Calling getBean during Phase 2
        SomeService svc = (SomeService) bf.getBean("someService");
    }
}

Tại bước 5, danh sách BPP chưa hề được đăng ký (bước 6 chưa chạy). Lệnh gọi getBean() ép container phải khởi tạo someService sớm. Hậu quả là bean này bị tước bỏ toàn bộ các bộ xử lý BPP:

  • Các trường @Autowired không được tiêm giá trị (null).
  • Phương thức @PostConstruct không được kích hoạt.
  • Không được bọc AOP Proxy, khiến @Transactional hay @Async hoàn toàn vô hiệu.

Quy tắc bắt buộc: BFPP chỉ được phép đọc và biến đổi BeanDefinition, tuyệt đối không được gọi getBean().

Tóm tắt

  • Phương thức refresh() điều phối 12 bước khởi tạo container qua 3 giai đoạn chặt chẽ.
  • Giai đoạn 1 nạp siêu dữ liệu vào beanDefinitionMap; Giai đoạn 2 cho phép BFPP tinh chỉnh siêu dữ liệu; Giai đoạn 3 cấp phát bean.
  • Bước 11 tạo toàn bộ Singleton bean non-lazy và thực thi nguyên lý Fail-Fast để bảo vệ hệ thống.
  • Hãy lắng nghe sự kiện ApplicationReadyEvent để thực thi logic sau khởi động nhằm tránh hiện tượng chạy sớm khi server chưa sẵn sàng.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Lỗi UnsatisfiedDependencyException thường phát sinh ở bước nào trong chu trình 12 bước của phương thức refresh()?
  2. Q2
    Hậu quả nghiêm trọng nhất khi gọi getBean() bên trong phương thức của một BeanFactoryPostProcessor (BFPP) là gì?
  3. Q3
    Khác biệt căn bản giữa BeanFactoryPostProcessor (BFPP) và BeanPostProcessor (BPP) về thời điểm và đối tượng tác động là gì?
  4. Q4
    Tại sao bước 11 (finishBeanFactoryInitialization) lại chiếm tới 99% tổng thời gian khởi động của ứng dụng Spring?
  5. Q5
    Để thực hiện một tác vụ sau khi toàn bộ ứng dụng Spring Boot đã khởi động hoàn tất và máy chủ sẵn sàng nhận request, sự kiện nào là an toàn nhất?

Bài tiếp theo: BeanDefinition & BFPP: Thao Tác Siêu Dữ Liệu Trước Khi Tạo Bean

Bài này có giúp bạn hiểu bản chất không?

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.

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

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

BeanDefinition & BFPP: Thao Tác Siêu Dữ Liệu Trước Khi Tạo Bean