SpringMVC的組件之HandlerExceptionResolver詳解
前言
在介紹完Handler、HandlerAdapter、HandlerMapping之后,剩下的比較關(guān)鍵的組件就是HandlerExceptionResolver、ViewResolver。
其他的像國(guó)際化、主題、文件上傳、重定向,這些錦上添花的組件都是一個(gè)框架需要關(guān)心的。
但不是我們平常使用的核心功能,所以有興趣的同學(xué)就自己了解吧。
HandlerExceptionResolver
不管是在處理請(qǐng)求映射(HandlerMapping),還是在請(qǐng)求被處理(Handler)時(shí)拋出的異常,DispatcherServlet都會(huì)委托給HandlerExceptionResolver進(jìn)行異常處理。
該接口只有一個(gè)方法。
/** * 嘗試處理handler執(zhí)行過(guò)程中拋出的異常。可能返回一個(gè)代表特定頁(yè)面的ModelAndView * 如果返回的ModelAndView為空:{ModelAndView#isEmpty()},則表示該異常已經(jīng)被成功處理,并且不需要渲染視圖。例如:通過(guò)設(shè)置httpStatus處理異常. * @param request 當(dāng)前HTTP請(qǐng)求 * @param response 當(dāng)前HTTP響應(yīng) * @param handler 被執(zhí)行的handler??赡転閚ull,如果異常發(fā)生在處理器選擇之前(例如:multipart處理失敗) * @param ex 處理過(guò)程中拋出的異常 * @return 對(duì)應(yīng)的ModelAndView。如果無(wú)法處理則返回null,DispatcherServlet將使用默認(rèn)的處理流程。返回new ModelAndView(),則說(shuō)明請(qǐng)求直接被處理完成了,不需要試圖處理。 */ @Nullable ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
SpringMVC提供的異常處理器
HandlerExceptionResolver | Description |
SimpleMappingExceptionResolver | 將異常Class簡(jiǎn)單的映射到錯(cuò)誤視圖的名字。常用于瀏覽器應(yīng)用渲染錯(cuò)誤頁(yè)面 |
DefaultHandlerExceptionResolver | 負(fù)責(zé)處理SpringMVC拋出的異常,并且將這些異常映射到對(duì)應(yīng)的HTTP狀態(tài)碼??梢詫?duì)照他的替代者:ResponseEntiryExceptionResolver、Controller Advice |
ResponseStatusExceptionResolver | 通過(guò)@ResponseStatus注解來(lái)處理異常,并基于注解的值映射到HTTP狀態(tài)碼 |
ExceptionHandlerExceptionResolver | .通過(guò)調(diào)用@Controller或者@ControllerAdvice中的@ExceptionHandler注解方法來(lái)處理異常 |
接下來(lái),我們介紹一下比較常用的ExceptionHandlerExceptionResolver。
ExceptionHandlerExceptionResolver
如果有同學(xué)配置過(guò)全局異常處理的,應(yīng)該會(huì)認(rèn)識(shí)這兩注解:@ControllerAdvice @ExceptionHandler,而他們正是ExceptionHandlerExceptionResolver處理異常的重要抓手。
全局異常配置
在了解ExceptionHandlerExceptionResolver的設(shè)計(jì)之前,我們先來(lái)看看最常用的全局異常配置是怎樣的。只有知道他要達(dá)到什么樣的目標(biāo),才能理解他為什么這么設(shè)計(jì)/實(shí)現(xiàn)!
@ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) @ResponseBody public ResultDTO handleRuntimeException(RuntimeException e, HandlerMethod handlerMethod) { // ... } /** * 這是官方的例子 */ @ExceptionHandler({FileSystemException.class, RemoteException.class}) public ResponseEntity<String> handle(IOException ex) { // ... } }
好,現(xiàn)在來(lái)分析一下需求:
- 識(shí)別帶@ControllerAdvice注解的bean
- 識(shí)別這些bean中的@ExceptionHandler方法
- 根據(jù)@ExceptionHandler的條件,找到匹配的異常處理方法
- 識(shí)別和處理異常處理方法的參數(shù),并準(zhǔn)備參數(shù)列表 PS: SpringMVC提供了豐富的可選參數(shù)
- 識(shí)別和處理方法返回值,并通過(guò)response給客戶端進(jìn)行響應(yīng)。
額,有沒(méi)有覺(jué)得似曾相識(shí)?對(duì)標(biāo)一下@Controller、@RequestMapping?
Handler領(lǐng)域 | Exception領(lǐng)域 | 作用/描述 |
@Controller | @ControllerAdvice | 標(biāo)記目標(biāo)處理對(duì)象類(lèi) |
@RestController | @RestControllerAdvice | 標(biāo)記目標(biāo)處理對(duì)象,并表示返回值即為要響應(yīng)的消息體,通常會(huì)被json序列化 |
@RequestMapping | @ExceptionHandler | 標(biāo)記目標(biāo)處理方法,并且包含方法可以處理的匹配條件 |
方法參數(shù)列表靈活多變,需要進(jìn)行參數(shù)解析 | 相較于HandlerAdapter,支持的參數(shù)要少一些。例如:不支持@RequestBody參數(shù) | 方法參數(shù) |
方法返回值靈活多變, 需要進(jìn)行返回值處理 | 相較于HandlerAdapter,支持的返回值要少一些。例如不支持ModelAndView,但支持ViewName,不支持異步響應(yīng)返回值等等 | 方法返回值 |
是不是高度相似?這也意味著他們?cè)趨?shù)解析和返回值處理上高度相似。
異常處理邏輯
與上面的全局異常處理相比,實(shí)際上Spring在處理異常時(shí),還需要考慮@Controller中的@ExceptionHandler方法。
因此異常的處理分為兩部分
處理方法 | 描述 |
@ControllerAdvice中的@ExceptionHandler | 這里面的方法是全局性的,所有的@RequestMapping方法只要發(fā)生異常且Controller中沒(méi)有聲明異常處理方法,則都會(huì)用這些方法處理 |
@Controller中的@ExceptionHandler | 這些異常處理方法則只對(duì)該Controller有效。即,只能處理該Controller中的@RequestMapping方法異常 |
由此,我們將不得不有個(gè)優(yōu)先級(jí),同樣也是最靠近@RequestMapping優(yōu)先。只有當(dāng)對(duì)應(yīng)的@Controller中沒(méi)有@ExceptionHandler時(shí),才能用全局異常處理方法進(jìn)行處理。 但是仔細(xì)考慮一下,在應(yīng)用運(yùn)行過(guò)程中,每個(gè)類(lèi)都是固定的,方法也是固定的,方法有什么注解也是固定的。如果在調(diào)用時(shí)頻繁去使用反射遍歷所有的方法來(lái)獲取異常處理方法,是不是不太合理?首先,@Controller類(lèi)很多時(shí)候都沒(méi)有異常處理方法,做這個(gè)遍歷操作純粹是無(wú)用功。其次,即使有異常處理方法,每次都遍歷所有方法也不合理,應(yīng)該緩存起來(lái)。因此運(yùn)行時(shí)類(lèi)一般是不會(huì)變的。
Spring的設(shè)計(jì)
因?yàn)椴还苁茾ControllerAdvice還是@Controller,解析@ExceptionHandler的方式都是一樣的,都要遍歷所有方法來(lái)尋找。因此可以統(tǒng)一起來(lái)。于是抽象出來(lái)ExceptionHandlerMethodResolver。先說(shuō)明,別搞混了哈,我們今天說(shuō)的ExceptionHandlerExceptionResolver是負(fù)責(zé)調(diào)用ExceptionHandler方法來(lái)處理異常的ExceptionResolver[異常處理器],而這個(gè)ExceptionHandlerMethodResolver負(fù)責(zé)解析@ExceptionHandler的MethodResolver[方法解析器],
- 他負(fù)責(zé)解析管理類(lèi)中的@ExceptionHandler方法。mappedMethods可以理解為其異常處理方法的注冊(cè)中心。
- 由于異??梢郧短祝瑸榱思铀倨ヅ?,還搞了一個(gè)緩存exceptionLookupCache。該緩存使用的是Spring的ConcurrentReferenceHashMap,整個(gè)Entry都是軟引用,即發(fā)生OOM異常之前,key、value都會(huì)被清理。(key是異常類(lèi)型,value是異常處理方法)
ExceptionHandlerExceptionResolver則需要統(tǒng)籌之前說(shuō)的@ControllerAdvice和@Controller的異常處理。
- exceptionHandlerCache
- key是handlerType(HandlerMethod對(duì)應(yīng)的@Controller對(duì)象類(lèi)型),value是與之對(duì)應(yīng)的ExceptionHandlerMethodResolver
- exceptionHandlerAdviceCache
- 緩存@ControllerAdvice對(duì)象的ExceptionHandlerMethodResolver,
- key是ControllerAdviceBean(他實(shí)際上封裝了@ControllerAdvice的類(lèi)型信息,同時(shí)也可以通過(guò)beanFactory拿到相應(yīng)的bean),value是與該對(duì)象對(duì)應(yīng)的ExceptionHandlerMethodResolver
核心處理邏輯
核心處理邏輯在 ExceptionHandlerExceptionResolver#doResolveHandlerMethodException
/** * 尋找一個(gè)@ExceptionHandler方法并調(diào)用他處理拋出的異常 */ @Override @Nullable protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { // 1. 獲取匹配的異常處理方法,并封裝成ServletInvocableHandlerMethod // 就是這個(gè)方法控制著優(yōu)先使用@Controller中的@ExceptionHandler方法 // 他會(huì)首先檢查exceptionHandlerCache,然后才到exceptionHandlerAdviceCache。他們都是通過(guò)ExceptionHandler**Method**Resolver找到目標(biāo)方法的。 ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception); // 沒(méi)有異常處理方法,則直接退出 if (exceptionHandlerMethod == null) { return null; } // 初始化exceptionHandlerMethod。主要是設(shè)置參數(shù)解析器、返回值處理器 // 省略... // 2. 準(zhǔn)備exceptionHandlerMethod的調(diào)用參數(shù) ServletWebRequest webRequest = new ServletWebRequest(request, response); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); ArrayList<Throwable> exceptions = new ArrayList<>(); try { // 遍歷嵌套異常作為方法參數(shù) Throwable exToExpose = exception; while (exToExpose != null) { exceptions.add(exToExpose); Throwable cause = exToExpose.getCause(); exToExpose = (cause != exToExpose ? cause : null); } Object[] arguments = new Object[exceptions.size() + 1]; exceptions.toArray(arguments); // efficient arraycopy call in ArrayList arguments[arguments.length - 1] = handlerMethod; // 調(diào)用exceptionHandlerMethod處理異常 // 該方法的調(diào)用就跟RequestMappingHandlerAdapter是一樣的了 exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments); } catch (Throwable invocationEx) { // 繼續(xù)默認(rèn)的異常處理 return null; } if (mavContainer.isRequestHandled()) { // 表示異常已被處理完成 return new ModelAndView(); } else { // 從ModelAndViewContainer封裝ModelAndView返回 // 省略... return mav; } }
總結(jié)
- 異常處理會(huì)優(yōu)先使用對(duì)應(yīng)的@Controller中的@ExceptionHandler方法,然后才是@ControllerAdvice中的異常處理方法。
- 從宏觀層面,@ExceptionHandler的緩存分為兩層
層次 | 緩存所在 | 注冊(cè)中心或緩存 | 描述 |
類(lèi)層面 | ExceptionHandlerExceptionResolver | 管理@ControllerAdvice的exceptionHandlerAdviceCache以及管理@Controller的exceptionHandlerCache | 每個(gè)BeanType對(duì)應(yīng)一個(gè)ExceptionHandlerMethodResolver |
方法層 | ExceptionHandlerMethodResolver | 管理@ExceptionHandler方法的mappedMethods。負(fù)責(zé)加速尋找處理方法的exceptionLookupCache | 每個(gè)類(lèi)都可能有多個(gè)@ExceptionHandler |
- 方法調(diào)用與RequestMappingHandlerAdapter一樣,都是通過(guò)ServletInvocableHandlerMethod進(jìn)行處理。
如果理解了RequestMappingHandlerAdapter那么再來(lái)理解這個(gè)ExceptionHandlerExceptionResolver應(yīng)該相對(duì)簡(jiǎn)單些,只需要重點(diǎn)理解兩個(gè)點(diǎn):
- @ExceptionHandler的出現(xiàn)的位置:@ControllerAdvice和@Controller。
- @ExceptionHandler的分層設(shè)計(jì)。
到此這篇關(guān)于SpringMVC的組件之HandlerExceptionResolver詳解的文章就介紹到這了,更多相關(guān)HandlerExceptionResolver詳解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java 數(shù)據(jù)結(jié)構(gòu)之時(shí)間復(fù)雜度與空間復(fù)雜度詳解
算法復(fù)雜度分為時(shí)間復(fù)雜度和空間復(fù)雜度。其作用: 時(shí)間復(fù)雜度是度量算法執(zhí)行的時(shí)間長(zhǎng)短;而空間復(fù)雜度是度量算法所需存儲(chǔ)空間的大小2021-11-11通過(guò)實(shí)例解析synchronized和lock區(qū)別
這篇文章主要介紹了通過(guò)實(shí)例解析synchronized和lock區(qū)別,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-12-12Java實(shí)現(xiàn)簡(jiǎn)單GUI登錄和注冊(cè)界面
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)簡(jiǎn)單GUI登錄和注冊(cè)界面,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04java反射_改變private中的變量及方法的簡(jiǎn)單實(shí)例
下面小編就為大家?guī)?lái)一篇java反射_改變private中的變量及方法的簡(jiǎn)單實(shí)例。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-06-06在CentOS上安裝Java 17并實(shí)現(xiàn)多版本共存的詳細(xì)教程
在現(xiàn)代軟件開(kāi)發(fā)中,Java 作為一種廣泛使用的編程語(yǔ)言,其版本更新頻繁,不同項(xiàng)目可能依賴不同版本的 Java 運(yùn)行環(huán)境,CentOS 作為一款流行的 Linux 發(fā)行版,常被用于服務(wù)器部署和開(kāi)發(fā)環(huán)境,本文將詳細(xì)介紹如何在 CentOS 上安裝 Java 17,并實(shí)現(xiàn)與現(xiàn)有 Java 8 的多版本共存2025-03-03快速掌握SpringBoot應(yīng)用的啟動(dòng)入口
本篇并不是深究?jī)?nèi)置服務(wù)器的啟動(dòng)過(guò)程,而是追溯Springboot啟動(dòng)之前到底做了什么?它是如何與我們經(jīng)常寫(xiě)的@SpringBootApplication注解注釋的main方法類(lèi)綁定起來(lái)的?對(duì)SpringBoot啟動(dòng)入口相關(guān)知識(shí)感興趣的朋友一起看看吧2022-05-05Java啟動(dòng)Tomcat的實(shí)現(xiàn)步驟
本文主要介紹了Java啟動(dòng)Tomcat的實(shí)現(xiàn)步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05使用springcloud+oauth2攜帶token去請(qǐng)求其他服務(wù)
這篇文章主要介紹了使用springcloud+oauth2攜帶token去請(qǐng)求其他服務(wù)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08