Bài 1 nói Spring quản lý 266 đối tượng. Bài này về cách nó quản lý chúng.

ApplicationContext là container

var ctx = SpringApplication.run(App.class, args);

ctx là container. Nó giữ mọi bean, biết bean nào cần bean nào, và quản vòng đời của chúng.

Bạn hiếm khi dùng ctx trực tiếp — đó là điểm của IoC. Nhưng biết nó tồn tại giúp hiểu chuyện gì xảy ra, và nó hữu ích khi gỡ lỗi:

ctx.getBeanDefinitionNames()      // liệt kê mọi bean
ctx.getBean(DichVu.class)         // lấy bean theo kiểu
ctx.containsBean("tenBean")

Ba cách khai bean

Chú thích stereotype — cách dùng nhiều nhất:

@Component    // chung
@Service      // tầng nghiệp vụ
@Repository    // tầng dữ liệu
@Controller / @RestController   // tầng web

Bốn cái sau về mặt kỹ thuật giống hệt @Component — chúng chỉ khác về ý nghĩa với người đọc và về vài xử lý riêng. @Repository có thêm việc dịch ngoại lệ của driver CSDL sang ngoại lệ của Spring; các cái khác chủ yếu là tài liệu.

Chúng chỉ có tác dụng nếu nằm trong package được quét — bài 2 đã nói: đặt lớp App ở package gốc.

@Bean trong lớp @Configuration — khi bạn cần kiểm soát việc tạo, hoặc khi lớp đó không phải của bạn:

@Configuration
class CauHinh {
    @Bean
    ObjectMapper objectMapper() {
        return new ObjectMapper().findAndRegisterModules();
    }
}

Không sửa được mã của thư viện bên thứ ba, nên @Component không dùng được — @Bean là cách duy nhất.

Tự động qua auto-configuration — 263 bean còn lại ở bài 1 đến từ đây. Bài 8.

Vòng đời, in ra thật

  1. hàm khởi tạo
  2. tiêm qua setter/method
  3. @PostConstruct
  ... ứng dụng chạy ...
  4. @PreDestroy (lúc ctx.close())

Bốn bước, và thứ tự này quan trọng:

Hàm khởi tạo chạy trước — phụ thuộc bắt buộc nên vào đây. Ở bước này, các phụ thuộc tiêm qua trường chưa được gán.

Tiêm qua trường và setter sau khi đối tượng đã tồn tại.

@PostConstruct khi mọi phụ thuộc đã sẵn sàng. Đây là chỗ đúng cho việc khởi tạo cần dùng tới phụ thuộc — kiểm tra cấu hình, nạp bộ nhớ đệm, mở kết nối.

@PreDestroy khi context đóng.

Một cảnh báo về bước cuối:

@PreDestroy chỉ chạy khi context đóng tử tế. kill -9, hết bộ nhớ, hay container bị SIGKILL đều bỏ qua nó — giống hệt defer ở bài 25 sê-ri Go và log.Fatal ở Java. Đừng đặt việc bắt buộc phải hoàn thành vào đó.

Spring Boot đăng ký shutdown hook nên SIGTERM vẫn chạy @PreDestroy — bài 58 sẽ nói về graceful shutdown.

Singleton được tạo ngay lúc khởi động

Mặc định Spring tạo mọi bean singleton khi khởi động, không đợi tới lúc dùng.

Nghe lãng phí, nhưng nó có lý do quan trọng: sai cấu hình bị phát hiện lúc khởi động, không phải ở request thứ một nghìn. Thiếu một bean, sai một thuộc tính, phụ thuộc vòng — tất cả nổ ra trước khi ứng dụng nhận request đầu tiên.

Đây cũng là lý do khởi động mất 1,1 giây ở bài 1.

Muốn hoãn thì @Lazy, nhưng dùng tiết chế — nó đánh đổi đúng cái lợi ích trên.

Tên bean và trùng lặp

Tên mặc định là tên lớp viết thường chữ đầu: KhoDonHang thành khoDonHang. Với @Bean, tên là tên phương thức.

Đổi tên: @Component("tenKhac").

Hai bean cùng tên thì bean sau ghi đè bean trước — và Spring Boot báo lỗi theo mặc định:

  The bean 'x' could not be registered. A bean with that name has already been defined

Bật spring.main.allow-bean-definition-overriding=true để cho phép, nhưng gần như luôn nên sửa nguyên nhân thay vì bật cờ này.

Bean là singleton, nên phải an toàn luồng

Đây là điều người mới hay bỏ qua: một bean phục vụ mọi request đồng thời.

@Service
class DichVu {
    private int dem;                    // NGUY HIỂM: trạng thái chia sẻ
    void xuLy() { dem++; }              // lỗi đua
}

Bài 34 sê-ri Go và bài 68 sê-ri Java đều đo cùng hiện tượng. Trong Spring, quy tắc là: bean không có trạng thái thay đổi được. Trạng thái nằm trong tham số phương thức hoặc trong CSDL.

Cần đếm thì dùng AtomicLong hoặc LongAdder, đừng dùng int trần.

Thử ba mươi giây

@Component
class Kiem {
    @PostConstruct void in() { System.out.println("khởi tạo: " + getClass().getSimpleName()); }
}

Thêm vào vài lớp và chạy. Thứ tự in ra cho bạn thấy đồ thị phụ thuộc thật — và nó thường sâu hơn bạn nghĩ, đúng như init() ở bài 20 sê-ri Go.

Ngày mai: ba cách tiêm phụ thuộc, và vì sao chỉ một cách nên dùng.