先說辦法,如果看官覺得合適再往下看原理吧
解決辦法
步驟:
1、創建一個專門拋出Filter中異常的Controller及接口方法,例如該接口地址為:/filter/login_auth_fail
1 @RequestMapping("/filter") 2 @RestController 3 public class FilterController { 4 @RequestMapping("/login_auth_fail") 5 public void loginAuthFail(HttpServletRequest request) { 6 // 此處構造一個合適的異常並拋出即可 7 String code = request.getAttribute("code"); 8 throw new CustomException(code); 9 } 10 }
2、在Filter中,需要拋出異常的地方,將異常信息存起來(例如:可以存在HttpServletRequest中),並將請求轉發給上面創建的接口地址:
1 request.setAttribute("code", "xxx"); 2 request.getRequestDispatcher("/filter/login_auth_fail").forward(request, response);
原理
可能大家嘗試過一種攔截辦法:使用@ControllerAdvice和@ExceptionHanlder組合攔截,但並沒有成功攔截Filter中的異常
1 @ControllerAdvice 2 public class ExpHanlder { 3 @ExceptionHandler(Exception.class) 4 public void handle() { 5 // do somthing... 6 } 7 }
那為什么沒有成功呢?關鍵在於@ControllerAdvice只是對Controller做了加強,而Filter在Controller之前進行,故而異常就這樣逃出了咱們的“掌心”。
本文中介紹的辦法,恰是利用了這樣的運行順序,讓異常乖乖地拋出去:既然Filter中不能拋出,那我先把錯誤信息記錄下來,把請求轉發到一個特定的接口(可認為是一個“陷阱”),然后在這個接口中利用記錄的錯誤信息復原一個異常拋出即可。
到這,解決辦法的原理已經介紹完了了,后面內容按需觀看,將和大家一起回顧Filter與Controller的業務流程。
借用Spring 梳理 - filter、interceptor、aop實現與區別 -第二篇中的順序圖:
可以看到,Spring在將請求交給Controller的接口處理前、后分別調用Filter鏈中Filter的方法對處理進行增強。當preHandle中將異常拋出時,並沒有到Controller,故而@ControllerAdvice未能攔截該異常。
筆者是在集成Shiro時,需要保留原有項目功能:在身份驗證失敗或越權時返回JSON格式錯誤信息而遇到了這個問題,因為Shiro是基於Filter做的攔截,故而需要將Filter中的錯誤信息拋出。
項目原先的登陸驗證是放在AOP做的,而AOP也在Filter之后進行,關於Filter、Interceptor、AOP及其異常拋出順序,可以看下這篇文章:Spring:過濾器filter、攔截器interceptor、和AOP的區別與聯系