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

Resource & @SpringBootApplication: Cơ Chế Nạp Tài Nguyên

Giao diện Resource trừu tượng hóa classpath và file system; phân tích 3 chú thích cốt lõi cấu thành @SpringBootApplication và cơ chế bootstrap.

TL;DR: Giao diện Resource của Spring cung cấp một API thống nhất (getInputStream()) để đọc dữ liệu bất kể tài nguyên nằm trong classpath:, file hệ thống file:, hay đường dẫn mạng https:. Tiền tố classpath*: cho phép quét đồng thời qua nhiều file JAR để thu thập tài nguyên cấu hình tự động. Chú thích @SpringBootApplication hoàn toàn không có phép màu đặc biệt mà là sự kết hợp của 3 chú thích cơ sở: @SpringBootConfiguration (đánh dấu class cấu hình), @EnableAutoConfiguration (kích hoạt nạp starter), và @ComponentScan (quét component từ package hiện tại trở xuống).

Trong bài Environment & PropertySource: Cơ Chế Cấu Hình Đa Nguồn, chúng ta đã tìm hiểu cách Spring nạp các thuộc tính cấu hình. Bài học này sẽ tiếp tục bóc tách cơ chế nạp file tài nguyên ngoại vi (Resource) và cách @SpringBootApplication phối hợp để bootstrap toàn bộ container.

Giao diện Resource trừu tượng hóa các nguồn dữ liệu như thế nào?

Trước khi có Spring Framework, việc đọc dữ liệu phụ thuộc vào từng API Java riêng biệt:

  • File trên đĩa: Dùng new FileInputStream("config.json") — thất bại hoàn toàn khi file nằm bên trong file JAR.
  • File trong Classpath: Dùng getClass().getResourceAsStream("/config.json") — trả về null im lặng nếu đường dẫn sai.
  • File qua mạng: Dùng new URL("https://...").openStream() — giao diện hoàn toàn khác.

Spring giải quyết sự phân mảnh này bằng interface Resource:

Java
public interface Resource extends InputStreamSource {
    InputStream getInputStream() throws IOException; // API thong nhat
    boolean exists();
    boolean isReadable();
    String getFilename();
}

Bạn có thể tiêm bất kỳ nguồn tài nguyên nào qua chú thích @Value:

Java
@Service
public class TemplateService {
    @Value("classpath:templates/mail.html")
    private Resource mailTemplate;

    @Value("file:/etc/app/config.json")
    private Resource externalConfig;

    public String loadTemplate(Resource resource) throws IOException {
        try (var reader = new BufferedReader(new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
            return reader.lines().collect(Collectors.joining("\n"));
        }
    }
}

Cùng một lời gọi ResourceLoader.getResource rẽ ra bốn implementation tuỳ prefix trong chuỗi: classpath cho ClassPathResource, file cho FileSystemResource, http hoặc https cho UrlResource, không prefix thì tuỳ ApplicationContext đang dùng

Tại sao tiền tố classpath lại được sử dụng phổ biến nhất khi đóng gói JAR?

Triết lý chuẩn của ứng dụng Java hiện đại là Single Executable Artifact — toàn bộ mã nguồn, thư viện phụ thuộc và tài nguyên tĩnh (HTML template, SQL schema, banner) được đóng gói trong một file JAR duy nhất:

  1. Độc lập khỏi hệ thống tệp bên ngoài: File JAR có thể chạy trên máy tính cá nhân, CI/CD runner hay container Docker mà không đòi hỏi tạo sẵn thư mục trên đĩa.
  2. Khắc phục lỗi đường dẫn tương đối: Sử dụng new File("config.json") sẽ phụ thuộc vào thư mục làm việc (working directory) khi gõ lệnh chạy, rất dễ gây lỗi NoSuchFileException.
  3. Tính bất biến (Immutability): Tài nguyên nạp từ classpath: nằm trong file nén JAR, đảm bảo không bị can thiệp hay sửa đổi ngoài ý muốn sau khi phát hành.

Sự khác biệt giữa tiền tố classpath: và classpath*: là gì?

  • classpath: (Không có dấu sao): Chỉ tìm kiếm và trả về kết quả khớp đầu tiên trong Classpath. Nếu có nhiều file JAR cùng chứa file trùng tên, các file phía sau sẽ bị bỏ qua.
  • classpath*: (Có dấu sao): Quét qua tất cả các file JAR có trong Classpath và gom toàn bộ các file khớp đường dẫn thành một mảng Resource[].

Lời gọi getResources với prefix classpath sao quét ba jar trên classpath, hai jar có file AutoConfiguration.imports đóng góp vào kết quả còn jar không có file thì bị bỏ qua, cho ra mảng Resource hai phần tử

Đây chính là cơ chế mà Spring Boot sử dụng để tự động phát hiện cấu hình từ các starter: Quét chuỗi classpath*:META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports để nạp toàn bộ danh sách AutoConfiguration từ tất cả các thư viện thứ ba.

Khởi tạo Container thủ công giúp làm sáng tỏ cơ chế gì bên dưới?

Để hiểu cách @SpringBootApplication vận hành, hãy quan sát việc khởi tạo container thuần túy bằng Java:

Java
@Configuration
@ComponentScan(basePackages = "com.olhub.demo")
public class AppConfig {
    @Bean
    public DataSource dataSource() {
        var cfg = new HikariConfig();
        cfg.setJdbcUrl("jdbc:postgresql://localhost/demo");
        return new HikariDataSource(cfg);
    }
}

public class ManualBootstrap {
    public static void main(String[] args) {
        // Tu dong goi close() nho try-with-resources de kich hoat @PreDestroy
        try (var ctx = new AnnotationConfigApplicationContext(AppConfig.class)) {
            var service = ctx.getBean(OrderService.class);
            service.process();
        }
    }
}

Bốn bước cốt lõi diễn ra:

  1. AnnotationConfigApplicationContext kích hoạt phương thức refresh().
  2. @ComponentScan quét gói com.olhub.demo và nạp các BeanDefinition.
  3. Bước 11 của refresh() tạo đối tượng thật và tiêm phụ thuộc.
  4. Khi khối try kết thúc, hàm close() được gọi để kích hoạt callback @PreDestroy dọn dẹp kết nối mạng.

Chú thích @SpringBootApplication được cấu thành từ 3 thành phần nào?

@SpringBootApplication không phải từ khóa bí mật của trình biên dịch, mà là sự hợp nhất của 3 chú thích độc lập:

Java
@SpringBootConfiguration // 1. Khai bao day la class cau hinh Spring (@Configuration)
@EnableAutoConfiguration // 2. Kich hoat co che nap Starter Auto-configuration
@ComponentScan           // 3. Quet cac @Component tu package hien tai tro xuong
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • @SpringBootConfiguration: Đóng vai trò là class @Configuration chính của ứng dụng, hỗ trợ viết trực tiếp các hàm @Bean và giúp @SpringBootTest nhận diện điểm bắt đầu của context.
  • @EnableAutoConfiguration: Kích hoạt AutoConfigurationImportSelector đọc file imports từ các JAR starter để nạp bean có điều kiện (@Conditional).
  • @ComponentScan: Quét toàn bộ các chú thích nghiệp vụ (@Service, @Repository, @RestController).

Tại sao việc đặt class Application ở root package lại mang tính sống còn?

Theo quy ước ngầm định, @ComponentScan không có tham số basePackages sẽ chỉ quét package chứa chính class đó và tất cả các sub-package con:

Plain Text
com.olhub/                     <-- Root package (Dat Application.java o day)
  ├── Application.java         <-- Quet toan bo cac package con ben duoi
  ├── domain/
  │     └── OrderService.java  <-- DUOC QUET VA TAO BEAN
  └── controller/
        └── OrderApi.java      <-- DUOC QUET VA TAO BEAN

Nếu bạn vô tình đặt Application.java vào một package con như com.olhub.controller:

  • Container sẽ chỉ quét thư mục com.olhub.controller.
  • Các package ngang hàng như com.olhub.domain hay com.olhub.service hoàn toàn bị bỏ quên.
  • Hậu quả: Ứng dụng ném lỗi NoSuchBeanDefinitionException vì không tìm thấy service nghiệp vụ.

Vì sao Spring Boot tinh gọn về một ApplicationContext duy nhất?

Trong kiến trúc Spring MVC truyền thống trước đây, hệ thống thường sử dụng mô hình hai context cha-con (Hierarchy Context):

  • Root Context: Nạp các bean chia sẻ chung như Service, Repository, DataSource.
  • Servlet Web Context: Nạp Controller và ViewResolver riêng cho từng Servlet.

Đối chiếu hai kiến trúc context: Spring Boot gom service, repository, controller và DispatcherServlet vào một root context duy nhất; Spring MVC cũ tách web context con chứa controller tra ngược lên root context cha chứa service

Spring Boot đã loại bỏ sự phân cấp rườm rà này và gộp toàn bộ vào một Root Context duy nhất. Việc này giúp đơn giản hóa cấu hình, triệt tiêu lỗi nhầm lẫn phạm vi bean và tối ưu hóa tài nguyên bộ nhớ cho các ứng dụng đám mây microservices.

Tóm tắt

  • Resource thống nhất các nguồn dữ liệu thành giao diện getInputStream(), chạy an toàn trên cả file hệ thống và file đóng gói JAR.
  • Tiền tố classpath*: cho phép quét đồng thời qua toàn bộ các file JAR phụ thuộc để nạp cấu hình tự động.
  • @SpringBootApplication là tổ hợp của 3 chú thích: @SpringBootConfiguration, @EnableAutoConfiguration và @ComponentScan.
  • Đặt class chính tại root package bảo đảm mọi component nghiệp vụ đều được phát hiện và đăng ký vào container.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tại sao việc dùng new File("config.json") có thể chạy bình thường trong IDE nhưng lại thất bại khi đóng gói thành file JAR chạy trên máy chủ?
  2. Q2
    Khác biệt chính giữa tiền tố 'classpath:' và 'classpath*:' trong việc quét tài nguyên của Spring là gì?
  3. Q3
    Ba chú thích cơ sở nào cấu thành nên meta-annotation @SpringBootApplication?
  4. Q4
    Nếu class chính mang @SpringBootApplication nằm ở package com.olhub.api, điều gì sẽ xảy ra với class OrderService nằm ở package com.olhub.domain?
  5. Q5
    Tại sao Spring Boot lại chuyển từ mô hình hai context cha-con (Root và Servlet Web) của Spring MVC cũ sang một Context duy nhất?

Bài tiếp theo: Mini-Challenge: Tự Xây Dựng Mini IoC Container Thuần Java

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.

Thảo luận & góp ý

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

Bài tiếp theo

Mini-challenge: tự build mini IoC container 80 dòng