Filter và interceptor
Nội dung bài · 5 mục
- 1.Khái niệm
- 2.Ví dụ
- 3.Thử ngay
- 4.Lỗi hay gặp
- 5.Tóm tắt
Để đo mỗi request mất bao lâu, bạn không muốn thêm code đo vào từng method của controller. Việc chung cho mọi request như vậy nên làm ở một chỗ, trước khi request tới controller. Chỗ đó là filter chain.
Khái niệm
🚰 Filter chain: chuỗi các bước mà mọi request đi qua trước khi tới controller, rồi response đi ngược lại qua đúng các bước đó.
🧱 Filter: một bước trong filter chain, xử lý request, gọi bước tiếp theo, rồi xử lý response.
🪝 Interceptor: bước do Spring MVC chạy ngay trước và sau method của controller, nằm bên trong filter chain.
Ví dụ
// TimingFilter.java
package com.shop.api;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;
import org.springframework.stereotype.*;
import org.springframework.web.filter.*;
@Component
class TimingFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
long start = System.currentTimeMillis();
chain.doFilter(request, response);
long ms = System.currentTimeMillis() - start;
System.out.println(request.getRequestURI()
+ " " + response.getStatus()
+ " " + ms + "ms");
}
}@Componentbiến filter thành bean, Spring Boot tự thêm nó vào filter chain.OncePerRequestFilterlà class có sẵn của Spring, đảm bảo filter chạy đúng một lần cho mỗi request. Class con viết lại methoddoFilterInternal.- Code trước
chain.doFilter(...)chạy khi request đi vào. Code sau nó chạy khi response đi ra. chain.doFiltercó thể ném hai checked exception, nên method khai báothrows ServletException, IOException.- Gọi
GET /api/productsthì cửa sổ server in ra dòng như/api/products 200 2ms. - Nhiều filter thì đặt thứ tự bằng
@Order(số)phía trên class, số nhỏ chạy trước. Không ghi@Orderthì thứ tự giữa các filter không rõ ràng.
Thử ngay
Xoá TimingFilter.java, thêm hai filter sau:
// AFilter.java
package com.shop.api;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;
import org.springframework.core.annotation.*;
import org.springframework.stereotype.*;
import org.springframework.web.filter.*;
@Component
@Order(1)
class AFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
System.out.println("A vào");
chain.doFilter(request, response);
System.out.println("A ra");
}
}
// BFilter.java
package com.shop.api;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;
import org.springframework.core.annotation.*;
import org.springframework.stereotype.*;
import org.springframework.web.filter.*;
@Component
@Order(2)
class BFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
System.out.println("B vào");
chain.doFilter(request, response);
System.out.println("B ra");
}
}Chạy lại server, gọi curl -i http://localhost:5000/api/products rồi xem
cửa sổ đang chạy server.
Đoán trước khi chạy: bốn dòng in ra theo thứ tự nào?
Xem kết quả
A vào
B vào
B ra
A raRequest đi xuôi A rồi B, tới controller, rồi response đi ngược B rồi A.
Filter có @Order nhỏ hơn thì bọc ngoài filter có @Order lớn hơn.
Lỗi hay gặp
Quên chain.doFilter(...). Request dừng ở filter này, controller không
bao giờ chạy. Client nhận status 200 với body rỗng.
// SAI — không gọi doFilter, request không đi tiếp
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) {
System.out.println("Có request");
}Cách đúng là luôn gọi chain.doFilter(request, response), như
TimingFilter ở phần Ví dụ.
Sửa response sau khi đã gửi. Sau chain.doFilter(...), response có thể
đã bắt đầu gửi về client, nhất là khi body lớn. Header thêm lúc đó bị bỏ qua
mà không báo lỗi. Body nhỏ thì header vẫn có, nên lỗi dễ lọt qua.
// SAI — body lớn thì header đã gửi đi trước
chain.doFilter(request, response);
response.setHeader("X-Shop", "An");Muốn thêm header thì đặt code đó trước chain.doFilter(...).
// ĐÚNG — đổi header khi response chưa gửi
// ShopHeaderFilter.java
package com.shop.api;
import jakarta.servlet.*;
import jakarta.servlet.http.*;
import java.io.IOException;
import org.springframework.stereotype.*;
import org.springframework.web.filter.*;
@Component
class ShopHeaderFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain)
throws ServletException, IOException {
response.setHeader("X-Shop", "An");
chain.doFilter(request, response);
}
}Tóm tắt
- Filter chain là chuỗi filter mà mọi request đi qua.
- Filter xử lý request trước
chain.doFilter(...), xử lý response sau đó. @Orderquyết định thứ tự chạy: số nhỏ bọc ngoài.- Việc chung cho mọi request như đo thời gian, ghi log nên làm ở filter.
- Interceptor (
HandlerInterceptor) chạy sát controller hơn, dùng khi cần biết method nào của controller sẽ xử lý request.
Tự kiểm tra
0/3 câuBa filter X, Y, Z có @Order(1), @Order(2), @Order(3). Response đi ra qua chúng theo thứ tự nào?
Việc nào hợp để làm bằng filter?
Một filter không gọi chain.doFilter(...). Chuyện gì xảy ra với request?