Spring Boot攔截器詳解
攔截器(Interceptor)與過濾器(Filter)類似,是面向切面編程的一種具體實現。你可以使用攔截器執行某些任務,比如在控制器處理請求前記錄日志、更新配置等。在 Spring 中,當請求發送到控制器時,在被控制器處理之前,它必須經過攔截器。
攔截器與過濾器的區別
攔截器(Interceptor)和過濾器(Filter)的主要區別在于作用范圍和實現方式。
作用范圍
- 過濾器作用于整個 Web 應用程序,可以過濾所有請求和響應。它是 Servlet 規范的一部分,由 Servlet 容器管理。
- 攔截器通常作用于特定框架。比如,在 Spring Boot 中,它主要攔截特定框架的請求處理流程,并在特定框架內處理請求。
實現方式
- 過濾器實現 javax.servlet.Filter 接口,并在 web.xml 中或通過注解進行配置。需要實現 init()、doFilter() 和 destroy() 等方法。
- 攔截器在不同框架中有不同的實現方式。比如,在 Spring MVC 中,可以實現 HandlerInterceptor 接口或繼承 HandlerInterceptorAdapter 類。需要實現 preHandle()、postHandle() 和 afterCompletion() 等方法。
攔截器的作用
- 登錄驗證和訪問控制:攔截器可用于檢查用戶的登錄狀態和權限,并根據需要執行相關處理。比如,可以使用攔截器驗證用戶的登錄狀態。如果未登錄,則重定向到登錄頁面或返回相應的錯誤信息。
- 異常處理和統一錯誤處理:攔截器可以捕獲并處理請求處理過程中發生的異常。可以根據異常類型執行適當的處理,比如返回自定義錯誤頁面或錯誤信息,或執行特定的錯誤處理邏輯。當然,它還有許多其他應用場景,這里不再一一列舉。
自定義攔截器
如果你需要自定義攔截器,必須實現 org.springframework.web.servlet.HandlerInterceptor 接口或繼承 org.springframework.web.servlet.handler.HandlerInterceptorAdapter 類,并且需要重寫以下三個方法:
- **preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)**:此方法在請求處理之前調用。該方法在攔截器類中首先執行,用于一些預初始化操作或對當前請求進行預處理。你還可以進行一些判斷,以確定請求是否應該繼續。此方法的返回值為布爾類型。當它返回 false 時,表示請求結束,后續的攔截器和控制器將不再執行。當它返回 true 時,將調用下一個攔截器的 preHandle 方法。如果已經是最后一個攔截器,則將調用當前請求的控制器方法。
- **postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView)**:此方法在當前請求處理完成后執行,即在控制器方法被調用之后。但是,它將在 DispatcherServlet 渲染視圖之前被調用。因此,我們可以在此方法中在控制器處理后對 ModelAndView 對象進行操作。
- **afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex)**:只有當相應攔截器類的 postHandle 方法的返回值為 true 時,此方法才會執行。顧名思義,此方法將在整個請求結束后執行,即在 DispatcherServlet 渲染相應視圖之后。此方法主要用于資源清理。
接下來,讓我們通過實際代碼學習。以用戶登錄權限驗證為例:
用戶登錄權限驗證
1.自定義攔截器
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Component
publicclass LoginInterceptor implements HandlerInterceptor {
// 在調用目標方法之前執行的方法。
// 返回 true 表示攔截器驗證成功,執行目標方法。
// 返回 false 表示攔截器驗證失敗,不執行后續業務邏輯。
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 用戶登錄判斷業務。
HttpSession session = request.getSession(false);
if (session!= null && session.getAttribute("session_userinfo")!= null) {
// 用戶已登錄。
returntrue;
}
response.setStatus(401);
returnfalse;
}
}
代碼中的 preHandle 方法是攔截器的主要方法,在目標方法被調用之前執行。它接收三個參數:HttpServletRequest 對象表示當前 HTTP 請求,HttpServletResponse 對象表示當前 HTTP 響應,Object handler 表示被攔截的處理器(通常是控制器中的一個方法)。
在 preHandle 方法中,首先通過 request.getSession(false)(如果存在)獲取當前請求的 HttpSession 對象,然后判斷這個 HttpSession 對象是否為 null 以及是否存在名為“session_userinfo”的屬性。
如果這個條件為真,則表示用戶已登錄,可以繼續執行后續業務,所以返回 true。否則,驗證失敗,將 HTTP 響應的狀態碼設置為 401,表示未授權,然后返回 false,不再繼續執行后續業務。
2.將自定義攔截器添加到系統配置中
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
publicclass MyConfig implements WebMvcConfigurer {
// 注入。
@Autowired
private LoginInterceptor loginInterceptor;
// 添加攔截器。
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**") // 攔截所有 URL。
.excludePathPatterns("/user/login") // 排除 URL:/user/login(登錄)。
.excludePathPatterns("/user/reg") // 排除 URL:/user/reg(注冊)。
.excludePathPatterns("/image/**") // 排除“image”文件夾下的所有文件。
.excludePathPatterns("/**/*.js") // 排除任意深度目錄下的所有“.js”文件。
.excludePathPatterns("/**/*.css");
}
}
在配置類中,重寫 addInterceptors 方法。此方法用于注冊攔截器。在這里,通過調用 InterceptorRegistry 的 addInterceptor 方法添加攔截器,并設置攔截路徑和排除路徑。
具體來說,通過調用 addInterceptor(loginInterceptor) 添加 LoginInterceptor 攔截器。然后使用 addPathPatterns 方法指定需要攔截的 URL 路徑模式。這里,“/**”用于表示攔截所有 URL。使用 excludePathPatterns 方法排除一些不會被攔截的特定 URL 路徑。
3.用戶控制器
@RestController
@RequestMapping("/user")
publicclass UserController {
@RequestMapping("/login")
public String login() {
return"login";
}
@RequestMapping("/index")
public String index() {
return"index";
}
@RequestMapping("/reg")
public String reg() {
return"reg";
}
}
使用瀏覽器訪問路徑/user/login 的結果輸出如下:
然后,使用瀏覽器訪問路徑/user/index 的結果輸出如下:
可以看到,返回了 401 錯誤,這是預期的結果。
然后,使用瀏覽器訪問路徑/user/reg 的結果輸出如下:
添加統一訪問前綴
在 WebMvcConfigurer 接口中,configurePathMatch 方法用于配置路徑匹配規則。這里我們給所有請求地址添加前綴“pre”。
@Configuration
public class MyConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.addPathPrefix("pre", new Predicate<Class<?>>() {
@Override
public boolean test(Class<?> aClass) {
return true;
}
});
}
}
在這個例子中,傳遞給 addPathPrefix 方法的前綴是“pre”,Predicate 對象是一個實現了 Predicate<Class<?>>接口的匿名內部類。Predicate 接口是 Java 8 中引入的一個函數式接口,其 test 方法用于判斷傳入的類是否滿足條件。
在這個匿名內部類中,重寫 test 方法使其始終返回 true,這意味著所有類都滿足條件,并且將添加統一的訪問前綴。
因此,通過這段代碼的配置,所有請求路徑都將在前面添加“pre”前綴。比如,如果原始路徑是“example”,添加前綴后的路徑變為“/pre/example”。這樣可以實現對請求路徑的統一處理。
注意:如果添加了前綴,攔截器的排除路徑也應該相應更改。
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor)
.excludePathPatterns("/pre/user/login") // 排除 URL:/user/login(登錄)。
.excludePathPatterns("/pre/user/reg") // 排除 URL:/user/reg(注冊)。
.addPathPatterns("/**") // 攔截所有 URL。
.excludePathPatterns("/pre/image/**") // 排除“image”文件夾下的所有文件。
.excludePathPatterns("/pre/**/*.js") // 排除任意深度目錄下的所有“.js”文件。
.excludePathPatterns("/pre/**/*.css");
}
配置本地資源映射路徑
實現 WebMvcConfigurer 接口并重寫 addResourceHandlers(ResourceHandlerRegistry registry) 方法,其中:
- addResourceHandler() 添加訪問路徑;
- addResourceLocations() 添加映射的真實路徑。映射的真實路徑末尾必須跟“/”,否則無法映射。“/”在 Windows 和 Linux 中都適用。
示例代碼:
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
publicclass MyWebMVCConfig implements WebMvcConfigurer {
@Value("${file.location}") // D:/test/
private String fileLocation;
@Value("${file.path}") // /file/**
private String filePath;
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 當資源處理器匹配時,將 URL 映射到位置,即本地文件夾。
registry.addResourceHandler(filePath).addResourceLocations("file:///" + fileLocation); // 這里的最后一個“/”不能省略。
}
}
這段代碼將配置一個攔截器。如果訪問路徑是 addResourceHandler 中的路徑,那么它將被映射到 addResourceLocations 參數中的路徑。這樣,其他人就可以訪問服務器上的本地文件,如本地圖片或本地音樂視頻。
統一異常處理
猜猜訪問以下代碼后會返回什么?
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login() {
Object object = null;
object.hashCode();
return "login";
}
}
答案是:
有沒有一種方法可以在發生異常時返回有用信息,而不是這樣混亂的錯誤消息?這就是統一異常處理。
@ControllerAdvice
@ResponseBody
public class MyExceptionAdvice {
@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> handleNPE(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", 1001);
result.put("msg", "npe:" + e.getMessage());
result.put("data", null);
return result;
}
}
- @ControllerAdvice 注解表示這個類是一個全局異常處理器。它將捕獲應用程序中拋出的異常并執行相應的處理邏輯。
- @ExceptionHandler(NullPointerException.class) 注解指定處理 NullPointerException 類型異常的方法 handleNPE()。
- handleNPE() 方法的參數是一個 NullPointerException 類型的異常對象,表示具體捕獲的異常實例。
- handleNPE() 方法返回一個 HashMap<String, Object>對象,用于封裝異常處理結果。
這段代碼的作用是,當捕獲到 NullPointerException 異常時,執行 handleNPE() 方法并返回一個包含異常處理結果的 HashMap 對象。這個結果將以 JSON 格式返回給客戶端。
訪問 localhost:8080/user/login 的結果如下:
當有多個異常處理器時,處理順序如下: 異常處理器按照它們在代碼中定義的順序執行。如果一個異常匹配多個異常處理器,將首先執行與異常類型更具體匹配的那個。如果沒有具體匹配,則可能考慮更通用的異常處理器。
比如,如果有一個針對異常的特定子類的異常處理器和另一個針對更通用的超類異常的異常處理器,將優先執行針對特定子類的處理器。
@ControllerAdvice
@ResponseBody
publicclass MyExceptionAdvice {
@ExceptionHandler(NullPointerException.class)
public HashMap<String, Object> handleNPE(NullPointerException e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", -1);
result.put("msg", "npe:" + e.getMessage());
result.put("data", null);
return result;
}
@ExceptionHandler(Exception.class)
public HashMap<String, Object> handleAllException(Exception e) {
HashMap<String, Object> result = new HashMap<>();
result.put("code", -1);
result.put("msg", "Exception:" + e.getMessage());
result.put("data", null);
return result;
}
}
再次嘗試訪問/user/login。
結論是:如果有匹配,子類優先。如果沒有匹配,查找父類。