Springboot Fileter VS Interceptor VS Aspect
這兩天專案上遇到要在很多地方處理 JWT,我提出 Filter 跟 Interceptor 都可以使用。
查一查發現,對吼,我還有 Aspect 也可以達到一樣的目的。
那到底,這三種到底有甚麼差別?
建立專案
來建立一個專案,後面把三個主題都動手寫寫看。
環境
Project: maven
Java 17
spring boot 3.3.2
Dependencies:
- Spring Web
- Spring Boot DevTools
- Lombok
- org.springdoc 2.6.0
建立一個檔案 config/OpenApiConfig.java
@Configuration
@OpenAPIDefinition(
info = @Info(
title = "Practice Filete, Interceptor, Aspect",
version = "1.0"
)
)
public class OpenApiConfig {
}- 接下來就來實做吧
三者的介紹
Filter
Filter 是 Servlet 的一部分,它可以在請求進入 Servlet 之前,以及響應返回之前,對請求進行處理。 也就是到達 Controller 及離開 Controller 之後,Filter 都可以進行處理。
Filter 的主要特點:
- 可以訪問 URL、headers、parameters 等
- 不知道哪個控制器方法會處理這個請求
- 可以修改請求和響應
- 可以阻止請求進入 Controller,但是是藉由不要進入 chain 來達到,不夠直觀。
例如:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 可以訪問 URL、headers、parameters 等
String url = httpRequest.getRequestURL().toString();
// 但不知道哪個控制器方法會處理這個請求
}通常我們會用來執行一些通用的任務,例如日誌記錄、字符編碼轉換、驗證登錄用戶身份、數據篩選等。
實作
接下來我們會用 Spring 提供的 OncePerRequestFilter 來實現 Filter。
當收到請求時,會經過 doFilterInternal,參數包含一個 FilterChain,他會將所有現有的Filter串起來,進入到 controller 時以升序執行,離開 controller 時以降序執行。
- 先建立一個
FilterTestController
@RestController
@RequestMapping("/filter")
public class FilterTestController {
@GetMapping("/hello")
@Operation(summary = "Hello")
public String hello() {
System.out.println("hello in FilterController");
return "hello";
}
}- 建立第一個
FirstFilter
public class FirstFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
System.out.println("FirstFilter: Request URL before filterChain.doFilter: " + request.getRequestURL() + " Time: " + startTime + "ms");
filterChain.doFilter(request, response);
long endTime = System.currentTimeMillis();
System.out.println("FirstFilter: Request URL after filterChain.doFilter: " + request.getRequestURL() + " Time: " + endTime + "ms");
}
}- 建立第二個
SecondFilter
public class SecondFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
System.out.println("SecondFilter: Request URL before filterChain.doFilter: " + request.getRequestURL() + " Time: " + startTime + "ms");
filterChain.doFilter(request, response);
long endTime = System.currentTimeMillis();
System.out.println("SecondFilter: Request URL after filterChain.doFilter: " + request.getRequestURL() + " Time: " + endTime + "ms");
}
}- 建立
FilterConfig
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean FirstFilter() {
FilterRegistrationBean<FirstFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new FirstFilter());
bean.addUrlPatterns("/filter/*"); // 設定 Filter 的 URL Pattern,只有符合這個 Pattern 的 URL 才會經過這個 Filter
bean.setName("firstFilter"); // 設定 Filter 的名稱,要注意不要跟其他 Filter 重複
bean.setOrder(1); // 設定 Filter 的執行順序
return bean;
}
@Bean
public FilterRegistrationBean SecondFilter() {
FilterRegistrationBean<SecondFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new SecondFilter());
bean.addUrlPatterns("/filter/*"); // 設定 Filter 的 URL Pattern,只有符合這個 Pattern 的 URL 才會經過這個 Filter
bean.setName("secondFilter"); // 設定 Filter 的名稱,要注意不要跟其他 Filter 重複
bean.setOrder(2); // 設定 Filter 的執行順序
return bean;
}
}- 啟動專案,訪問
http://localhost:8080/hello,可以看到 console 有以下輸出
FirstFilter: Request URL before filterChain.doFilter: http://localhost:8080/hello Time: 1721801428816ms
SecondFilter: Request URL before filterChain.doFilter: http://localhost:8080/hello Time: 1721801428816ms
hello in FilterController
SecondFilter: Request URL after filterChain.doFilter: http://localhost:8080/hello Time: 1721801428818ms
FirstFilter: Request URL after filterChain.doFilter: http://localhost:8080/hello Time: 1721801428818ms- 我們可以觀察到,
FirstFilter在SecondFilter之前執行,而在離開 Controller 時,SecondFilter在FirstFilter之前執行。
Interceptor
它可以在Controller處理請求之前、之後,以及完成視圖渲染之後執行特定的邏輯。
Interceptor的主要特點:
- 可以訪問請求和響應對象
- 可以決定請求是否繼續到達Controller,這個非常有用,可以用來實現權限控制。雖然 Filter 也可以藉由不要進入 chain 來達到,但 Interceptor 更可以藉由 return false 來達到。在程式碼的操作、閱讀與理解上會更為直觀。
- 可以在請求處理完成後執行一些後續操作
- 可以應用於特定的URL模式
- 可以知道將被使用的控制器方法資訊。
例如:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 可以訪問 HTTP 請求資訊
String url = request.getRequestURL().toString();
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 可以獲取將被調用的控制器方法資訊
String controllerName = handlerMethod.getBeanType().getSimpleName();
String methodName = handlerMethod.getMethod().getName();
// 但不能直接獲取方法的實際參數值
}
return true;
}接下來在回到我們的專案中來實作它吧。
實作
- 建立一個
InterceptorTestController
@RestController
@RequestMapping("/interceptor")
public class InterceptorTestController {
@GetMapping("/hello")
public String hello() {
System.out.println("hello in InterceptorController");
return "hello";
}
}- 建立一個
CustomInterceptor
@Component
public class CustomInterceptor implements HandlerInterceptor {
/*
preHandle: 在Controller方法執行之前調用
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
System.out.println("Request URL: " + request.getRequestURL().toString());
System.out.println("Start Time: " + System.currentTimeMillis());
return true; // 返回true表示繼續處理請求,返回false則請求將在此終止
}
/*
postHandle: 在Controller方法執行之後調用
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("Request URL: " + request.getRequestURL().toString() +
" Sent to Handler :: Current Time=" + System.currentTimeMillis());
}
/*
afterCompletion: 在Controller方法執行之後,並且在生成視圖之後調用
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
long startTime = (Long) request.getAttribute("startTime");
System.out.println("Request URL: " + request.getRequestURL().toString() +
" :: End Time=" + System.currentTimeMillis());
System.out.println("Time Taken=" + (System.currentTimeMillis() - startTime));
}
}- 建立
WebMvcConfig
@Configuration
@AllArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final CustomInterceptor customInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(customInterceptor)
.addPathPatterns("/interceptor/*"); // 應用於所有URL
// .excludePathPatterns("/admin/**"); // 排除admin路徑
}
}- 啟動專案,訪問
http://localhost:8080/interceptor/hello,可以看到 console 有以下輸出
Request URL: http://localhost:8080/interceptor/hello
Start Time: 1721805129840
hello in InterceptorController
Request URL: http://localhost:8080/interceptor/hello Sent to Handler :: Current Time=1721805129843
Request URL: http://localhost:8080/interceptor/hello :: End Time=1721805129843
Time Taken=3Aspect
能拿到方法的參數資訊:Aspect 可以直接訪問被切入方法的參數。 拿不到 HTTP 請求資訊:Aspect 作用於方法級別,默認情況下不直接與 HTTP 層交互。
例如:
@Around("execution(* com.example.controller.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 可以訪問方法參數
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
// 但不能直接訪問 HTTP 請求資訊
// 如果需要,可以將 HttpServletRequest 作為參數傳遞給方法
return joinPoint.proceed();
}程式碼範例之前寫過了,我就不在這邊再寫一次了。 有興趣的話,可以來這邊看看。
差別整理
Springboot Fileter VS Interceptor VS Aspect
這兩天專案上遇到要在很多地方處理 JWT,我提出 Filter 跟 Interceptor 都可以使用建議。
查一查發現,對吼,我還有 Aspect 也可以達到一樣的目的。
那到底,這三種到底有甚麼差別?
建立專案
來建立一個專案,後面把三個主題都動手寫寫看。
環境
Project: maven
Java 17
spring boot 3.3.2
Dependencies:
- Spring Web
- Spring Boot DevTools
- Lombok
- org.springdoc 2.6.0
建立一個檔案 config/OpenApiConfig.java
@Configuration
@OpenAPIDefinition(
info = @Info(
title = "Practice Filete, Interceptor, Aspect",
version = "1.0"
)
)
public class OpenApiConfig {
}- 接下來就來實做吧
三者的介紹
Filter
Filter 是 Servlet 的一部分,它可以在請求進入 Servlet 之前,以及響應返回之前,對請求進行處理。 也就是到達 Controller 及離開 Controller 之後,Filter 都可以進行處理。
Filter 的主要特點:
- 可以訪問 URL、headers、parameters 等
- 不知道哪個控制器方法會處理這個請求
- 可以修改請求和響應
- 可以阻止請求進入 Controller,但是是藉由不要進入 chain 來達到,不夠直觀。
例如:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 可以訪問 URL、headers、parameters 等
String url = httpRequest.getRequestURL().toString();
// 但不知道哪個控制器方法會處理這個請求
}通常我們會用來執行一些通用的任務,例如日誌記錄、字符編碼轉換、驗證登錄用戶身份、數據篩選等。
實作
接下來我們會用 Spring 提供的 OncePerRequestFilter 來實現 Filter。
當收到請求時,會經過 doFilterInternal,參數包含一個 FilterChain,他會將所有現有的Filter串起來,進入到 controller 時以升序執行,離開 controller 時以降序執行。
- 先建立一個
FilterTestController
@RestController
@RequestMapping("/filter")
public class FilterTestController {
@GetMapping("/hello")
@Operation(summary = "Hello")
public String hello() {
System.out.println("hello in FilterController");
return "hello";
}
}- 建立第一個
FirstFilter
public class FirstFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
System.out.println("FirstFilter: Request URL before filterChain.doFilter: " + request.getRequestURL() + " Time: " + startTime + "ms");
filterChain.doFilter(request, response);
long endTime = System.currentTimeMillis();
System.out.println("FirstFilter: Request URL after filterChain.doFilter: " + request.getRequestURL() + " Time: " + endTime + "ms");
}
}- 建立第二個
SecondFilter
public class SecondFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
System.out.println("SecondFilter: Request URL before filterChain.doFilter: " + request.getRequestURL() + " Time: " + startTime + "ms");
filterChain.doFilter(request, response);
long endTime = System.currentTimeMillis();
System.out.println("SecondFilter: Request URL after filterChain.doFilter: " + request.getRequestURL() + " Time: " + endTime + "ms");
}
}- 建立
FilterConfig
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean FirstFilter() {
FilterRegistrationBean<FirstFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new FirstFilter());
bean.addUrlPatterns("/filter/*"); // 設定 Filter 的 URL Pattern,只有符合這個 Pattern 的 URL 才會經過這個 Filter
bean.setName("firstFilter"); // 設定 Filter 的名稱,要注意不要跟其他 Filter 重複
bean.setOrder(1); // 設定 Filter 的執行順序
return bean;
}
@Bean
public FilterRegistrationBean SecondFilter() {
FilterRegistrationBean<SecondFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new SecondFilter());
bean.addUrlPatterns("/filter/*"); // 設定 Filter 的 URL Pattern,只有符合這個 Pattern 的 URL 才會經過這個 Filter
bean.setName("secondFilter"); // 設定 Filter 的名稱,要注意不要跟其他 Filter 重複
bean.setOrder(2); // 設定 Filter 的執行順序
return bean;
}
}- 啟動專案,訪問
http://localhost:8080/hello,可以看到 console 有以下輸出
FirstFilter: Request URL before filterChain.doFilter: http://localhost:8080/hello Time: 1721801428816ms
SecondFilter: Request URL before filterChain.doFilter: http://localhost:8080/hello Time: 1721801428816ms
hello in FilterController
SecondFilter: Request URL after filterChain.doFilter: http://localhost:8080/hello Time: 1721801428818ms
FirstFilter: Request URL after filterChain.doFilter: http://localhost:8080/hello Time: 1721801428818ms- 我們可以觀察到,
FirstFilter在SecondFilter之前執行,而在離開 Controller 時,SecondFilter在FirstFilter之前執行。
Interceptor
它可以在Controller處理請求之前、之後,以及完成視圖渲染之後執行特定的邏輯。
Interceptor的主要特點:
- 可以訪問請求和響應對象
- 可以決定請求是否繼續到達Controller,這個非常有用,可以用來實現權限控制。雖然 Filter 也可以藉由不要進入 chain 來達到,但 Interceptor 更可以藉由 return false 來達到。在程式碼的操作、閱讀與理解上會更為直觀。
- 可以在請求處理完成後執行一些後續操作
- 可以應用於特定的URL模式
- 可以知道將被使用的控制器方法資訊。
例如:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 可以訪問 HTTP 請求資訊
String url = request.getRequestURL().toString();
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 可以獲取將被調用的控制器方法資訊
String controllerName = handlerMethod.getBeanType().getSimpleName();
String methodName = handlerMethod.getMethod().getName();
// 但不能直接獲取方法的實際參數值
}
return true;
}接下來在回到我們的專案中來實作它吧。
實作
- 建立一個
InterceptorTestController
@RestController
@RequestMapping("/interceptor")
public class InterceptorTestController {
@GetMapping("/hello")
public String hello() {
System.out.println("hello in InterceptorController");
return "hello";
}
}- 建立一個
CustomInterceptor
@Component
public class CustomInterceptor implements HandlerInterceptor {
/*
preHandle: 在Controller方法執行之前調用
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
System.out.println("Request URL: " + request.getRequestURL().toString());
System.out.println("Start Time: " + System.currentTimeMillis());
return true; // 返回true表示繼續處理請求,返回false則請求將在此終止
}
/*
postHandle: 在Controller方法執行之後調用
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("Request URL: " + request.getRequestURL().toString() +
" Sent to Handler :: Current Time=" + System.currentTimeMillis());
}
/*
afterCompletion: 在Controller方法執行之後,並且在生成視圖之後調用
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
long startTime = (Long) request.getAttribute("startTime");
System.out.println("Request URL: " + request.getRequestURL().toString() +
" :: End Time=" + System.currentTimeMillis());
System.out.println("Time Taken=" + (System.currentTimeMillis() - startTime));
}
}- 建立
WebMvcConfig
@Configuration
@AllArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final CustomInterceptor customInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(customInterceptor)
.addPathPatterns("/interceptor/*"); // 應用於所有URL
// .excludePathPatterns("/admin/**"); // 排除admin路徑
}
}- 啟動專案,訪問
http://localhost:8080/interceptor/hello,可以看到 console 有以下輸出
Request URL: http://localhost:8080/interceptor/hello
Start Time: 1721805129840
hello in InterceptorController
Request URL: http://localhost:8080/interceptor/hello Sent to Handler :: Current Time=1721805129843
Request URL: http://localhost:8080/interceptor/hello :: End Time=1721805129843
Time Taken=3Aspect
能拿到方法的參數資訊:Aspect 可以直接訪問被切入方法的參數。 拿不到 HTTP 請求資訊:Aspect 作用於方法級別,默認情況下不直接與 HTTP 層交互。
例如:
@Around("execution(* com.example.controller.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 可以訪問方法參數
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
// 但不能直接訪問 HTTP 請求資訊
// 如果需要,可以將 HttpServletRequest 作為參數傳遞給方法
return joinPoint.proceed();
}程式碼範例之前寫過了,我就不在這邊再寫一次了。 有興趣的話,可以來這邊看看。
差別整理
Springboot Fileter VS Interceptor VS Aspect
這兩天專案上遇到要在很多地方處理 JWT,我提出 Filter 跟 Interceptor 都可以使用建議。
查一查發現,對吼,我還有 Aspect 也可以達到一樣的目的。
那到底,這三種到底有甚麼差別?
建立專案
來建立一個專案,後面把三個主題都動手寫寫看。
環境
Project: maven
Java 17
spring boot 3.3.2
Dependencies:
- Spring Web
- Spring Boot DevTools
- Lombok
- org.springdoc 2.6.0
建立一個檔案 config/OpenApiConfig.java
@Configuration
@OpenAPIDefinition(
info = @Info(
title = "Practice Filete, Interceptor, Aspect",
version = "1.0"
)
)
public class OpenApiConfig {
}- 接下來就來實做吧
三者的介紹
Filter
Filter 是 Servlet 的一部分,它可以在請求進入 Servlet 之前,以及響應返回之前,對請求進行處理。 也就是到達 Controller 及離開 Controller 之後,Filter 都可以進行處理。
Filter 的主要特點:
- 可以訪問 URL、headers、parameters 等
- 不知道哪個控制器方法會處理這個請求
- 可以修改請求和響應
- 可以阻止請求進入 Controller,但是是藉由不要進入 chain 來達到,不夠直觀。
例如:
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
// 可以訪問 URL、headers、parameters 等
String url = httpRequest.getRequestURL().toString();
// 但不知道哪個控制器方法會處理這個請求
}通常我們會用來執行一些通用的任務,例如日誌記錄、字符編碼轉換、驗證登錄用戶身份、數據篩選等。
實作
接下來我們會用 Spring 提供的 OncePerRequestFilter 來實現 Filter。
當收到請求時,會經過 doFilterInternal,參數包含一個 FilterChain,他會將所有現有的Filter串起來,進入到 controller 時以升序執行,離開 controller 時以降序執行。
- 先建立一個
FilterTestController
@RestController
@RequestMapping("/filter")
public class FilterTestController {
@GetMapping("/hello")
@Operation(summary = "Hello")
public String hello() {
System.out.println("hello in FilterController");
return "hello";
}
}- 建立第一個
FirstFilter
public class FirstFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
System.out.println("FirstFilter: Request URL before filterChain.doFilter: " + request.getRequestURL() + " Time: " + startTime + "ms");
filterChain.doFilter(request, response);
long endTime = System.currentTimeMillis();
System.out.println("FirstFilter: Request URL after filterChain.doFilter: " + request.getRequestURL() + " Time: " + endTime + "ms");
}
}- 建立第二個
SecondFilter
public class SecondFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
long startTime = System.currentTimeMillis();
System.out.println("SecondFilter: Request URL before filterChain.doFilter: " + request.getRequestURL() + " Time: " + startTime + "ms");
filterChain.doFilter(request, response);
long endTime = System.currentTimeMillis();
System.out.println("SecondFilter: Request URL after filterChain.doFilter: " + request.getRequestURL() + " Time: " + endTime + "ms");
}
}- 建立
FilterConfig
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean FirstFilter() {
FilterRegistrationBean<FirstFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new FirstFilter());
bean.addUrlPatterns("/filter/*"); // 設定 Filter 的 URL Pattern,只有符合這個 Pattern 的 URL 才會經過這個 Filter
bean.setName("firstFilter"); // 設定 Filter 的名稱,要注意不要跟其他 Filter 重複
bean.setOrder(1); // 設定 Filter 的執行順序
return bean;
}
@Bean
public FilterRegistrationBean SecondFilter() {
FilterRegistrationBean<SecondFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new SecondFilter());
bean.addUrlPatterns("/filter/*"); // 設定 Filter 的 URL Pattern,只有符合這個 Pattern 的 URL 才會經過這個 Filter
bean.setName("secondFilter"); // 設定 Filter 的名稱,要注意不要跟其他 Filter 重複
bean.setOrder(2); // 設定 Filter 的執行順序
return bean;
}
}- 啟動專案,訪問
http://localhost:8080/hello,可以看到 console 有以下輸出
FirstFilter: Request URL before filterChain.doFilter: http://localhost:8080/hello Time: 1721801428816ms
SecondFilter: Request URL before filterChain.doFilter: http://localhost:8080/hello Time: 1721801428816ms
hello in FilterController
SecondFilter: Request URL after filterChain.doFilter: http://localhost:8080/hello Time: 1721801428818ms
FirstFilter: Request URL after filterChain.doFilter: http://localhost:8080/hello Time: 1721801428818ms- 我們可以觀察到,
FirstFilter在SecondFilter之前執行,而在離開 Controller 時,SecondFilter在FirstFilter之前執行。
Interceptor
它可以在Controller處理請求之前、之後,以及完成視圖渲染之後執行特定的邏輯。
Interceptor的主要特點:
- 可以訪問請求和響應對象
- 可以決定請求是否繼續到達Controller,這個非常有用,可以用來實現權限控制。雖然 Filter 也可以藉由不要進入 chain 來達到,但 Interceptor 更可以藉由 return false 來達到。在程式碼的操作、閱讀與理解上會更為直觀。
- 可以在請求處理完成後執行一些後續操作
- 可以應用於特定的URL模式
- 可以知道將被使用的控制器方法資訊。
例如:
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 可以訪問 HTTP 請求資訊
String url = request.getRequestURL().toString();
if (handler instanceof HandlerMethod) {
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 可以獲取將被調用的控制器方法資訊
String controllerName = handlerMethod.getBeanType().getSimpleName();
String methodName = handlerMethod.getMethod().getName();
// 但不能直接獲取方法的實際參數值
}
return true;
}接下來在回到我們的專案中來實作它吧。
實作
- 建立一個
InterceptorTestController
@RestController
@RequestMapping("/interceptor")
public class InterceptorTestController {
@GetMapping("/hello")
public String hello() {
System.out.println("hello in InterceptorController");
return "hello";
}
}- 建立一個
CustomInterceptor
@Component
public class CustomInterceptor implements HandlerInterceptor {
/*
preHandle: 在Controller方法執行之前調用
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
long startTime = System.currentTimeMillis();
request.setAttribute("startTime", startTime);
System.out.println("Request URL: " + request.getRequestURL().toString());
System.out.println("Start Time: " + System.currentTimeMillis());
return true; // 返回true表示繼續處理請求,返回false則請求將在此終止
}
/*
postHandle: 在Controller方法執行之後調用
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
System.out.println("Request URL: " + request.getRequestURL().toString() +
" Sent to Handler :: Current Time=" + System.currentTimeMillis());
}
/*
afterCompletion: 在Controller方法執行之後,並且在生成視圖之後調用
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
Exception ex) throws Exception {
long startTime = (Long) request.getAttribute("startTime");
System.out.println("Request URL: " + request.getRequestURL().toString() +
" :: End Time=" + System.currentTimeMillis());
System.out.println("Time Taken=" + (System.currentTimeMillis() - startTime));
}
}- 建立
WebMvcConfig
@Configuration
@AllArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {
private final CustomInterceptor customInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(customInterceptor)
.addPathPatterns("/interceptor/*"); // 應用於所有URL
// .excludePathPatterns("/admin/**"); // 排除admin路徑
}
}- 啟動專案,訪問
http://localhost:8080/interceptor/hello,可以看到 console 有以下輸出
Request URL: http://localhost:8080/interceptor/hello
Start Time: 1721805129840
hello in InterceptorController
Request URL: http://localhost:8080/interceptor/hello Sent to Handler :: Current Time=1721805129843
Request URL: http://localhost:8080/interceptor/hello :: End Time=1721805129843
Time Taken=3Aspect
能拿到方法的參數資訊:Aspect 可以直接訪問被切入方法的參數。 拿不到 HTTP 請求資訊:Aspect 作用於方法級別,默認情況下不直接與 HTTP 層交互。
例如:
@Around("execution(* com.example.controller.*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 可以訪問方法參數
Object[] args = joinPoint.getArgs();
String methodName = joinPoint.getSignature().getName();
// 但不能直接訪問 HTTP 請求資訊
// 如果需要,可以將 HttpServletRequest 作為參數傳遞給方法
return joinPoint.proceed();
}程式碼範例之前寫過了,我就不在這邊再寫一次了。 有興趣的話,可以來這邊看看。
差別整理
| Filter | Interceptor | 摘要 |
|---|---|---|
Filter 接口定義在 javax.servlet 包中 |
接口 HandlerInterceptor 定義在 org.springframework.web.servlet 包中 |
|
Filter 在 web.xml 中定義 |
||
| Filter 只在 Servlet 前後起作用。 | 攔截器能夠深入到方法前後、異常拋出前後,具有更大的彈性。 | 在 Spring 框架中,優先使用攔截器。攔截器能輕鬆實現 Filter 的所有功能。 |
| Filter 是 Servlet 規範中定義的。 | 攔截器既可用於 Web 程式,也可用於 Application、Swing 程式中。 | 使用範圍不同 |
| Filter 是在 Servlet 規範中定義的,由 Servlet 容器支持。 | 攔截器是在 Spring 容器內,由 Spring 框架支持。 | 規範不同 |
| Filter 不能使用 Spring 容器資源 | 攔截器是 Spring 組件,能使用 Spring 裡的任何資源,通過 IoC 注入。 | 在 Spring 中使用攔截器更容易 |
| Filter 由 Server(如 Tomcat)調用 | Interceptor 由 Spring 調用 | 因此 Filter 總是優先於 Interceptor 執行 |
使用情境
| 機制 | 適合的使用情境 |
|---|---|
| OncePerRequestFilter | • 請求預處理(字符編碼設置、CORS處理) • 安全相關(JWT Token驗證、基本身份驗證) • 請求/響應修改(添加HTTP頭、壓縮響應內容) • 全局異常處理 • 請求日誌記錄 |
| Interceptor | • 性能監控(記錄請求處理時間) • 用戶會話管理 • 國際化(設置本地化資訊) • 審計日誌(記錄用戶操作) • 權限檢查(基於角色或權限的訪問控制) • 數據預處理或後處理 |
| Aspect | • 事務管理 • 緩存處理 • 方法級安全 • 詳細的方法調用日誌記錄 • 統一的異常處理和日誌記錄 • 方法執行時間監控 • 參數驗證 |
選擇考慮因素:
- 需要處理原始HTTP請求和響應時,選擇OncePerRequestFilter。
- 需要在Spring MVC處理流程中進行干預時,選擇Interceptor。
- 需要跨多個層實現通用功能或處理方法級別的邏輯時,選擇Aspect。
結論
我們用 mermaid 來畫一張一個 http request 進來後,經過 Filter、Interceptor 的流程圖。
sequenceDiagram
participant Client
participant Filter
participant Interceptor
participant Controller
Client->>Filter: Request
Filter->>Interceptor: Request
Interceptor->>Controller: Request
Controller->>Interceptor: Response
Interceptor->>Filter: Response
Filter->>Client: Response總的來說,Filter、Interceptor、Aspect 都是 Spring 框架中用於處理請求的機制,各有各的特點和使用情境。
如果很單純只是要處理 HTTP request,Filter 就夠用了。
如果你需要對 HTTP request 做更多的操作及拒絕 request,Interceptor 就是你的選擇。
而 Aspect 則是功能強大、彈性也非常大,可以切在你想切得地方,不過也比較複雜,適合用來處理較為複雜的業務邏輯。