Mọi request HTTP trong Spring đi qua cùng một đường. 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. Đâ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. 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
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

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.

Thử 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.

Ngày mai: @RestController và ánh xạ request.