Hình dung ApplicationContext như một xưởng trung tâm. Trước giờ mở cửa, nó dựng sẵn mọi dụng cụ đúng một lần, lắp mỗi cái với những dụng cụ khác mà nó cần, xếp hết lên một cái giá chung, rồi khi ai hỏi thì đưa ra cùng một cái. Bạn không tự đi mua và lắp đồ nghề của mình — đó chính là "đảo ngược điều khiển". 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 — cái xưởng. 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 — như mở hộp một dụng cụ, gắn phụ kiện, hiệu chỉnh lần cuối, rồi cất đi lúc đóng cửa:
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 — xưởng dựng hết đồ nghề trước khi mở cửa.
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, đúng lúc bạn còn đứng trong xưởng chứ không phải giữa giờ cao điểm.
Đâ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 — cùng một cái cờ-lê cho mượn cho tất cả cùng lúc, nên đừng ghi chú riêng của mình lên nó.
@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.
Nếu muốn nhìn thấy đồ thị phụ thuộc thật của ứng dụng trong ba mươi giây, thêm một dấu vết vào vài lớp:
@Component
class Kiem {
@PostConstruct void in() { System.out.println("khởi tạo: " + getClass().getSimpleName()); }
}
Chạy lên, 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.
Mẫu số chung
Cái "đảo ngược điều khiển" này có một câu châm ngôn gói trọn: "đừng gọi chúng tôi, chúng tôi sẽ gọi bạn" (Hollywood Principle). Bạn khai thứ mình cần, một container dựng và tiêm nó vào — thay vì tự đi gom. Và khuôn mẫu đó lặp lại ở mọi framework lớn, chỉ khác tên:
- .NET có
IServiceCollectioncủa ASP.NET Core, Angular có injector, Java còn có Guice/Dagger, Node có NestJS. - Go thì cố ý không có khung DI (chỉ có
wiresinh mã lúc biên dịch) — nối dây thủ công trongmain, đúng tinh thần bàiinitcủa loạt Go.
Hai hằng số đáng mang theo. Một: câu hỏi "phạm vi" luôn quay lại — một thực thể dùng chung hay một cái cho mỗi lần dùng (singleton/prototype/request của Spring, singleton/scoped/transient của .NET) — và luật sắt là thực thể dùng chung bắt buộc phải không có trạng thái thay đổi được; khoảnh khắc một đối tượng phục vụ nhiều việc đồng thời, mỗi trường biến đổi của nó là một lỗi đua chờ sẵn. Hai: container đánh đổi việc dựng-tường-minh lấy sự khai-báo tách rời — bạn được dễ test và linh hoạt, nhưng mất khả năng "đọc mã là biết cái gì được tạo", và nhận về một lớp lỗi mới: thiếu bean, mơ hồ bean, phụ thuộc vòng. Đó đúng là lý do phe Go giữ việc nối dây thủ công. Sợi chỉ chung: đẩy trạng thái vào tham số, request, hoặc CSDL thay vì vào trường của bean; và dù cái gì dựng đối tượng cho bạn, hãy bắt sai-cấu-hình nổ lúc khởi động, đừng để nó nổ ở request thứ một nghìn.
Ngày mai: ba cách tiêm phụ thuộc, và vì sao chỉ một cách nên dùng.