VieTopik
Tiếng HànTiếng AnhIT
  • Góc học tập
Tải app
  • Thư viện
  • Luyện thi
  • Cẩm nang
  • Góc học tập
Spring Boot Web APICấu trúc ứng dụng
Bài 12/19
5 phút

Filter và interceptor

Nội dung bài · 5 mục
  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 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.

Request Filter A Filter B Controller Filter B xử lý response Filter A xử lý response Response
Request đi xuôi qua từng filter, response đi ngược lại

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");
    }
}
  • @Component biến filter thành bean, Spring Boot tự thêm nó vào filter chain.
  • OncePerRequestFilter là 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 method doFilterInternal.
  • Code trước chain.doFilter(...) chạy khi request đi vào. Code sau nó chạy khi response đi ra.
  • chain.doFilter có thể ném hai checked exception, nên method khai báo throws ServletException, IOException.
  • Gọi GET /api/products thì 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 @Order thì 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 ra

Request đ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 đó.
  • @Order quyế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âu
Câu 1

Ba filter X, Y, Z có @Order(1), @Order(2), @Order(3). Response đi ra qua chúng theo thứ tự nào?

Câu 2

Việc nào hợp để làm bằng filter?

Câu 3

Một filter không gọi chain.doFilter(...). Chuyện gì xảy ra với request?

LoggingJPA và repository

Nội dung bài

  1. 1.Khái niệm
  2. 2.Ví dụ
  3. 3.Thử ngay
  4. 4.Lỗi hay gặp
  5. 5.Tóm tắt