Hình dung xây một ngôi nhà. Tự mình đặt từng loại vật tư, đúng thứ tự — móng trước tường trước mái — tự thuê từng thợ, tự nhớ ngắt điện nước khi phá dỡ: với một cái lán thì làm được, với một toà nhà thì không. Spring là framework Java phổ biến nhất, và cũng là thứ bị dùng mà không hiểu nhiều nhất. Bài này bắt đầu bằng câu hỏi cơ bản: nó giải bài toán gì?
Bài toán: nối các mảnh lại với nhau
Một dịch vụ đơn giản cũng có nhiều tầng:
var pool = new HikariDataSource(cauHinh);
var kho = new KhoDonHang(pool);
var thanhToan= new CongThanhToan(httpClient, khoaAPI);
var dichVu = new DichVuDonHang(kho, thanhToan, hangDoiMail);
var handler = new DonHangController(dichVu);
Với năm lớp thì viết tay được. Với năm mươi lớp — con số bình thường của một dịch vụ thật — bạn có một hàm main dài hàng trăm dòng, và mỗi lần thêm một phụ thuộc là sửa ở nhiều chỗ.
Ba vấn đề nữa xuất hiện: thứ tự khởi tạo, vòng đời (ai đóng pool khi tắt máy), và cấu hình khác nhau giữa môi trường dev, test, sản xuất.
Spring làm việc đó thay bạn. Bạn khai cái gì cần cái gì — như đưa nhà thầu bản vẽ "bếp cần điện và nước" — Spring dựng đồ thị và tạo theo đúng thứ tự.
Đảo ngược điều khiển
Tên "IoC" nghe hàn lâm nhưng ý rất đơn giản:
Không có Spring: mã của bạn gọi thư viện, và bạn tạo mọi đối tượng — bạn tự cầm búa đóng từng cái.
Có Spring: bạn khai lớp và phụ thuộc, framework tạo đối tượng rồi gọi mã của bạn — nhà thầu dựng xong rồi mới gọi bạn tới.
Đó là "đảo ngược" — quyền điều khiển chuyển từ mã của bạn sang container. Đúng câu kinh điển: "đừng gọi chúng tôi, chúng tôi sẽ gọi bạn".
@Service
class DichVuDonHang {
private final KhoDonHang kho;
DichVuDonHang(KhoDonHang kho) { this.kho = kho; } // Spring tự truyền vào
}
Không có new KhoDonHang(...) ở đâu cả. Bạn chỉ nói "tôi cần một KhoDonHang".
Cái giá: 266 bean
Tôi dựng một ứng dụng Spring Boot tối giản — chỉ spring-boot-starter-web và actuator, ba lớp của riêng tôi:
tổng số bean: 266
bean của tôi: [khoPrototype, dichVu, khoSingleton]
Ba bean của tôi, 263 bean của Spring.
Đó là máy chủ Tomcat nhúng, bộ chuyển đổi JSON, xử lý ngoại lệ, quản lý giao dịch, endpoint sức khoẻ, bộ đo chỉ số, và hàng chục thứ khác — cả một đội thợ nhà thầu mang theo sẵn.
Cùng với nó là thời gian khởi động:
Started App in 1.146 seconds
Started App in 1.208 seconds
Started App in 1.061 seconds
Một giây cho ứng dụng in ra vài dòng. Đặt cạnh sê-ri Go trước đó — một dịch vụ HTTP Go khởi động trong vài mili giây — khác biệt là hai bậc độ lớn. Một đội đông thì mất thời gian huy động, một người cầm búa thì không.
Và jar:
fat jar: 22 MB
Khi nào Spring đáng, khi nào không
Đáng: ứng dụng nghiệp vụ nhiều tầng, đội đông người, cần tích hợp nhiều thứ (CSDL, hàng đợi, bảo mật, giám sát), và vòng đời dự án tính bằng năm.
Không đáng: công cụ dòng lệnh, hàm serverless cần khởi động dưới 100 ms, dịch vụ rất nhỏ chỉ có vài endpoint, hoặc khi khởi động nhanh là yêu cầu cứng.
Với nhóm thứ hai, Go hoặc Java thuần đều hợp lý hơn. Bài 58 của sê-ri này sẽ đo AOT và Native Image — cách Spring thu hẹp khoảng cách đó.
Spring khác Spring Boot
Hai tên hay bị dùng lẫn:
Spring Framework là phần lõi: IoC container, AOP, quản lý giao dịch, Spring MVC. Có từ 2003.
Spring Boot (2014) là lớp bên trên: cấu hình tự động, máy chủ nhúng, starter dependency, và application.yml.
Trước Spring Boot, dựng một ứng dụng Spring nghĩa là hàng trăm dòng XML và một tệp WAR triển khai lên Tomcat riêng. Boot bỏ hết chuyện đó.
Trong sê-ri này, "Spring" gần như luôn nghĩa là Spring Boot.
Ba khái niệm chống đỡ mọi thứ
Ba thứ này sẽ quay lại ở mọi bài sau:
Bean — một đối tượng do container quản lý. Bài 3.
Dependency Injection — cách bean nhận phụ thuộc. Bài 4.
Auto-configuration — cách Boot đoán bạn cần gì từ những jar có trong classpath. Bài 8, và nó là thứ tạo ra 263 bean kia.
Về sê-ri này
Sáu mươi bài, đi từ container tới đưa dịch vụ lên sản xuất. Nguyên tắc giống hai sê-ri trước: không viết điều chưa chạy thử. Mọi con số đều đo trên Spring Boot 3.3 và Java 21, và khi phép đo đi ngược lời khuyên phổ biến, tôi sẽ nói rõ.
Bạn cần Java 21 và Maven. Bài sau sẽ dựng dự án.
Nếu bạn đã có một ứng dụng Spring Boot, hỏi thẳng nó đang quản bao nhiêu đối tượng hộ bạn, trong ba mươi giây:
System.out.println(ctx.getBeanDefinitionNames().length);
Con số đó là số đối tượng Spring đang quản lý hộ bạn. Với ứng dụng thật, nó thường nằm trong khoảng vài trăm tới hơn một nghìn — và biết con số đó là bước đầu để hiểu chuyện gì xảy ra khi ứng dụng khởi động.
Mẫu số chung
Bài toán nối các mảnh — dựng và sắp thứ tự một đồ thị đối tượng có vòng đời — là chuyện ở đâu cũng gặp, và lời giải là một container IoC/DI dựng đồ thị đó từ khai báo của bạn: Spring, nhưng cũng là DI cài sẵn của .NET, Guice/Dagger của Java, DI của Angular, NestJS của Node, wire biên-dịch của Go. Nước đi cốt lõi là tách "cái gì phụ thuộc cái gì" khỏi "ai dựng nó và dựng khi nào", trao quyền điều khiển cho container — đảo ngược điều khiển, đúng câu "đừng gọi chúng tôi, chúng tôi sẽ gọi bạn". Dưới một nhúm đối tượng thì một hàm main viết tay là đủ; vượt qua đó, nối tay chính là cái gánh nặng bảo trì mà container sinh ra để gỡ.
Điều thứ hai: một framework là một cuộc mặc cả, không phải bữa trưa miễn phí. Nó mang sẵn cả một đội thợ — máy chủ, JSON, giao dịch, giám sát — thứ mà nếu không có bạn phải thuê từng người, đổi lại bạn trả bằng thời gian khởi động, bộ nhớ và sự mờ đục (ở đây là 263 bean bạn không viết, ~1,1 giây, 22 MB, đặt cạnh một dịch vụ Go tính bằng mili giây). Câu hỏi đúng không bao giờ là "nhiều quá không" mà là "tôi có cần cái nó mang tới không" — loại đầy-đủ-pin (Spring, Django, Rails) hợp với ứng dụng sống-lâu, nhiều-tích-hợp, nhiều-người; loại tối giản (net/http của Go, Flask, Sinatra) hợp với một công cụ dòng lệnh hay một hàm nhạy-cảm-với-khởi-động-nguội. Khớp sức nặng của công cụ với tuổi thọ và bề rộng của bài toán, và luôn biết mình đang trả giá cho cái gì.
Ngày mai: dựng dự án Spring Boot đầu tiên.