SpringMVC控制器統(tǒng)一異常處理

摘要

介紹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é)一般如下:

http處理環(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)控制器的異常處理:

  1. 使用實(shí)現(xiàn)HandlerExceptionResolver接口的類處理異常
  2. 使用@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ù)可以有

  1. 異常類型的參數(shù);Exception或者特定類型的異常類灵迫,要與value中指定的異常匹配
  2. request和response對(duì)象秦叛;javax.servlet.ServletRequest/javax.servlet.ServletResponse,javax.servlet.http.HttpServletRequest/javax.servlet.http.HttpServletRequest
  3. Session對(duì)象
  4. 等等

返回值可以是:

  1. ModelAndView, model object, Map, View
  2. 表示視圖名的String
  3. @Response修飾瀑粥,設(shè)置響應(yīng)內(nèi)容挣跋;使用配置的message converts將返回值轉(zhuǎn)換為響應(yīng)流
  4. HttpEntity / ResponseEntity,同樣使用message converts轉(zhuǎn)換
  5. 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è)控制器嚎研。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市库倘,隨后出現(xiàn)的幾起案子临扮,更是在濱河造成了極大的恐慌,老刑警劉巖教翩,帶你破解...
    沈念sama閱讀 218,122評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杆勇,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡饱亿,警方通過(guò)查閱死者的電腦和手機(jī)蚜退,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)彪笼,“玉大人钻注,你說(shuō)我怎么就攤上這事∨涿ǎ” “怎么了幅恋?”我有些...
    開(kāi)封第一講書人閱讀 164,491評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)泵肄。 經(jīng)常有香客問(wèn)我佳遣,道長(zhǎng),這世上最難降的妖魔是什么凡伊? 我笑而不...
    開(kāi)封第一講書人閱讀 58,636評(píng)論 1 293
  • 正文 為了忘掉前任零渐,我火速辦了婚禮,結(jié)果婚禮上系忙,老公的妹妹穿的比我還像新娘诵盼。我一直安慰自己,他們只是感情好银还,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布风宁。 她就那樣靜靜地躺著,像睡著了一般蛹疯。 火紅的嫁衣襯著肌膚如雪戒财。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書人閱讀 51,541評(píng)論 1 305
  • 那天捺弦,我揣著相機(jī)與錄音饮寞,去河邊找鬼孝扛。 笑死,一個(gè)胖子當(dāng)著我的面吹牛幽崩,可吹牛的內(nèi)容都是我干的苦始。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼慌申,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼陌选!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起蹄溉,我...
    開(kāi)封第一講書人閱讀 39,211評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤咨油,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后柒爵,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體臼勉,經(jīng)...
    沈念sama閱讀 45,655評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評(píng)論 3 336
  • 正文 我和宋清朗相戀三年餐弱,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了宴霸。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡膏蚓,死狀恐怖瓢谢,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情驮瞧,我是刑警寧澤氓扛,帶...
    沈念sama閱讀 35,684評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站论笔,受9級(jí)特大地震影響采郎,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜狂魔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評(píng)論 3 329
  • 文/蒙蒙 一蒜埋、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧最楷,春花似錦整份、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 31,894評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至犯建,卻和暖如春讲冠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背适瓦。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,012評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工竿开, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留谱仪,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,126評(píng)論 3 370
  • 正文 我出身青樓德迹,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親揭芍。 傳聞我的和親對(duì)象是個(gè)殘疾皇子胳搞,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評(píng)論 2 355