一功能處理)
文章目錄一、SpringMVC 攔截器1. 什么是攔截器2. 攔截器三大方法3. 注冊攔截器WebMvc 配置4. Session 登錄校驗攔截器5. 攔截器底層Servlet生命周期DispatcherServlet doDispatch 源碼二、統(tǒng)一數(shù)據(jù)返回格式ResponseBodyAdvice 全局包裝1. 為什么需要統(tǒng)一返回2. 基礎(chǔ)統(tǒng)一返回實體 Result3. 全局響應(yīng)包裝4. String 返回報 ClassCastException三、全局統(tǒng)一異常處理1. 實現(xiàn)方案ControllerAdvice ExceptionHandler2. 異常匹配規(guī)則3. ControllerAdvice 底層原理總結(jié)附閱讀源碼快捷操作后端開發(fā)中我們總會遇到三類重復(fù)通用需求登錄校驗、權(quán)限攔截每個接口重復(fù)寫 Session 判斷代碼冗余難維護接口返回格式五花八門有的返回對象、有的返回布爾、有的返回字符串前端解析成本高代碼空指針、運算異常直接拋出原生報錯頁面展示雜亂無法統(tǒng)一提示。SpringBoot 提供三套方案一次性解決以上問題攔截器統(tǒng)一鑒權(quán)ControllerAdvice ResponseBodyAdvice統(tǒng)一返回格式ControllerAdvice ExceptionHandler全局異常捕獲。一、SpringMVC 攔截器1. 什么是攔截器攔截器是 Spring 原生組件在請求到達 Controller前后執(zhí)行自定義通用邏輯支持放行/阻斷請求適合登錄校驗、日志打印、請求耗時統(tǒng)計等全局操作。簡單類比銀行辦業(yè)務(wù)進門先取號校驗身份證preHandle請求前辦完業(yè)務(wù)評價服務(wù)postHandle請求后全部流程結(jié)束歸檔afterCompletion。2. 攔截器三大方法自定義攔截器需要實現(xiàn)HandlerInterceptor接口重寫三個生命周期方法Slf4jComponentpublicclassLoginInterceptorimplementsHandlerInterceptor{// 1. Controller方法執(zhí)行前核心鑒權(quán)邏輯寫這里OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{log.info(請求前置攔截執(zhí)行鑒權(quán));// true放行繼續(xù)訪問Controllerfalse阻斷請求直接返回響應(yīng)returntrue;}// 2. Controller方法執(zhí)行完成視圖渲染前OverridepublicvoidpostHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,ModelAndViewmodelAndView)throwsException{log.info(Controller執(zhí)行完畢視圖渲染前);}// 3. 整個請求全部結(jié)束視圖渲染/響應(yīng)返回完成資源釋放、日志收尾OverridepublicvoidafterCompletion(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler,Exceptionex)throwsException{log.info(請求完全結(jié)束);}}preHandle返回false時后續(xù)Controller、postHandle、afterCompletion全部不會執(zhí)行直接中斷請求。3. 注冊攔截器WebMvc 配置定義完攔截器后需要實現(xiàn)WebMvcConfigurer完成注冊配置攔截/放行路徑ConfigurationpublicclassWebConfigimplementsWebMvcConfigurer{AutowiredprivateLoginInterceptorloginInterceptor;OverridepublicvoidaddInterceptors(InterceptorRegistryregistry){registry.addInterceptor(loginInterceptor).addPathPatterns(/**)// 攔截所有接口// 放行登錄接口、靜態(tài)資源.excludePathPatterns(/user/login,/**/*.html,/**/*.js,/**/*.css,/**/*.png);}}路徑匹配規(guī)則匹配規(guī)則含義示例/*一級路徑匹配/login不匹配/user/login/**任意多級路徑匹配/user、user/list、user/info/1/book/*book下一級接口匹配/book/add不匹配/book/info/1/book/**book下所有子接口匹配所有 book 前綴接口4. Session 登錄校驗攔截器業(yè)務(wù)需求未登錄用戶訪問除登錄外所有接口返回 401 未認(rèn)證狀態(tài)碼。Slf4jComponentpublicclassLoginInterceptorimplementsHandlerInterceptor{OverridepublicbooleanpreHandle(HttpServletRequestrequest,HttpServletResponseresponse,Objecthandler)throwsException{log.info(preHandle 目標(biāo)方法執(zhí)行前...);//true 表示繼續(xù)執(zhí)行false 表示中斷執(zhí)行HttpSessionsessionrequest.getSession(false);if(!checkUser(session)){//字符串設(shè)置//設(shè)置響應(yīng)內(nèi)容類型為UTF-8編碼的HTML防止返回中文提示時瀏覽器亂碼//如果是json數(shù)據(jù)則設(shè)置為application/jsonresponse.setContentType(text/html;charsetutf-8);//400-參數(shù)錯誤 401-未登錄 403-沒有權(quán)限r(nóng)esponse.setStatus(401);Stringmsg用戶未登錄;response.getOutputStream().write(msg.getBytes(StandardCharsets.UTF_8));returnfalse;}returntrue;}publicbooleancheckUser(HttpSessionsession){if(sessionnull||session.getAttribute(Constants.USER_SESSION_KEY)null){log.warn(用戶未登錄);returnfalse;}UserInfouserInfo(UserInfo)session.getAttribute(Constants.USER_SESSION_KEY);if(userInfo.getId()0){log.warn(用戶ID無效);returnfalse;}returntrue;}測試未登錄訪問/book/getList接口返回 401先調(diào)用登錄接口存入 Session再次訪問圖書接口正常返回數(shù)據(jù)。5. 攔截器底層Servlet生命周期publicinterfaceServlet{//啟動服務(wù)時初始化voidinit(ServletConfigconfig)throwsServletException;ServletConfiggetServletConfig();//調(diào)用接口時會調(diào)用service方法可能會調(diào)用多次voidservice(ServletRequestreq,ServletResponseres)throwsServletException,IOException;StringgetServletInfo();//關(guān)閉服務(wù)時銷毀voiddestroy();}DispatcherServlet doDispatch 源碼Tomcat啟動后DispatcherServlet來控制執(zhí)行程序的順序。所有請求最終都會進入 SpringMVC 核心調(diào)度類DispatcherServlet的doDispatch方法執(zhí)行順序根據(jù) URL 匹配處理器執(zhí)行鏈HandlerExecutionChain包含攔截器列表循環(huán)執(zhí)行所有攔截器preHandle()任意攔截器返回false直接終止流程preHandle全部放行后執(zhí)行 Controller 業(yè)務(wù)方法Controller 執(zhí)行完畢倒序執(zhí)行postHandle()最后執(zhí)行afterCompletion()統(tǒng)一處理異常與資源清理。關(guān)鍵源碼片段// 執(zhí)行所有攔截器前置方法if(!mappedHandler.applyPreHandle(processedRequest,response)){return;// 任意攔截器阻斷直接返回}// 執(zhí)行Controllermvha.handle(processedRequest,response,mappedHandler.getHandler());// 執(zhí)行后置攔截mappedHandler.applyPostHandle(processedRequest,response,mv);// 收尾執(zhí)行afterCompletionthis.processDispatchResult(processedRequest,response,mappedHandler,mv,dispatchException);二、統(tǒng)一數(shù)據(jù)返回格式ResponseBodyAdvice 全局包裝1. 為什么需要統(tǒng)一返回項目中接口返回類型混亂實體類、Boolean、String、數(shù)字前端需要寫多套解析邏輯維護成本高。統(tǒng)一格式后所有響應(yīng)結(jié)構(gòu)標(biāo)準(zhǔn)化{status:SUCCESS,errorMessage:,data:{...}}2. 基礎(chǔ)統(tǒng)一返回實體 Resultimportlombok.Data;DatapublicclassResultT{privateStringstatus;privateStringerrorMessage;privateTdata;// 成功靜態(tài)方法publicstaticTResultTsuccess(Tdata){ResultTresultnewResult();result.setStatus(SUCCESS);result.setErrorMessage();result.setData(data);returnresult;}// 失敗靜態(tài)方法publicstaticTResultTfail(Stringmsg){ResultTresultnewResult();result.setStatus(FAIL);result.setErrorMessage(msg);result.setData(null);returnresult;}}3. 全局響應(yīng)包裝使用ControllerAdvice掃描所有控制器實現(xiàn)ResponseBodyAdvice對返回體統(tǒng)一封裝Slf4jControllerAdvicepublicclassResponseAdviceimplementsResponseBodyAdviceObject{privatestaticfinalObjectMappermappernewObjectMapper();// 是否開啟統(tǒng)一包裝返回true才執(zhí)行beforeBodyWriteOverridepublicbooleansupports(MethodParameterreturnType,ClassconverterType){returntrue;}SneakyThrowsOverridepublicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,ClassselectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){// 1. 如果已經(jīng)是Result對象不再二次包裝避免嵌套if(bodyinstanceofResult){returnbody;}// 2. 特殊處理String類型解決類型轉(zhuǎn)換異常if(bodyinstanceofString){returnmapper.writeValueAsString(Result.success(body));}// 3. 普通對象統(tǒng)一包裝returnResult.success(body);}}4. String 返回報 ClassCastException當(dāng)方法返回String類型時就會報錯Spring 內(nèi)置HttpMessageConverter執(zhí)行有優(yōu)先級StringHttpMessageConverter優(yōu)先級高于 Jackson 序列化器。當(dāng) Controller 返回字符串包裝后返回Result對象StringHttpMessageConverter強制把返回值轉(zhuǎn)為字符串類型不匹配拋出異常。解決方案手動使用ObjectMapper將 Result 序列化為 JSON 字符串返回讓字符串轉(zhuǎn)換器正常識別。SneakyThrowsOverridepublicObjectbeforeBodyWrite(Objectbody,MethodParameterreturnType,MediaTypeselectedContentType,ClassselectedConverterType,ServerHttpRequestrequest,ServerHttpResponseresponse){// 1. 如果已經(jīng)是Result對象不再二次包裝避免嵌套if(bodyinstanceofResult){returnbody;}// 2. 特殊處理String類型解決類型轉(zhuǎn)換異常if(bodyinstanceofString){returnmapper.writeValueAsString(Result.success(body));}// 3. 普通對象統(tǒng)一包裝returnResult.success(body);}三、全局統(tǒng)一異常處理1. 實現(xiàn)方案ControllerAdvice ExceptionHandler攔截項目所有未捕獲異常統(tǒng)一封裝為Result格式返回?zé)o需在每個接口寫 try-catch。ResponseBodyControllerAdvicepublicclassGlobalExceptionAdvice{// 捕獲空指針異常ExceptionHandler(NullPointerException.class)publicResult?handleNullPoint(NullPointerExceptione){log.error(空指針異常,e);returnResult.fail(系統(tǒng)空指針e.getMessage());}// 捕獲除零運算異常ExceptionHandler(ArithmeticException.class)publicResult?handleMath(ArithmeticExceptione){log.error(運算異常,e);returnResult.fail(計算錯誤e.getMessage());}// 兜底捕獲所有Exception優(yōu)先級最低ExceptionHandler(Exception.class)publicResult?handleAll(Exceptione){log.error(系統(tǒng)未知異常,e);returnResult.fail(服務(wù)器異常請稍后重試);}}RestControllerAdvice內(nèi)包含ResponseBody和ControllerAdvice因此這兩個注解也可以用RestControllerAdvice代替可以在注解上指定異常類型也可以在方法參數(shù)中指定異常類型2. 異常匹配規(guī)則拋出異常時優(yōu)先匹配最精準(zhǔn)的子類異常方法沒有對應(yīng)方法才會走頂層Exception兜底拋出NullPointerException→ 執(zhí)行handleNullPoint拋出自定義業(yè)務(wù)異常 → 匹配自定義異常處理器拋出 IO 異常、數(shù)組越界 → 走Exception通用兜底。3. ControllerAdvice 底層原理ControllerAdvice本質(zhì)攜帶Component啟動時會被 Spring 掃描為 BeanRequestMappingHandlerAdapter啟動時掃描所有ControllerAdvice緩存ResponseBodyAdviceExceptionHandlerExceptionResolver緩存所有ExceptionHandler異常處理方法Controller 返回/拋出異常時自動調(diào)用對應(yīng) Advice 邏輯完成包裝??偨Y(jié)攔截器兩步實現(xiàn)自定義HandlerInterceptorWebMvcConfigurer注冊preHandle負責(zé)鑒權(quán)阻斷false直接終止請求通過excludePathPatterns放行登錄、靜態(tài)資源。統(tǒng)一返回格式ControllerAdvice ResponseBodyAdvice全局包裝必處理 String 類型解決類型轉(zhuǎn)換異常判斷返回值為 Result 時跳過包裝避免嵌套 JSON。全局異常分層捕獲異常精準(zhǔn)子類優(yōu)先Exception 兜底所有異常統(tǒng)一返回 Result前端無需處理多種報錯格式底層核心全部依托DispatcherServlet九大組件ControllerAdvice作為控制器增強統(tǒng)一處理全局通用邏輯。DispatcherServlet九大組件protectedvoidinitStrategies(ApplicationContextcontext){initMultipartResolver(context);initLocaleResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);}附閱讀源碼快捷操作快捷鍵功能使用場景CtrlF當(dāng)前文件內(nèi)查找已經(jīng)打開源碼類內(nèi)部搜關(guān)鍵詞CtrlShiftF全局文本搜索只記得字符串、日志、異常信息CtrlN查找類記得類名直接打開源碼類雙擊 ShiftSearch Everywhere 萬能搜索模糊記憶啥都搜CtrlShiftAltN搜索方法 / 變量符號只記得方法片段CtrlF12當(dāng)前類結(jié)構(gòu)視圖瀏覽大源碼類的全部方法AltF7查找用法看方法 / 變量在哪被調(diào)用CtrlAltH調(diào)用層次樹追蹤完整調(diào)用鏈CtrlAltB跳轉(zhuǎn)到接口實現(xiàn)類Spring 接口找實現(xiàn)Alt← / Alt→跳轉(zhuǎn)歷史前進后退源碼來回跳轉(zhuǎn)防止迷路