使用Spring MVC搭建一個(gè)web應(yīng)用時(shí),我們有很多種辦法處理異常并返回異常視圖給browser愧口,下面我們分別介紹幾種異常的處理方式雨让。
通過(guò)HandlerExceptionResolver處理異常
該接口是DispatcherServlet提供的唯一的異常處理機(jī)制,在Spring MVC內(nèi)部所有的異常處理方式都是基于該機(jī)制實(shí)現(xiàn)的丽涩,包括@ExceptionHandler注解桐腌。
當(dāng)一個(gè)未捕獲的Exception在DispatcherServlet處理請(qǐng)求的過(guò)程中發(fā)生時(shí)拄显,Spring會(huì)使用該接口的實(shí)現(xiàn)來(lái)處理Exception。該接口唯一的方法resolveException抽象了Exception轉(zhuǎn)換為ModelAndView的過(guò)程案站,方法簽名是這樣的:
ModelAndViewresolveException(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex)
Spring允許多個(gè)該接口的實(shí)現(xiàn)同時(shí)工作躬审,Spring會(huì)將已注冊(cè)的實(shí)現(xiàn)根據(jù)order排序后順序調(diào)用,直到某一個(gè)實(shí)現(xiàn)返回了非空結(jié)果蟆盐,這時(shí)Spring會(huì)終止調(diào)用鏈并返回ModelAndView承边。
缺省情況下,ExceptionHandlerExceptionResolver石挂、ResponseStatusExceptionResolver博助、DefaultHandlerExceptionResolver(排名根據(jù)優(yōu)先級(jí)從高到低)這三個(gè)實(shí)現(xiàn)會(huì)被注冊(cè)到Spring。
Spring內(nèi)置的HandlerExceptionResolver實(shí)現(xiàn)
Spring一共有以下4個(gè)典型的HandlerExceptionResolver實(shí)現(xiàn):
SimpleMappingExceptionResolver
該實(shí)現(xiàn)需要你配置一個(gè)Exception類名到視圖的映射清單痹愚,他會(huì)基于你的配置將Exception映射為視圖并返回browser富岳。
你的配置看起是這樣的,
java.sql.SqlException=sql_error_view
BizException=biz_error_view
除此之外拯腮,該實(shí)現(xiàn)還允許你定義視圖和response status的映射窖式、要排除的Exception、缺省異常視圖动壤、缺省response status等萝喘。(注意,這里的response status不用于HttpServletResponse.sendError琼懊,只用來(lái)HttpServletResponse.setStatus)
缺省情況下該實(shí)現(xiàn)并沒有注冊(cè)到Spring阁簸,你需要手動(dòng)將他注冊(cè)到Spring并進(jìn)行必要的配置才可使用。
ResponseStatusExceptionResolver
該實(shí)現(xiàn)并沒有明確指定返回什么視圖給browser哼丈,只是根據(jù)拋出的Exception類的@ResponseStatus注解启妹,調(diào)用HttpServletResponse.sendError方法通知servlet容器處理該response status。
你可以這樣聲明一個(gè)自定義Exception削祈,并在用戶無(wú)權(quán)限時(shí)在Controller中拋出:
@ResponseStatus(value=HttpStatus.FORBIDDEN)publicclassAuthzExceptionextendsRuntimeException{//...}
需要注意的是翅溺,這時(shí)如果你沒有配置servlet容器的error-page,servlet容器會(huì)返回缺省的異常頁(yè)面給browser髓抑。
這往往不是我們希望看到的咙崎,所以在使用@ResponseStatus注解時(shí),我們一般要配合error-page或反向代理使用吨拍,在下面會(huì)有相關(guān)的介紹褪猛。
DefaultHandlerExceptionResolver
這個(gè)類實(shí)現(xiàn)了Spring內(nèi)部的Exception如何映射到response status,并調(diào)用HttpServletResponse.sendError方法通知servlet容器處理羹饰。
如Spring會(huì)在請(qǐng)求的http method和@RequestMapping聲明的都不匹配時(shí)拋出org.springframework.web.HttpRequestMethodNotSupportedException伊滋,該實(shí)現(xiàn)收到后會(huì)將異常轉(zhuǎn)換為response status 405并調(diào)用HttpServletResponse.sendError。
有的同學(xué)就會(huì)有疑問(wèn)了队秩,這里Spring自己為什么不用@ResponseStatus注解笑旺?觀察代碼就會(huì)發(fā)現(xiàn),這里不僅僅是將Exception簡(jiǎn)單的映射到response status馍资,還會(huì)針對(duì)不同的Exception有不同的處理(response.setHeader)和選擇性的記錄日志筒主,這些是@ResponseStatus注解不能滿足的。
因?yàn)樵搶?shí)現(xiàn)和上面ResponseStatusExceptionResolver一樣只是調(diào)用HttpServletResponse.sendError方法通知servlet容器處理鸟蟹,所以你同樣需要考慮配合error-page或反向代理返回自定義異常視圖給browser乌妙。
亦或者你想改變sendError這一處理方式,比如直接返回自定義視圖給browser(其實(shí)完全可以在error-page中再統(tǒng)一處理建钥,除非你很在意這點(diǎn)性能的話)藤韵。這時(shí)你可以通過(guò)@ExceptionHandler注解(因?yàn)閮?yōu)先級(jí)的原因,@ExceptionHandler用于處理Spring內(nèi)部異常時(shí)優(yōu)先級(jí)高于該實(shí)現(xiàn))或繼承ResponseEntityExceptionHandler(也是基于@ExceptionHandler實(shí)現(xiàn))自己實(shí)現(xiàn)Spring內(nèi)部Exception的處理熊经。
ExceptionHandlerExceptionResolver
這個(gè)就是@ExceptionHandler注解的處理實(shí)現(xiàn)類泽艘,它是一個(gè)high-level的實(shí)現(xiàn),下面會(huì)專門說(shuō)奈搜。
自己實(shí)現(xiàn)HandlerExceptionResolver
當(dāng)然悉盆,如果上面的實(shí)現(xiàn)都滿足不了需求,你也可以自己實(shí)現(xiàn)HandlerExceptionResolver馋吗,并使用order控制他與其它實(shí)現(xiàn)的執(zhí)行優(yōu)先順序焕盟。
通過(guò)@ExceptionHandler注解處理異常
相對(duì)于HandlerExceptionResolver來(lái)言,這是一個(gè)high-level的處理方式宏粤。因?yàn)槟慊静辉傩枰虷ttpServletRequest脚翘、HttpServletResponse這種底層API打交道,而是像編寫Controller方法一樣使用Spring Controller的幾乎所有注解來(lái)處理并返回異常(比如@ResponseBody)绍哎。這就意味著来农,不管是根據(jù)http請(qǐng)求頭的accept返回不同的content type,還是讀寫request崇堰、session都將變的非常簡(jiǎn)單沃于。
需要注意的是涩咖,@ExceptionHandler方法的位置決定了他的作用范圍,如果寫在Controller中那么他的作用域就是當(dāng)前Controller繁莹,如果寫在ControllerAdvice中那么他的作用域就是ControllerAdvice的作用域(未特殊指定的ControllerAdvice就表示作用于全部Controller)檩互。
/**
* 處理RestController產(chǎn)生的異常,返回json咨演。
* @see ErrorController 處理非RestController產(chǎn)生的異常闸昨,返回html視圖。
*
* @author zaoheng.lb
*/@ControllerAdvice(annotations=RestController.class)publicclassRestErrorController{/**
? ? * 根據(jù)異常類型匹配處理spring mvc拋出的指定異常薄风。
? ? *
? ? * 處理下述情況:
? ? *? 1饵较、spring mvc內(nèi)部異常(如conversion-service、jsr-303 validator)
? ? *? 2遭赂、Controller中業(yè)務(wù)代碼的BusinessException異常循诉。
? ? *
? ? * @param ex
? ? * @return
? ? */@ExceptionHandler({TypeMismatchException.class,BindException.class,BusinessException.class})@ResponseBodypublicResponsehandleException(Exception ex){Response response=createResponse(ex);returnresponse;}}
上面說(shuō)的都是在Spring MVC之內(nèi)的異常處理,但是在DispatcherServlet之外也需要處理異常撇他,比如filter Exception和HttpServletResponse.sendError產(chǎn)生的異常response status打洼,這些如何處理呢?
servlet規(guī)范中的error-page就是設(shè)計(jì)用來(lái)處理拋出到容器級(jí)別的Exception和異常response status的逆粹。他支持異常類型和異常response status到異常處理url的配置募疮,也支持缺省的異常處理url配置(用來(lái)兜底處理未配置的異常類型和異常response status)。
這是一個(gè)用web.xml來(lái)配置error-page的示例:
404/404java.sql.SqlException/sqlError/error
你可以編寫一個(gè)Controller響應(yīng)“/error”這個(gè)url來(lái)統(tǒng)一的處理Exception和異常response status僻弹,Exception對(duì)象等信息可以通過(guò)request attribute拿到(如有)阿浓。
Spring boot應(yīng)用
如果你的應(yīng)用是Spring boot應(yīng)用,那么恭喜你蹋绽,你不再需要自己配置error-page和實(shí)現(xiàn)異常處理芭毙,因?yàn)檫@些Spring都幫你實(shí)現(xiàn)好了(包括根據(jù)accept返回html或json)。你需要做的僅僅是在視圖文件夾(velocity的話就是spring.velocity.resource-loader-path這個(gè)配置)下新建一個(gè)error文件夾卸耘,再將編寫好的異常頁(yè)面根據(jù)response status命名后放到這里即可退敦。
例如你的視圖文件夾是templates的話,你的異常視圖文件結(jié)構(gòu)應(yīng)該是這樣的:
src/
+- main/
? ? +- java/
? ? |? +
? ? +- resources/
? ? ? ? +- templates/
? ? ? ? ? ? +- error/
? ? ? ? ? ? |? +- 404.vm
? ? ? ? ? ? |? +- 5xx.vm
? ? ? ? ? ? +-
當(dāng)然如果Spring boot的默認(rèn)實(shí)現(xiàn)不滿足你的需求(比如json屬性名稱不滿足)蚣抗,你可以繼承并修改他的行為侈百。詳見org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration。
nginx等反向代理可以在頁(yè)面返回browser之前對(duì)頁(yè)面進(jìn)行修改翰铡,所以在nginx中配置error_page也可以達(dá)到將異常response status轉(zhuǎn)換為異常頁(yè)面返回browser的目的钝域。但在nginx中,你無(wú)法方便的獲取到Java Exception對(duì)象等信息锭魔。
error_page 404? ? ? ? /404.html;
error_page 502 503? ? /5xx.html;
個(gè)人認(rèn)為例证,最佳實(shí)踐是多種方式配合使用,達(dá)到完善的異常處理效果迷捧。
方式處理Exception處理異常response status
HandlerExceptionResolver(包括@ExceptionHandler)支持不支持
servlet error-page支持支持
反向代理不支持支持
使用@ExceptionHandler注解處理Controller的Exception:在ExceptionHandler里我們一定可以拿到Exception對(duì)象织咧,所以你可以根據(jù)Exception對(duì)象返回異常視圖給browser胀葱。
使用servlet error-page兜底處理非Controller Exception和sendError產(chǎn)生的異常response status:此時(shí)不一定有Exception對(duì)象(如404),所以你可以根據(jù)response status返回異常視圖給browser笙蒙。
使用nginx配置一些特殊的異常response status:如502的異常頁(yè)面巡社,配置后可以防止servlet容器在重啟時(shí)用戶看到nginx的缺省異常頁(yè)面。
以上手趣,歡迎討論和指正。(* ̄︶ ̄)