Spring統一功能處理
- 攔截器
- 攔截器
- 什么是攔截器
- 攔截器的基本使用
- 定義攔截器
- 注冊配置攔截器
- 攔截器詳解
- 攔截器的攔截路徑配置
- 攔截器實現原理
- 初始化
- 處理請求
- 適配器模式
- 統一數據返回格式
- 統一數據返回格式快速入門
- 統一異常處理
攔截器
場景: 我們要對一個網站實現強制登陸的功能,后端根據Session來判斷用戶是否登錄,但是如果我們要這樣實現,就需要對每一個接口都增加這樣的邏輯處理 此時就比較麻煩
? 需要修改每個接?的處理邏輯
? 需要修改每個接?的返回結果
? 接?定義修改, 前端代碼也需要跟著修改
有沒有更簡單的辦法, 統?攔截所有的請求, 并進?Session校驗呢, 這?我們學習?種新的解決辦法: 攔截器
攔截器
什么是攔截器
攔截器是Spring框架提供的核?功能之?, 主要?來攔截??的請求, 在指定?法前后, 根據業務需要執?預先設定的代碼
也就是說, 允許開發?員提前預定義?些邏輯, 在??的請求響應前后執?. 也可以在??請求前阻?
其執?.
在攔截器當中,開發?員可以在應?程序中做?些通?性的操作, ?如通過攔截器來攔截前端發來的
請求, 判斷Session中是否有登錄??的信息. 如果有就可以放?, 如果沒有就進?攔截.
攔截器的基本使用
定義攔截器
實現HandleInterceptor接口 重寫方法
@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {log.info("LoginInterceptor 目標方法執行前");return true;}@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {log.info("LoginInterceptor 目標方法執行后");}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {log.info("LoginInterceptor 視圖執行后");}
}
preHandle方法:目標方法執行前執行,返回true 繼續執行后續操作,返回false 中斷后續操作
postHandle方法:目標方法執行后執行
afterCompletion()?法:視圖渲染完畢后執?,最后執?
注冊配置攔截器
實現WebMvcConfigurer接口 重寫addInterceptors方法
@Configuration
public class WebConfig implements WebMvcConfigurer {//?定義的攔截器對象@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//注冊?定義攔截器對象registry.addInterceptor(loginInterceptor).addPathPatterns("/**");//設置攔截器攔截的請求路徑}
}
攔截器詳解
攔截器的攔截路徑配置
攔截路徑是指我們定義的這個攔截器, 對哪些請求?效.我們在注冊配置攔截器的時候, 通過 addPathPatterns()?法指定要攔截哪些請求. 也可以通過excludePathPatterns() 指定不攔截哪些請求
@Configuration
public class WebConfig implements WebMvcConfigurer {//自定義攔截器對象@Autowiredprivate LoginInterceptor loginInterceptor;@Overridepublic void addInterceptors(InterceptorRegistry registry) {//設置攔截器的請求路徑// /**表示攔截所有registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login").excludePathPatterns("/**/*.js").excludePathPatterns("/**/*.css").excludePathPatterns("/**/*.png").excludePathPatterns("/**/*.html");//addPath設置攔截那些請求//excludePath設置不攔截哪些請求}
}
在攔截器中除了可以設置 /** 攔截所有資源外,還有?些常?攔截路徑設置:
攔截路徑 | 含義 | 舉例 |
---|---|---|
/* | 一級路徑 | 能匹配/user,/book,/login,不能匹配 /user/login |
/** | 任意級路徑 | 能匹配/user,/user/login,/user/reg |
/book/* | /book下的?級路徑 | 能匹配/book/addBook,不能匹配/book/addBook/1,/book |
/book/** | /book下的任意級路徑 | 能匹配/book,/book/addBook,/book/addBook/2,不能匹配/user/login |
添加攔截器后, 執?Controller的?法之前, 請求會先被攔截器攔截住. 執? preHandle() ?法,
這個?法需要返回?個布爾類型的值.
如果返回true, 就表?放?本次操作, 繼續訪問controller中的?法.
如果返回false,則不會放?(controller中的?法也不會執?).
controller當中的?法執?完畢后,再回過來執? postHandle() 這個?法以及afterCompletion() ?法,執?完畢之后,最終給瀏覽器響應數據.
攔截器實現原理
當Tomcat啟動之后, 有?個核?的類DispatcherServlet, 它來控制程序的執?順序.
所有請求都會先進到DispatcherServlet,執?doDispatch 調度?法. 如果有攔截器,會先執?攔截器preHandle() ?法的代碼, 如果 preHandle() 返回true, 繼續訪問controller中的?法.controller當中的?法執?完畢后,再回過來執? postHandle() 和 afterCompletion(),返回給DispatcherServlet, 最終給瀏覽器響應數據.
初始化
DispatcherServlet的初始化?法 init() 在其?類 HttpServletBean 中實現的.
主要作?是加載 web.xml 中 DispatcherServlet 的 配置, 并調??類的初始化.
在 HttpServletBean 的 init() 中調?了 initServletBean() , 它是在FrameworkServlet 類中實現的, 主要作?是建? WebApplicationContext 容器(有時也稱上下?), 并加載 SpringMVC 配置?件中定義的 Bean到該容器中, 最后將該容器添加到 ServletContext 中. 下?是initServletBean() 的具體代碼:
初始化web容器的過程中, 會通過onRefresh 來初始化SpringMVC的容器
在initStrategies()中進?9?組件的初始化, 如果沒有配置相應的組件,就使?默認定義的組件(在
DispatcherServlet.properties中有配置默認的策略, ?致了解即可
?法initMultipartResolver、initLocaleResolver、initThemeResolver、initRequestToViewNameTranslator、initFlashMapManager的處理?式?乎都?樣(1.2.3.7.8,9),從應??中取出指定的Bean, 如果沒有, 就使?默認的.?法initHandlerMappings、initHandlerAdapters、initHandlerExceptionResolvers的處理?式?乎都?樣(4,5,6)
- 初始化處理器映射器HandlerMappings:處理器映射器作?,1)通過處理器映射器找到對應的處理器適配器,將請求交給適配器處理;2)緩存每個請求地址URL對應的位置(Controller.xxx?法);如果在ApplicationContext發現有HandlerMappings,則從ApplicationContext中獲取到所有的HandlerMappings,并進?排序;如果在ApplicationContext中沒有發現有處理器映射器,則默認BeanNameUrlHandlerMapping作為處理器映射器
- 初始化處理器適配器HandlerAdapter:作?是通過調?具體的?法來處理具體的請求;如果在ApplicationContext發現有handlerAdapter,則從ApplicationContext中獲取到所有的 HandlerAdapter,并進?排序;如果在ApplicationContext中沒有發現處理器適配器,則不設置異常處理器,則默認SimpleControllerHandlerAdapter作為處理器適配器
- 初始化異常處理器解析器HandlerExceptionResolver:如果在ApplicationContext發現有handlerExceptionResolver,則從ApplicationContext中獲取到所有的HandlerExceptionResolver,并進?排序;如果在ApplicationContext中沒有發現異常處理器解析器,則不設置異常處理器
處理請求
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {HttpServletRequest processedRequest = request;HandlerExecutionChain mappedHandler = null;boolean multipartRequestParsed = false;WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);try {ModelAndView mv = null;Exception dispatchException = null;try {processedRequest = checkMultipart(request);multipartRequestParsed = (processedRequest != request);//1.獲取執行鏈//遍歷所有的HandlerMapper 找到與請求對應的HandlermappedHandler = getHandler(processedRequest);if (mappedHandler == null) {noHandlerFound(processedRequest, response);return;}//2. 獲取適配器//遍歷所有的HandlerAdapter 找到可以處理該Handler的HandlerAdapterHandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());String method = request.getMethod();boolean isGet = HttpMethod.GET.matches(method);if (isGet || HttpMethod.HEAD.matches(method)) {long lastModified = ha.getLastModified(request, mappedHandler.getHandler());if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {return;}}//3. 執行攔截器的preHandler方法if (!mappedHandler.applyPreHandle(processedRequest, response)) {return;}//4. 執行目標方法mv = ha.handle(processedRequest, response, mappedHandler.getHandler());if (asyncManager.isConcurrentHandlingStarted()) {return;}applyDefaultViewName(processedRequest, mv);//5. 執行攔截器的postHandle方法mappedHandler.applyPostHandle(processedRequest, response, mv);}catch (Exception ex) {dispatchException = ex;}catch (Throwable err) {// As of 4.3, we're processing Errors thrown from handler methods as well,// making them available for @ExceptionHandler methods and other scenarios.dispatchException = new NestedServletException("Handler dispatch failed", err);}//6.處理視圖 處理之后執?攔截器afterCompletion?法processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);}catch (Exception ex) {//7.執?攔截器afterCompletion?法triggerAfterCompletion(processedRequest, response, mappedHandler, ex);}catch (Throwable err) {triggerAfterCompletion(processedRequest, response, mappedHandler,new NestedServletException("Handler processing failed", err));}finally {if (asyncManager.isConcurrentHandlingStarted()) {// Instead of postHandle and afterCompletionif (mappedHandler != null) {mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);}}else {// Clean up any resources used by a multipart request.if (multipartRequestParsed) {cleanupMultipart(processedRequest);}}}}
此處最關鍵的就是3 4 5 點 這里規定了執行目標方法前執行preHandle 執行目標方法之后執行postHandle方法
適配器模式
適配器模式是一種設計模式,用于將一個類的接口轉換成客戶端所期望的另一個接口。它允許原本不兼容的類能夠合作無間。
適配器模式主要包括兩個核心角色:目標接口(Target)和適配器(Adapter)。目標接口是客戶端所期望的接口,適配器則是將原本不兼容的類轉換成目標接口的中間層。
適配器模式可以通過兩種方式實現:類適配器和對象適配器。在類適配器中,適配器繼承了被適配類,并實現了目標接口。而在對象適配器中,適配器持有一個被適配對象的實例,并實現了目標接口。
使用適配器模式可以有以下幾個好處:
- 可以讓原本不兼容的類能夠一起工作,提高代碼的復用性。
- 可以封裝已有的類,對外隱藏底層的實現細節。
- 可以在不修改現有代碼的情況下引入新的功能。
總之,適配器模式是一種常用的設計模式,可用于解決不同接口之間不兼容的問題,使得原本無法合作的類能夠協同工作。
HandlerAdapter 在 Spring MVC 中使?了適配器模式
HandlerAdapter 主要?于?持不同類型的處理器(如 Controller、HttpRequestHandler 或者Servlet 等),讓它們能夠適配統?的請求處理流程。這樣,Spring MVC 可以通過?個統?的接?來處理來?各種處理器的請求
場景: 前?學習的slf4j 就使?了適配器模式, slf4j提供了?系列打印?志的api, 底層調?的是log4j 或者logback來打?志, 我們作為調?者, 只需要調?slf4j的api就?了
//Slf4j接口
interface Slf4jApi{void log(String message);
}//log4j接口
class Log4j{void log4jLog(String message){System.out.println("Log4j打印:" + message);}
}//slf4j和log4j適配器class Slf4jLog4JAdapter implements Slf4jApi{private Log4j log4j;public Slf4jLog4JAdapter(Log4j log4j) {this.log4j = log4j;}@Overridepublic void log(String message) {log4j.log4jLog(message);}
}
public class Slf4jDemo {public static void main(String[] args) {Slf4jApi slf4jApi = new Slf4jLog4JAdapter(new Log4j());slf4jApi.log("使用slf4j打印日志");}
}
適配器模式應?場景
?般來說,適配器模式可以看作?種"補償模式",?來補救設計上的缺陷. 應?這種模式算是"?奈之舉", 如果在設計初期,我們就能協調規避接?不兼容的問題, 就不需要使?適配器模式了
所以適配器模式更多的應?場景主要是對正在運?的代碼進?改造, 并且希望可以復?原有代碼實現新的功能. ?如版本升級等
統一數據返回格式
強制登錄案例中, 我們共做了兩部分?作
- 通過Session來判斷??是否登錄
- 對后端返回數據進?封裝, 告知前端處理的結果
后端統一返回結果
package com.bite.book.model;import com.bite.book.enums.ResultCode;
import lombok.Data;@Data
public class Result<T> {/*** 業務狀態碼*/private ResultCode code; //0-成功 -1 失敗 -2 未登錄/*** 錯誤信息*/private String errMsg;/*** 數據*/private T data;public static <T> Result<T> success(T data){Result result = new Result();result.setCode(ResultCode.SUCCESS);result.setErrMsg("");result.setData(data);return result;}public static <T> Result<T> fail(String errMsg){Result result = new Result();result.setCode(ResultCode.FAIL);result.setErrMsg(errMsg);result.setData(null);return result;}public static <T> Result<T> fail(String errMsg,Object data){Result result = new Result();result.setCode(ResultCode.FAIL);result.setErrMsg(errMsg);result.setData(data);return result;}public static <T> Result<T> unlogin(){Result result = new Result();result.setCode(ResultCode.UNLOGIN);result.setErrMsg("用戶未登錄");result.setData(null);return result;}}
后端返回接口
@RequestMapping("/getBookListByPage")public Result getBookListByPage(PageRequest pageRequest, HttpSession session){log.info("查詢翻頁信息, pageRequest:{}",pageRequest);//校驗成功if (pageRequest.getPageSize()<0 || pageRequest.getCurrentPage()<1){return Result.fail("參數校驗失敗");}PageResult<BookInfo> bookInfoPageResult = null;try {bookInfoPageResult = bookService.selectBookInfoByPage(pageRequest);//此處對返回的數據進行再次封裝return Result.success(bookInfoPageResult);}catch (Exception e){log.error("查詢翻頁信息錯誤,e:{}",e);return Result.fail(e.getMessage());}}
攔截器幫我們實現了第?個功能, 接下來看SpringBoot對第?個功能如何?持
統一數據返回格式快速入門
統一的數據返回格式使用@ControllerAdvice
和 ResponseBodyAdvice
的方式實現
@ControllerAdvice表示控制器通知類
添加類 ResponseAdvice , 實現 ResponseBodyAdvice 接?, 并在類上添加 @ControllerAdvice 注解
@ControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice {@Autowired//轉jsonprivate ObjectMapper objectMapper;//判斷是否要執行beforeBodyWrite方法//ture為執行//false不執行@Overridepublic boolean supports(MethodParameter returnType, Class converterType) {return true;}@SneakyThrows@Overridepublic Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {return Result.success(body);}
}
supports方法: 判斷是否要執行beforeBodyWrite方法, true為執行 false不執行, 通過該方法可以選擇那些類或者哪些方法的response要進行處理 其他的不處理
beforeBodyWrite方法: 對response方法進行具體操作處理
統一異常處理
統?異常處理使?的是 @ControllerAdvice
+ @ExceptionHandler
來實現的,@ControllerAdvice
表?控制器通知類, @ExceptionHandler
是異常處理器,兩個結合表?當出現異常的時候執?某個通知,也就是執?某個?法事件
package com.bite.book.config;import com.bite.book.model.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;@ControllerAdvice
@ResponseBody
@Slf4j
public class ErrorHandler {@ExceptionHandlerpublic Object handler(Exception e){log.info("發生異常 e:{}",e.getMessage());return Result.fail(e.getMessage());}@ExceptionHandlerpublic Object handler(NullPointerException e) {return Result.fail("發?NullPointerException:"+e.getMessage());}@ExceptionHandlerpublic Object handler(ArithmeticException e) {return Result.fail("發?ArithmeticException:"+e.getMessage());}
}
當有多個異常通知時,匹配順序為當前類及其?類向上依次匹配