Spring MVC的异常处理

使用Spring MVC搭建一个web应用时,我们有很多种办法处理异常并返回异常视图给browser,下面我们分别介绍几种异常的处理方式。

通过HandlerExceptionResolver处理异常

该接口是DispatcherServlet提供的唯一的异常处理机制,在Spring MVC内部所有的异常处理方式都是基于该机制实现的,包括@ExceptionHandler注解。

当一个未捕获的Exception在DispatcherServlet处理请求的过程中发生时,Spring会使用该接口的实现来处理Exception。该接口唯一的方法resolveException抽象了Exception转换为ModelAndView的过程,方法签名是这样的:

ModelAndViewresolveException(HttpServletRequest request,HttpServletResponse response,Object handler,Exception ex)

Spring允许多个该接口的实现同时工作,Spring会将已注册的实现根据order排序后顺序调用,直到某一个实现返回了非空结果,这时Spring会终止调用链并返回ModelAndView。

缺省情况下,ExceptionHandlerExceptionResolver、ResponseStatusExceptionResolver、DefaultHandlerExceptionResolver(排名根据优先级从高到低)这三个实现会被注册到Spring。

Spring内置的HandlerExceptionResolver实现

Spring一共有以下4个典型的HandlerExceptionResolver实现:

SimpleMappingExceptionResolver

该实现需要你配置一个Exception类名到视图的映射清单,他会基于你的配置将Exception映射为视图并返回browser。

你的配置看起是这样的,

java.sql.SqlException=sql_error_view

BizException=biz_error_view

除此之外,该实现还允许你定义视图和response status的映射、要排除的Exception、缺省异常视图、缺省response status等。(注意,这里的response status不用于HttpServletResponse.sendError,只用来HttpServletResponse.setStatus)

缺省情况下该实现并没有注册到Spring,你需要手动将他注册到Spring并进行必要的配置才可使用。

ResponseStatusExceptionResolver

该实现并没有明确指定返回什么视图给browser,只是根据抛出的Exception类的@ResponseStatus注解,调用HttpServletResponse.sendError方法通知servlet容器处理该response status。

你可以这样声明一个自定义Exception,并在用户无权限时在Controller中抛出:

@ResponseStatus(value=HttpStatus.FORBIDDEN)publicclassAuthzExceptionextendsRuntimeException{//...}

需要注意的是,这时如果你没有配置servlet容器的error-page,servlet容器会返回缺省的异常页面给browser。

这往往不是我们希望看到的,所以在使用@ResponseStatus注解时,我们一般要配合error-page或反向代理使用,在下面会有相关的介绍。

DefaultHandlerExceptionResolver

这个类实现了Spring内部的Exception如何映射到response status,并调用HttpServletResponse.sendError方法通知servlet容器处理。

如Spring会在请求的http method和@RequestMapping声明的都不匹配时抛出org.springframework.web.HttpRequestMethodNotSupportedException,该实现收到后会将异常转换为response status 405并调用HttpServletResponse.sendError。

有的同学就会有疑问了,这里Spring自己为什么不用@ResponseStatus注解?观察代码就会发现,这里不仅仅是将Exception简单的映射到response status,还会针对不同的Exception有不同的处理(response.setHeader)和选择性的记录日志,这些是@ResponseStatus注解不能满足的。

因为该实现和上面ResponseStatusExceptionResolver一样只是调用HttpServletResponse.sendError方法通知servlet容器处理,所以你同样需要考虑配合error-page或反向代理返回自定义异常视图给browser。

亦或者你想改变sendError这一处理方式,比如直接返回自定义视图给browser(其实完全可以在error-page中再统一处理,除非你很在意这点性能的话)。这时你可以通过@ExceptionHandler注解(因为优先级的原因,@ExceptionHandler用于处理Spring内部异常时优先级高于该实现)或继承ResponseEntityExceptionHandler(也是基于@ExceptionHandler实现)自己实现Spring内部Exception的处理。

ExceptionHandlerExceptionResolver

这个就是@ExceptionHandler注解的处理实现类,它是一个high-level的实现,下面会专门说。

自己实现HandlerExceptionResolver

当然,如果上面的实现都满足不了需求,你也可以自己实现HandlerExceptionResolver,并使用order控制他与其它实现的执行优先顺序。

通过@ExceptionHandler注解处理异常

相对于HandlerExceptionResolver来言,这是一个high-level的处理方式。因为你基本不再需要和HttpServletRequest、HttpServletResponse这种底层API打交道,而是像编写Controller方法一样使用Spring Controller的几乎所有注解来处理并返回异常(比如@ResponseBody)。这就意味着,不管是根据http请求头的accept返回不同的content type,还是读写request、session都将变的非常简单。

需要注意的是,@ExceptionHandler方法的位置决定了他的作用范围,如果写在Controller中那么他的作用域就是当前Controller,如果写在ControllerAdvice中那么他的作用域就是ControllerAdvice的作用域(未特殊指定的ControllerAdvice就表示作用于全部Controller)。

/**

* 处理RestController产生的异常,返回json。

* @see ErrorController 处理非RestController产生的异常,返回html视图。

*

* @author zaoheng.lb

*/@ControllerAdvice(annotations=RestController.class)publicclassRestErrorController{/**

    * 根据异常类型匹配处理spring mvc抛出的指定异常。

    *

    * 处理下述情况:

    *  1、spring mvc内部异常(如conversion-service、jsr-303 validator)

    *  2、Controller中业务代码的BusinessException异常。

    *

    * @param ex

    * @return

    */@ExceptionHandler({TypeMismatchException.class,BindException.class,BusinessException.class})@ResponseBodypublicResponsehandleException(Exception ex){Response response=createResponse(ex);returnresponse;}}

Spring MVC之外的异常处理

上面说的都是在Spring MVC之内的异常处理,但是在DispatcherServlet之外也需要处理异常,比如filter Exception和HttpServletResponse.sendError产生的异常response status,这些如何处理呢?

servlet error-page

servlet规范中的error-page就是设计用来处理抛出到容器级别的Exception和异常response status的。他支持异常类型和异常response status到异常处理url的配置,也支持缺省的异常处理url配置(用来兜底处理未配置的异常类型和异常response status)。

这是一个用web.xml来配置error-page的示例:

404/404java.sql.SqlException/sqlError/error

你可以编写一个Controller响应“/error”这个url来统一的处理Exception和异常response status,Exception对象等信息可以通过request attribute拿到(如有)。

Spring boot应用

如果你的应用是Spring boot应用,那么恭喜你,你不再需要自己配置error-page和实现异常处理,因为这些Spring都帮你实现好了(包括根据accept返回html或json)。你需要做的仅仅是在视图文件夹(velocity的话就是spring.velocity.resource-loader-path这个配置)下新建一个error文件夹,再将编写好的异常页面根据response status命名后放到这里即可。

例如你的视图文件夹是templates的话,你的异常视图文件结构应该是这样的:

src/

+- main/

    +- java/

    |  +

    +- resources/

        +- templates/

            +- error/

            |  +- 404.vm

            |  +- 5xx.vm

            +-

当然如果Spring boot的默认实现不满足你的需求(比如json属性名称不满足),你可以继承并修改他的行为。详见org.springframework.boot.autoconfigure.web.ErrorMvcAutoConfiguration。

反向代理

nginx等反向代理可以在页面返回browser之前对页面进行修改,所以在nginx中配置error_page也可以达到将异常response status转换为异常页面返回browser的目的。但在nginx中,你无法方便的获取到Java Exception对象等信息。

error_page 404        /404.html;

error_page 502 503    /5xx.html;

总结

个人认为,最佳实践是多种方式配合使用,达到完善的异常处理效果。

方式处理Exception处理异常response status

HandlerExceptionResolver(包括@ExceptionHandler)支持不支持

servlet error-page支持支持

反向代理不支持支持

使用@ExceptionHandler注解处理Controller的Exception:在ExceptionHandler里我们一定可以拿到Exception对象,所以你可以根据Exception对象返回异常视图给browser。

使用servlet error-page兜底处理非Controller Exception和sendError产生的异常response status:此时不一定有Exception对象(如404),所以你可以根据response status返回异常视图给browser。

使用nginx配置一些特殊的异常response status:如502的异常页面,配置后可以防止servlet容器在重启时用户看到nginx的缺省异常页面。

以上,欢迎讨论和指正。(* ̄︶ ̄)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容