# 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
```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 來達到,不夠直觀。
例如:
```java
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 時以降序執行。
1. 先建立一個 `FilterTestController`
```java
@RestController
@RequestMapping("/filter")
public class FilterTestController {
@GetMapping("/hello")
@Operation(summary = "Hello")
public String hello() {
System.out.println("hello in FilterController");
return "hello";
}
}
```
2. 建立第一個 `FirstFilter`
```java
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");
}
}
```
3. 建立第二個 `SecondFilter`
```java
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");
}
}
```
4. 建立 `FilterConfig`
```java
@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;
}
}
```
5. 啟動專案,訪問 `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模式
- 可以知道將被使用的控制器方法資訊。
例如:
```java
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;
}
```
接下來在回到我們的專案中來實作它吧。
#### 實作
1. 建立一個 `InterceptorTestController`
```java
@RestController
@RequestMapping("/interceptor")
public class InterceptorTestController {
@GetMapping("/hello")
public String hello() {
System.out.println("hello in InterceptorController");
return "hello";
}
}
```
2. 建立一個 `CustomInterceptor`
```java
@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));
}
}
```
3. 建立 `WebMvcConfig`
```java
@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路徑
}
}
```
4. 啟動專案,訪問 `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=3
```
### Aspect
能拿到方法的參數資訊:Aspect 可以直接訪問被切入方法的參數。
拿不到 HTTP 請求資訊:Aspect 作用於方法級別,默認情況下不直接與 HTTP 層交互。
例如:
```java
@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();
}
```
程式碼範例之前寫過了,我就不在這邊再寫一次了。
有興趣的話,可以來[這邊](https://github.com/mister33221/AOP-example.git)看看。
## 差別整理
# 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
```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 來達到,不夠直觀。
例如:
```java
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 時以降序執行。
1. 先建立一個 `FilterTestController`
```java
@RestController
@RequestMapping("/filter")
public class FilterTestController {
@GetMapping("/hello")
@Operation(summary = "Hello")
public String hello() {
System.out.println("hello in FilterController");
return "hello";
}
}
```
2. 建立第一個 `FirstFilter`
```java
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");
}
}
```
3. 建立第二個 `SecondFilter`
```java
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");
}
}
```
4. 建立 `FilterConfig`
```java
@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;
}
}
```
5. 啟動專案,訪問 `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模式
- 可以知道將被使用的控制器方法資訊。
例如:
```java
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;
}
```
接下來在回到我們的專案中來實作它吧。
#### 實作
1. 建立一個 `InterceptorTestController`
```java
@RestController
@RequestMapping("/interceptor")
public class InterceptorTestController {
@GetMapping("/hello")
public String hello() {
System.out.println("hello in InterceptorController");
return "hello";
}
}
```
2. 建立一個 `CustomInterceptor`
```java
@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));
}
}
```
3. 建立 `WebMvcConfig`
```java
@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路徑
}
}
```
4. 啟動專案,訪問 `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=3
```
### Aspect
能拿到方法的參數資訊:Aspect 可以直接訪問被切入方法的參數。
拿不到 HTTP 請求資訊:Aspect 作用於方法級別,默認情況下不直接與 HTTP 層交互。
例如:
```java
@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();
}
```
程式碼範例之前寫過了,我就不在這邊再寫一次了。
有興趣的話,可以來[這邊](https://github.com/mister33221/AOP-example.git)看看。
## 差別整理
# 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
```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 來達到,不夠直觀。
例如:
```java
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 時以降序執行。
1. 先建立一個 `FilterTestController`
```java
@RestController
@RequestMapping("/filter")
public class FilterTestController {
@GetMapping("/hello")
@Operation(summary = "Hello")
public String hello() {
System.out.println("hello in FilterController");
return "hello";
}
}
```
2. 建立第一個 `FirstFilter`
```java
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");
}
}
```
3. 建立第二個 `SecondFilter`
```java
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");
}
}
```
4. 建立 `FilterConfig`
```java
@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;
}
}
```
5. 啟動專案,訪問 `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模式
- 可以知道將被使用的控制器方法資訊。
例如:
```java
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;
}
```
接下來在回到我們的專案中來實作它吧。
#### 實作
1. 建立一個 `InterceptorTestController`
```java
@RestController
@RequestMapping("/interceptor")
public class InterceptorTestController {
@GetMapping("/hello")
public String hello() {
System.out.println("hello in InterceptorController");
return "hello";
}
}
```
2. 建立一個 `CustomInterceptor`
```java
@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));
}
}
```
3. 建立 `WebMvcConfig`
```java
@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路徑
}
}
```
4. 啟動專案,訪問 `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=3
```
### Aspect
能拿到方法的參數資訊:Aspect 可以直接訪問被切入方法的參數。
拿不到 HTTP 請求資訊:Aspect 作用於方法級別,默認情況下不直接與 HTTP 層交互。
例如:
```java
@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();
}
```
程式碼範例之前寫過了,我就不在這邊再寫一次了。
有興趣的話,可以來[這邊](https://github.com/mister33221/AOP-example.git)看看。
## 差別整理

| 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處理)<br>• 安全相關(JWT Token驗證、基本身份驗證)<br>• 請求/響應修改(添加HTTP頭、壓縮響應內容)<br>• 全局異常處理<br>• 請求日誌記錄 |
| Interceptor | • 性能監控(記錄請求處理時間)<br>• 用戶會話管理<br>• 國際化(設置本地化資訊)<br>• 審計日誌(記錄用戶操作)<br>• 權限檢查(基於角色或權限的訪問控制)<br>• 數據預處理或後處理 |
| Aspect | • 事務管理<br>• 緩存處理<br>• 方法級安全<br>• 詳細的方法調用日誌記錄<br>• 統一的異常處理和日誌記錄<br>• 方法執行時間監控<br>• 參數驗證 |
選擇考慮因素:
1. 需要處理原始HTTP請求和響應時,選擇OncePerRequestFilter。
2. 需要在Spring MVC處理流程中進行干預時,選擇Interceptor。
3. 需要跨多個層實現通用功能或處理方法級別的邏輯時,選擇Aspect。
## 結論
我們用 mermaid 來畫一張一個 http request 進來後,經過 Filter、Interceptor 的流程圖。
```mermaid
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 則是功能強大、彈性也非常大,可以切在你想切得地方,不過也比較複雜,適合用來處理較為複雜的業務邏輯。