Sáu mươi bài, sáu mươi ngày. Bài này nhìn lại và vẽ bản đồ đi tiếp.
Năm chặng
Bài 1–10: nền tảng. IoC container, tiêm phụ thuộc, vòng đời và phạm vi bean, auto-configuration, @ConfigurationProperties, cấu trúc dự án.
Bài 11–22: web. DispatcherServlet, @RestController, validation, xử lý ngoại lệ, Jackson, Thymeleaf, Filter và Interceptor, CORS, tải tệp, gọi HTTP ra ngoài, thiết kế REST, phân trang và đánh phiên bản.
Bài 23–38: dữ liệu. Spring Data JPA, N+1, lazy loading và OSIV, giao dịch và mức lan truyền, khoá lạc quan và bi quan, Specification, projection, JdbcClient, chèn hàng loạt, Flyway, pool kết nối, cache, kiểm thử tầng dữ liệu, Redis.
Bài 39–48: bảo mật. Chuỗi lọc, mã hoá mật khẩu, phân quyền, CSRF, JWT, OAuth2, header bảo mật và CSP, lỗ hổng thường gặp, kiểm thử bảo mật.
Bài 49–60: đưa vào sản xuất. Kim tự tháp test, cấu hình, Actuator, log và truy vết, @Async và @Scheduled, sự kiện ứng dụng, luồng ảo, khởi động và bộ nhớ, đóng gói, ảnh native, danh sách kiểm.
Mười hai lần phép đo nói ngược
Đây là phần tôi thấy giá trị nhất, và cũng là thứ không lường trước khi bắt đầu.
RestClient mặc định không có phép chờ (bài 20). Chờ đủ 5027 ms cho endpoint ngủ 5 giây rồi báo thành công. Một dịch vụ treo thật giữ luồng tới khi TCP bỏ cuộc — khoảng mười lăm phút.
Interceptor có chạy cho tệp CSS (bài 17). Câu "Filter thấy mọi request, Interceptor chỉ thấy controller" — vế sau sai.
Request lỗi đi qua chuỗi lọc hai lần (bài 17). Chỉ số bị nhân đôi, xác thực chạy lại, MDC mất giá trị ở trang lỗi.
Origin CORS sai trả 403, không phải 200 thiếu header (bài 18).
saveAll() không nhanh hơn vòng lặp save() (bài 32). 12 mili giây trên 5000 bản ghi. Thứ tạo khác biệt là ba dòng batch_size, và nó nhanh gấp tám.
Checked exception vẫn được commit (bài 26). Mặc định của Spring chỉ rollback với RuntimeException.
Khoá bi quan cho ra số dư âm 100 — và đó là bằng chứng nó hoạt động (bài 28). Nếu khoá hỏng, kết quả sẽ là 0, và một trăm đồng biến mất mà trông vẫn hợp lý.
BCrypt cắt ở byte thứ 72 (bài 40). Hai mật khẩu khác hẳn nhau nhưng trùng 72 byte đầu được coi là một.
Gửi đúng token CSRF vẫn 403 (bài 42). Spring Security 6 đổi ba thứ cùng lúc.
Cấu hình HSTS xong mà header không xuất hiện (bài 45). Thiếu X-Forwarded-Proto là nó im lặng biến mất.
MockMvc nói 403, máy chủ thật trả 405 (bài 47).
@WebMvcTest nhanh hơn @SpringBootTest đúng 60 mili giây (bài 49). Thứ thật sự làm bộ test chậm là số ngữ cảnh khác nhau, không phải loại lát cắt.
Bài học không phải "đừng tin ai". Nó là: lời khuyên có hạn sử dụng, và mặc định thay đổi giữa các phiên bản. Ba trong số trên là hệ quả trực tiếp của việc nâng lên Spring Boot 3 và Spring Security 6.
Bốn chỗ proxy làm chú thích biến mất
Nếu chỉ mang một điều từ sê-ri này, tôi chọn cái này:
| Chú thích | Tự gọi trong cùng lớp thì | Bài |
|---|---|---|
@Transactional |
không có giao dịch | 26 |
@Cacheable |
cache không trúng | 35 |
@PreAuthorize |
phân quyền bị bỏ qua | 41 |
@Async |
chạy đồng bộ trên luồng gọi | 53 |
đọc trường public của bean |
trả về null |
35, 53 |
Cùng một nguyên nhân, và cả năm đều im lặng: không lỗi, không cảnh báo, không dòng log.
Cái thứ ba là lỗ hổng bảo mật thật. Cái thứ năm tôi mắc hai lần trong lúc viết sê-ri này, lần thứ hai sau khi đã viết hẳn một bài về nó.
Quy tắc: đặt chú thích ở ranh giới, không ở bên trong. Cần kiểm ở tầng sâu thì tách sang bean khác — việc đó thường lộ ra rằng lớp đang làm hai việc.
Ba thói quen đáng giữ
Bật log SQL trong lúc phát triển. Một dòng cấu hình, và toàn bộ chặng dữ liệu trở thành thứ nhìn thấy được thay vì thứ phải suy đoán.
Kiểm bằng curl, đừng kiểm bằng đọc lại cấu hình. Sáu bất ngờ ở chặng bảo mật đều là chỗ cấu hình trông đúng mà hành vi thật khác. Một lệnh curl kiểm được nhiều hơn một giờ đọc mã.
Đừng để entity ra khỏi tầng service. Nó giải quyết cùng lúc bốn thứ: LazyInitializationException, N+1 lúc render, vòng lặp tham chiếu khi serialize, và gán thuộc tính hàng loạt — cái cuối là lỗ hổng bảo mật.
Đi tiếp từ đây
Kiến trúc. Domain-Driven Design, kiến trúc lục giác, ranh giới ngữ cảnh. Đây là chỗ những bài về SOLID và về tách DTO khỏi entity trả công.
Nhắn tin và sự kiện. Kafka hoặc RabbitMQ, mẫu outbox (bài 54), tính nhận lại nhiều lần. Bước tiếp theo tự nhiên khi ứng dụng lớn hơn một tiến trình.
Vận hành sâu. Kubernetes, khả năng quan sát, kỹ thuật độ tin cậy. Bài 51 và 52 là điểm khởi đầu.
Hiệu năng. JMH nghiêm túc, đọc kế hoạch thực thi truy vấn, hiểu tầng bộ nhớ đệm CPU. Ít người đi và rất đáng giá.
Reactive. Spring WebFlux nếu bạn thật sự cần — nhưng bài 55 cho thấy luồng ảo giải quyết phần lớn lý do người ta chọn reactive, với chi phí nhận thức thấp hơn hẳn.
Ba nguồn tôi thật sự dùng
Tài liệu tham chiếu của Spring. Nó dài, nhưng nó chính xác và luôn cập nhật — khác hẳn phần lớn bài hướng dẫn trên mạng, vốn viết cho Spring Boot 2 và vẫn đứng đầu kết quả tìm kiếm.
Ghi chú phát hành mỗi bản nhỏ. Ba trong mười hai bất ngờ ở trên nằm trong đó và tôi đã không đọc.
Mã nguồn Spring. Nghe nản, nhưng khi một hành vi không giải thích được, đọc thẳng lớp cài đặt thường nhanh hơn tìm kiếm. CsrfFilter chỉ vài trăm dòng, và nó trả lời câu hỏi của bài 42 trong mười phút.
Điều cuối
Sê-ri này viết theo cùng nguyên tắc với hai sê-ri trước: không viết điều mình chưa chạy thử.
Nó tốn thời gian hơn nhiều so với chép lại kiến thức có sẵn. Bài 42 tôi phải chạy bốn lần mới ra kết quả đúng — và chính bốn lần đó thành nội dung của bài. Bài 28 suýt kết luận ngược hẳn: -100 trông như khoá hỏng, trong khi nó là bằng chứng khoá hoạt động.
Nhưng nó cũng là lý do sê-ri có mười hai trường hợp ở trên. Nếu chỉ đọc tài liệu rồi viết lại, tôi đã truyền tiếp mười hai điều không còn đúng.
Nếu có một thứ đáng mang theo từ sáu mươi ngày này, tôi nghĩ vẫn là thứ đó: chạy thử đi, rồi hãy tin.
Cảm ơn bạn đã đọc tới đây.