Hình dung một request như một bưu phẩm đi qua trung tâm chia thư. Ở cổng ngoài, mọi kiện đều bị soát cả lúc vào lẫn lúc ra; ở giữa có đúng một quầy điều phối đọc địa chỉ rồi giao cho đúng nhân viên. Mọi request HTTP trong Spring đi qua cùng một đường như vậy, và bài này in ra đường đó.
Sáu bước, in ra thật
[1] Filter thứ nhất - VÀO
[2] Filter thứ hai
[3] Interceptor.preHandle
[4] handler
[5] Interceptor.afterCompletion
[6] Filter thứ nhất - RA
Đây là output thật từ một request GET /thutu, với hai Filter và một HandlerInterceptor.
Chú ý cấu trúc lồng nhau: Filter thứ nhất bao ngoài tất cả, và phần sau chain.doFilter() của nó chạy cuối cùng — bạn qua cổng ngoài lúc vào, và qua lại đúng cổng ấy lúc ra. Đây đúng mô hình middleware ở bài 48 sê-ri Go.
Đường đi đầy đủ
Tomcat (kết nối, phân tích HTTP)
-> Servlet Filter chain (tầng Servlet, KHÔNG biết gì về Spring MVC)
-> DispatcherServlet (bộ điều phối trung tâm của Spring)
-> HandlerMapping (URL nào -> method nào)
-> HandlerInterceptor.preHandle
-> HandlerAdapter -> @Controller method
-> HttpMessageConverter (đối tượng -> JSON)
-> Interceptor.postHandle / afterCompletion
DispatcherServlet là "front controller": một servlet nhận mọi request rồi phân phối — đúng cái quầy điều phối trung tâm đọc địa chỉ trên mọi kiện thư. Bạn không bao giờ viết HttpServlet trong Spring MVC — bạn viết @Controller và Dispatcher tìm ra chúng.
Nó được đăng ký tự động bởi auto-configuration ở bài 8, ánh xạ vào /.
Filter khác Interceptor
Đây là câu hỏi phỏng vấn kinh điển, và khác biệt thật sự là tầng:
| Filter | Interceptor | |
|---|---|---|
| Thuộc về | Servlet API | Spring MVC |
| Chạy | trước và sau DispatcherServlet |
bên trong Dispatcher |
| Biết handler nào sẽ chạy | không | có |
| Sửa được request/response | có, kể cả bọc lại | hạn chế hơn |
| Thấy được | mọi request, kể cả tài nguyên tĩnh | chỉ request có handler |
Cổng ngoài thấy mọi kiện đi qua — kể cả hàng số lượng lớn, thư rác (tài nguyên tĩnh) — nhưng chưa biết kiện này về tay ai. Chốt tại quầy chỉ gặp thư đã được định tuyến, nên nó biết đích xác nhân viên nào.
Dùng Filter khi: cần xử lý mọi request kể cả tài nguyên tĩnh, cần bọc request hay response (nén, đọc lại body), cần chạy trước cả Spring Security.
Dùng Interceptor khi: cần biết handler nào sắp chạy, cần đọc chú thích trên method, hoặc chỉ quan tâm request vào controller.
Với đa số nhu cầu — log, mã tương quan, đo thời gian — cả hai đều làm được. Tôi thường chọn Filter vì nó không phụ thuộc Spring MVC và dùng lại được.
Thứ tự Filter
@Component @Order(1) class Filter1 implements Filter { }
@Component @Order(2) class Filter2 implements Filter { }
@Order nhỏ chạy trước. Không khai thì thứ tự không xác định — và đó là nguồn lỗi khó tìm khi một filter phụ thuộc filter khác.
Cách khai rõ ràng hơn, cho phép giới hạn đường dẫn:
@Bean
FilterRegistrationBean<Filter1> dangKy(Filter1 f) {
var b = new FilterRegistrationBean<>(f);
b.setOrder(1);
b.addUrlPatterns("/api/*");
return b;
}
Filter của Spring Security nằm ở order -100 theo mặc định, nên nó chạy trước filter của bạn — bài 37 sẽ nói.
Interceptor có ba móc
public boolean preHandle(...) // trước handler; trả false để CHẶN
public void postHandle(...) // sau handler, TRƯỚC khi render view
public void afterCompletion(...) // sau khi xong, kể cả có ngoại lệ
Trong output ở đầu bài, postHandle không xuất hiện vì tôi không cài nó. Chú ý afterCompletion chạy kể cả khi handler ném ngoại lệ — đó là chỗ đúng cho việc dọn dẹp.
preHandle trả false sẽ dừng chuỗi. Nhớ tự ghi response, nếu không client nhận 200 với thân rỗng.
@RestController khác @Controller
@Controller // trả về TÊN VIEW, cần ViewResolver
@RestController // = @Controller + @ResponseBody, trả về DỮ LIỆU
@RestController là thứ bạn dùng cho API. Giá trị trả về đi qua HttpMessageConverter để thành JSON — bài 15 sẽ nói.
Servlet hay Reactive
Spring có hai stack web hoàn toàn khác nhau:
Spring MVC (servlet) — mỗi request một luồng, chặn. Đây là thứ bài này nói và là mặc định.
Spring WebFlux (reactive) — không chặn, ít luồng, lập trình theo luồng dữ liệu.
WebFlux từng là câu trả lời cho vấn đề "một luồng mỗi request quá tốn". Nhưng với luồng ảo Java 21, Spring MVC chạy được mô hình một-luồng-mỗi-request mà không tốn kém — bài 50 sẽ đo.
Lời khuyên cho dự án mới trên Java 21: dùng Spring MVC cộng luồng ảo. Mã đơn giản hơn nhiều, gỡ lỗi dễ hơn, và hiệu năng tương đương cho phần lớn tải.
Nếu chỉ thử một thứ sau bài này, bật cho Dispatcher kể lại đường đi của nó trong ba mươi giây:
logging:
level:
org.springframework.web.servlet.DispatcherServlet: DEBUG
Gọi một endpoint và đọc log. Bạn sẽ thấy Dispatcher chọn handler nào, chọn converter nào, và mất bao lâu ở mỗi bước — hữu ích nhất khi một request khớp nhầm handler.
Mẫu số chung
Mẫu front controller — một bộ điều phối trung tâm nhận mọi request rồi định tuyến — có ở khắp nơi: DispatcherServlet của Spring, nhưng cũng là router của Express/Koa, URL dispatcher của Django, router của Rails, pipeline của ASP.NET, cái index.php duy nhất của các framework PHP. Bạn viết handler; framework sở hữu cửa vào và việc định tuyến. Ý cốt lõi là gom logic cửa-vào cắt-ngang vào một chỗ thay vì rải nó ra từng endpoint.
Điều thứ hai: một request đi qua một củ hành nhiều lớp bọc, và một mối quan tâm nằm ở lớp nào quyết định nó thấy được gì và làm được gì — toàn bộ câu trả lời Filter-hay-Interceptor là "ở tầng nào", không phải "làm gì". Lớp ngoài (Filter, ở rìa Servlet) thấy mọi request kể cả tài nguyên tĩnh và bọc lại được request/response, nhưng chưa biết đích; lớp trong (Interceptor, ở tầng MVC) chỉ chạy cho request đã định tuyến và biết handler cùng chú thích của nó. Đây đúng là kiểu middleware lồng nhau của mọi stack — middleware của Express, chuỗi http.Handler của Go, Rack, pipeline của ASP.NET — một luồng đối xứng vào-rồi-ra, lớp ngoài tổng quát còn lớp trong thì hiểu biết hơn. Bản năng cần có: đặt một mối quan tâm cắt-ngang ở lớp ngoài nhất mà vẫn có đủ thông tin nó cần, và không ra xa hơn thế — ngoài thì thấy tất cả mà biết ít, trong thì biết nhiều mà chỉ thấy cái đã tới tay.
Ngày mai: @RestController và ánh xạ request.