Hình dung dựng một dự án Spring Boot như gọi món ở nhà hàng. Một starter là suất combo: bạn gọi đúng một món "web", nhà bếp mang ra cả khay — Spring MVC, Tomcat, Jackson, validation — thay vì bạn phải gọi lẻ mười lăm món và tự lo chúng có hợp nhau không. Parent POM là người bếp trưởng đã nếm thử trước: ông ấy biết phiên bản nào của món nào ăn cùng nhau được, nên bạn không bao giờ gọi phải hai món cãi nhau (xung đột phiên bản). Và fat jar là gói cả bữa ăn lẫn một cái bếp di động vào một hộp mang đi. Giữ ba hình ảnh đó, cả bài này chỉ là đi xem từng mảnh của cái hộp. Bài này dựng dự án và giải thích từng mảnh của nó.
start.spring.io
Cách chuẩn để bắt đầu. Chọn Maven hoặc Gradle, Java 21, Spring Boot 3.x, rồi thêm dependency.
Hoặc bằng dòng lệnh:
curl https://start.spring.io/starter.zip \
-d dependencies=web,actuator \
-d javaVersion=21 -d bootVersion=3.3.4 \
-d groupId=vd -d artifactId=demo -o demo.zip
Đừng chép pom.xml từ dự án cũ — phiên bản Boot kéo theo hàng trăm phiên bản thư viện đã được kiểm thử cùng nhau, và start.spring.io luôn cho bộ mới nhất tương thích.
pom.xml: ba phần đáng đọc
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.4</version>
</parent>
Parent POM khai dependencyManagement cho hàng trăm thư viện. Đây là lý do bạn viết dependency không có số phiên bản — và là lý do dự án Spring Boot hiếm khi gặp xung đột phiên bản. Bài 91 sê-ri Java đã đo: chỉ cần đảo thứ tự hai dòng khai báo là phiên bản thư viện đổi; BOM của Boot chặn chuyện đó.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
Starter là một POM rỗng chỉ để kéo theo một nhóm thư viện. starter-web mang Spring MVC, Tomcat nhúng, Jackson, validation. Bạn khai một dòng thay vì mười lăm — gọi combo thay vì gọi lẻ.
<plugin>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
Plugin đóng gói thành fat jar và cho mvn spring-boot:run.
Fat jar
fat jar: 22 MB
mvn package
java -jar target/demo-1.0.jar
Không cần cài Tomcat, không triển khai WAR. Máy chủ web nằm trong jar — cái bếp di động gói sẵn trong hộp.
Cấu trúc bên trong không phải jar thường:
BOOT-INF/classes/ mã của bạn
BOOT-INF/lib/ mọi jar phụ thuộc, NGUYÊN VẸN
org/springframework/boot/loader/ trình nạp riêng
Java không nạp được jar lồng trong jar, nên Boot có trình nạp lớp riêng. Đó cũng là lý do fat jar của Boot khác "shaded jar" — thư viện không bị trộn vào nhau nên không có xung đột tệp tài nguyên.
Với Docker, cấu trúc này còn cho phép tách tầng để tận dụng cache — bài 55 sẽ nói.
Lớp khởi động
@SpringBootApplication
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
@SpringBootApplication gộp ba chú thích:
@SpringBootConfiguration — đánh dấu đây là nguồn định nghĩa bean. @EnableAutoConfiguration — bật cấu hình tự động, bài 8. @ComponentScan — quét package của lớp này và mọi package con để tìm @Component.
Điểm cuối rất quan trọng: đặt lớp App ở package gốc. Đặt nó trong vd.demo.web thì Spring không thấy vd.demo.kho, và bạn nhận lỗi "no qualifying bean" rất khó hiểu.
Cấu trúc thư mục
demo/
├── pom.xml
└── src/
├── main/java/vd/demo/ App.java + mã
├── main/resources/ application.yml, static/, templates/
└── test/java/vd/demo/
Trong src/main/java, tổ chức theo miền nghiệp vụ:
vd/demo/
├── App.java
├── donhang/ Controller, Service, Repository, entity của đơn hàng
└── khachhang/
thay vì theo tầng kỹ thuật (controllers/, services/, repositories/). Cùng lý do với bài 21 sê-ri Go: tách theo tầng nghĩa là mỗi thay đổi phải sửa nhiều package.
Chạy
mvn spring-boot:run # phát triển, có restart nhanh
java -jar target/demo.jar # sản xuất
Thêm spring-boot-devtools để tự khởi động lại khi mã đổi — chỉ dùng lúc phát triển, và nó tự tắt khi chạy từ jar.
application.yml
server:
port: 8081
spring:
application:
name: dich-vu-don-hang
logging:
level:
root: INFO
YAML hay properties đều được. Bài 6 sẽ nói về thứ tự ưu tiên và profile.
Ba lệnh đầu tiên nên biết
mvn dependency:tree | head -40 # xem starter kéo theo gì
curl localhost:8080/actuator/health # kiểm tra sống
mvn spring-boot:run -Ddebug # xem auto-config quyết định gì
Lệnh thứ ba đáng chạy một lần ngay hôm nay — nó in ra toàn bộ quyết định của Boot, và bài 8 sẽ đọc kết quả đó.
Và nếu muốn thấy cái combo thật ra to cỡ nào, đếm một dòng:
mvn dependency:tree | grep -c ':.*:.*:'
Con số — số thư viện mà một dòng spring-boot-starter-web kéo vào — thường trên bốn mươi, và đó là lời giải thích trực quan nhất cho 266 bean ở bài trước.
Mẫu số chung
Hai cơ chế trong bài này là cách mọi hệ sinh thái hiện đại giải cùng hai bài toán.
Một: một gói tổng hợp tuyển sẵn. Spring có starter; nhưng Rails có meta-gem, Python có nhóm "extras" (pip install pkg[all]), Node có các preset/template. Tất cả đổi quyền kiểm soát chi tiết lấy "một dòng kéo theo một bộ đã được thử cùng nhau". Món quà sâu hơn là sự nhất quán phiên bản: phải có cái gì đó quyết định phiên bản của hàng trăm phụ thuộc gián tiếp, và mỗi hệ chọn một cách khác hẳn — Maven dùng BOM cộng "gần nhất thắng", npm lồng nhiều bản sao, Go dùng chọn phiên bản tối thiểu (MVS), Python giải phẳng một bản. Nhưng thứ tất cả cùng chống lại là một: xung đột phụ thuộc hình kim cương — đúng cái "đảo hai dòng là phiên bản đổi" ở bài 91.
Hai: một artifact chạy được tự chứa. Fat jar kèm máy chủ nhúng của Spring cùng họ với binary tĩnh của Go, single-file publish của .NET, bundler của Node — tất cả vì kỷ nguyên container chuộng "triển khai = chép một tệp" hơn "cài máy chủ rồi thả WAR vào". Sợi chỉ chung đáng mang theo: hãy biết thứ gì đang quyết định phiên bản phụ thuộc của bạn — vì cái ngày hai thư viện đòi hai phiên bản khác nhau của một thư viện thứ ba, cơ chế đó là thứ duy nhất đứng giữa bạn và một buổi chiều mất trắng.
Ngày mai: IoC container và bean — Spring quản lý những đối tượng đó thế nào.