摘要
介紹spring mvc控制器中統(tǒng)一處理異常的兩種方式:HandlerExceptionResolver
以及@ExceptionHandler
;以及使用@ControllerAdvice
將@ExceptionHandler
方法的影響擴(kuò)大。
一收壕、問(wèn)題的提出
Spring MVC 項(xiàng)目的開(kāi)發(fā)中汇歹,不管是底層的數(shù)據(jù)庫(kù)操作過(guò)程屯阀,業(yè)務(wù)層的業(yè)務(wù)邏輯的處理救欧,還是控制層的處理過(guò)程值戳,都不可避免會(huì)遇到各種可預(yù)知的西篓、不可預(yù)知的異常愈腾。
這些異常可以在每個(gè)單獨(dú)的環(huán)節(jié)捕獲岂津,處理虱黄;但是大多數(shù)情況下,異常情況都會(huì)反饋到控制器(無(wú)論是通過(guò)拋出異常的方式吮成,還是自定義特殊返回值橱乱,如null等的方式),然后由控制器結(jié)合具體異常情況粱甫,返回特定信息(通常是不同的返回碼泳叠,錯(cuò)誤信息)給http請(qǐng)求的調(diào)用方。
然而茶宵,每個(gè)環(huán)節(jié)都單獨(dú)捕獲處理異常析二,業(yè)務(wù)代碼可讀性不強(qiáng),工作量大且不好統(tǒng)一,維護(hù)的工作量也很大叶摄。那么属韧,能不能將所有類型的異常處理從各處理過(guò)程解耦出來(lái),這樣既保證了相關(guān)處理過(guò)程的功能較單一蛤吓,也實(shí)現(xiàn)了異常信息的統(tǒng)一處理和維護(hù)宵喂?答案是肯定的。
二会傲、統(tǒng)一異常處理
對(duì)于spring mvc來(lái)說(shuō)锅棕,一次http請(qǐng)求在服務(wù)端處理涉及到的環(huán)節(jié)一般如下:
每個(gè)環(huán)節(jié)都有可能發(fā)生異常;問(wèn)題的解決思路淌山,恰恰是對(duì)于異常處理的自然過(guò)程: 能夠處理異常就捕獲處理裸燎,不能處理異常就將異常拋出(或者轉(zhuǎn)換拋出)。
一般來(lái)說(shuō)泼疑,服務(wù)層和持久層發(fā)生的異常德绿,這兩層都無(wú)能為力,因?yàn)檫@些異常情況會(huì)轉(zhuǎn)換為相關(guān)的信息返回到http調(diào)用方退渗。既然不能處理移稳,何不直接拋出(轉(zhuǎn)換拋出)到控制層?然后由http請(qǐng)求的入口處——控制層統(tǒng)一處理会油。
那么个粱,可能的處理方法是這樣的:
controller:
@RequestMapping(...)
public Object doController(){
try {
invokeService();
} catch(CustomizedEx1 e) {
// 返回碼1
} catch(CustomizedEx2 e) {
// 返回碼2
} ...
catch(Exception e) {
// 系統(tǒng)異常 ?
}
}
這樣可以做到在一次請(qǐng)求中,統(tǒng)一在入口控制器方法處處理異常翻翩。但是這樣的話都许,對(duì)于每個(gè)請(qǐng)求,在控制器中處理將請(qǐng)求處理委托給服務(wù)層的代碼外嫂冻,不得不書寫捕獲各種異常的catch塊胶征,對(duì)于懶惰的程序員來(lái)說(shuō),無(wú)疑是災(zāi)難性的操作絮吵。
封裝
考慮一下異常的種類弧烤,事實(shí)上業(yè)務(wù)異常的種類是有限的忱屑,不同的請(qǐng)求出現(xiàn)的異常情況無(wú)非就那么幾種蹬敲。這時(shí)可將catch處理封裝起來(lái),作為一個(gè)統(tǒng)一的方法莺戒,共各個(gè)controller方法調(diào)用伴嗡。
superController:
class SuperController {
public Object uniformExHandle(Exception e) {
if (e instanceof CustomizedEx1) {
// 返回碼1
} else if (e instanceof CustomizedEx2) {
// 返回碼2
}...
else {
// 系統(tǒng)異常 ?
}
}
}
specificContoller:
@Controller
class HelloController extends SuperController {
@RequestMapping(...)
public Object doController(){
try {
invokeService();
}
catch(Exception e) {
uniformExHandle(e);
}
}
}
封裝異常處理,為了各個(gè)控制器能夠方便調(diào)用从铲,抽象一個(gè)控制器的父類瘪校,供各個(gè)具體控制器繼承。
松耦合
上面的封裝+控制器統(tǒng)一異常處理,似乎解決了開(kāi)始提出的問(wèn)題阱扬,事實(shí)上也解決了問(wèn)題泣懊。但是也引入了新的問(wèn)題:所有控制器不得不繼承 SuperController 以獲得統(tǒng)一處理異常的能力。
這是一種緊耦合的體現(xiàn)麻惶,彷佛回到了EJB時(shí)代馍刮,為了獲取框架的功能,一個(gè)類必須實(shí)現(xiàn)一堆類窃蹋,繼承一堆接口卡啰。這也是良好的設(shè)計(jì)提倡 少用繼承,多用組合
的原因警没。
誠(chéng)然匈辱,使用組合的方式,將異常統(tǒng)一處理暴露出去供控制器方法調(diào)用杀迹,是一種松耦合的方法亡脸。但是既然在spring mvc的生態(tài)中,spring mvc也考慮到了這個(gè)問(wèn)題佛南,提供了兩種方式實(shí)現(xiàn)控制器的異常處理:
- 使用實(shí)現(xiàn)
HandlerExceptionResolver
接口的類處理異常 - 使用
@Exception
注解的方法處理異常
這兩種方法原理一樣梗掰,區(qū)別只在使用方式而已。
三嗅回、HandlerExceptionResolver
參考HandlerExceptionResolver
的jdk文檔及穗,就能輕松了解如何使用。
public interface HandlerExceptionResolver {
@Nullable
ModelAndView resolveException(
HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}
實(shí)現(xiàn)HandlerExceptionResolver接口的類能夠解決在處理器映射或處理器方法執(zhí)行過(guò)程中產(chǎn)生的異常绵载,通常導(dǎo)向錯(cuò)誤的view埂陆,實(shí)現(xiàn)類通常需要注冊(cè)到應(yīng)用spring上下文中才能生效;其resolveException方法試圖解決在處理器執(zhí)行期間拋出的異常娃豹,并在適當(dāng)?shù)那闆r下的返回代表特定錯(cuò)誤頁(yè)面的ModelAndView焚虱。返回的ModelAndView為空時(shí)標(biāo)明異常已經(jīng)被成功地解決,但是沒(méi)有錯(cuò)誤頁(yè)面返回懂版,例如鹃栽,設(shè)置了錯(cuò)誤碼。
簡(jiǎn)單的使用方式:
@Component // 必須注冊(cè)到spring容器中才有效
public class GlobalExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response, Object handler, Exception ex) {
String exMsg = "";
if(null != ex) {
exMsg = ex.getMessage();
}
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("exception");
Map<String, String> map = new HashMap<String, String>();
map.put( "key", "exception occured: " + exMsg);
modelAndView.addAllObjects(map);
return modelAndView;
}
}
原理
DispatcherServlet是SpringMVC的核心躯畴,當(dāng)然他也負(fù)責(zé)了這個(gè)“全局異常的處理”民鼓。
1)分發(fā)請(qǐng)求中捕獲異常:
doDispatch()是DispatcherServlet分發(fā)請(qǐng)求的入口,方法中捕獲請(qǐng)求執(zhí)行可能的異常蓬抄,并交給processDispatchResult()處理
DispatcherServlet#doDispatch():
try {
...
// Actually invoke the handler.
v = ha.handle(processedRequest, response, mappedHandler.getHandler());
...
} catch (Exception ex) {
dispatchException = ex;
}
// 處理結(jié)果以及異常
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
2)processDispatchResult核心
DispatcherServlet#processDispatchResult():
if (exception != null) { // 處理結(jié)果丰嘉,存在異常
if (exception instanceof ModelAndViewDefiningException) {
...
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// 調(diào)用異常處理獲得 ModelAndView
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
DispatcherServlet#processHandlerException():
if (this.handlerExceptionResolvers != null) {
for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {
exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
可見(jiàn)最終遍歷了DispatcherServlet的handlerExceptionResolvers
,依次調(diào)用配置的exception resolver來(lái)處理異常嚷缭,直到異常處理器返回的ModelAndView不為空饮亏。
3)handlerExceptionResolvers初始化
在初始化階段耍贾,會(huì)初始化異常處理器,將spring容器中注冊(cè)的HandlerExceptionResolver加入到DispatcherServlet的handlerExceptionResolvers列表中:
@Override
protected void onRefresh(ApplicationContext context) {
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
...
initHandlerExceptionResolvers(context);
...
}
private void initHandlerExceptionResolvers(ApplicationContext context) {
if (this.detectAllHandlerExceptionResolvers) {
// Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
.beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
// We keep HandlerExceptionResolvers in sorted order.
AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
}
}
}
四、ExceptionHandler
上面的HandlerExceptionResolver方式也需要實(shí)現(xiàn)這個(gè)接口;另一種注解方式是使用@ExceptionHandler
梗逮,只需在指定的控制器中簡(jiǎn)單使用即可:
@Controller
class ExampleController {
@ExceptionHandler(Exception.class)
@ReponseBody
public Object exceptionHandler(Exception e) {
...
return new Object();
}
@RequestMapping(...)
public Object doController() {
}
4锊肌!!需要注意的是,注解@ExceptionHandler修飾的方法,只能處理所在控制器的@RequestMapping方法的未捕獲異常杂伟,超出該控制器,或者沒(méi)有使用@RequestMapping修飾的方法調(diào)用仍翰,發(fā)生的未捕獲異常都不會(huì)被處理赫粥。
@ExceptionHandler
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
/**
* Exceptions handled by the annotated method. If empty, will default to any
* exceptions listed in the method argument list.
*/
Class<? extends Throwable>[] value() default {};
}
簡(jiǎn)要說(shuō)來(lái),使用這個(gè)注解標(biāo)注的方法能處理方法所在Controller的處理器中未捕獲的異常予借,處理異常的方法可以有多種方式的簽名越平,參數(shù)可以有
- 異常類型的參數(shù);Exception或者特定類型的異常類灵迫,要與value中指定的異常匹配
- request和response對(duì)象秦叛;javax.servlet.ServletRequest/javax.servlet.ServletResponse,javax.servlet.http.HttpServletRequest/javax.servlet.http.HttpServletRequest
- Session對(duì)象
- 等等
返回值可以是:
- ModelAndView, model object, Map, View
- 表示視圖名的String
- @Response修飾瀑粥,設(shè)置響應(yīng)內(nèi)容挣跋;使用配置的message converts將返回值轉(zhuǎn)換為響應(yīng)流
- HttpEntity / ResponseEntity,同樣使用message converts轉(zhuǎn)換
- void狞换,如果方法自己處理http response輸出
可以看出@ExceptionHandler方式靈活得多避咆,而且其原理與HandlerExceptionResolver是一樣的。
全局配置
由于@ExceptionHandler方法只能處理同一個(gè)控制器內(nèi)的方法修噪,這樣每一個(gè)控制器都要聲明@ExceptionHandler方法查库?
很自然的可以想到在所有控制器的一個(gè)父類中聲明一個(gè)@ExceptionHandler方法,即可全局處理黄琼。更優(yōu)雅的方式是使用@ControllerAdvice
樊销;
正如其名字一樣,注解修飾的類是“協(xié)助”其他控制器脏款,是@Component
的具化注解围苫,通過(guò)類路徑掃描(component scan)修飾的類可以被自動(dòng)檢測(cè)(注冊(cè)到spring容器)。
典型的用法是用來(lái)定義 @ExceptionHandler, @InitBinder, 和 @ModelAttribute方法弛矛,這些方法可運(yùn)用于所有的@RequestMapping方法够吩。默認(rèn)情況下@ControllerAdvice的修飾類會(huì)“協(xié)助”所有已知的控制器比然。
以下為使用demo:
@ControllerAdvice
public class UniformControllerExHandler {
@ExceptionHandler(Throwable.class)
@ResponseBody
public Object exHandler(Throwable e) {
AgentBaseResponse resp = new AgentBaseResponse();
resp.setRetMsg(e.getMessage());
log.error("控制器異常(Throwable), 返回: " + JSON.toJSONString(resp), e);
return resp;
}
}
五丈氓、總結(jié)
spring mvc中業(yè)務(wù)方法的異常,可以在控制層統(tǒng)一處理。
通過(guò)實(shí)現(xiàn)spring提供的HandlerExceptionResolver
接口万俗,并把實(shí)現(xiàn)類注入到spring容器湾笛,可統(tǒng)一處理控制器方法未捕獲的異常。
另一種方法是使用@ExceptionHandler
闰歪,借助@ControllerAdvice
可將影響擴(kuò)大到每一個(gè)控制器嚎研。