转载

Spring MVC的异常处理

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

通过HandlerExceptionResolver处理异常

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

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

ModelAndView resolveException(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)
public class AuthzException extends RuntimeException {
    //...
}
复制代码

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

Spring MVC的异常处理

这往往不是我们希望看到的,所以在使用@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 MVC的异常处理

有的同学就会有疑问了,这里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)
public class RestErrorController {

    /**
     * 根据异常类型匹配处理spring mvc抛出的指定异常。
     * 
     * 处理下述情况:
     *  1、spring mvc内部异常(如conversion-service、jsr-303 validator)
     *  2、Controller中业务代码的BusinessException异常。
     * 
     * @param ex
     * @return
     */
    @ExceptionHandler({ TypeMismatchException.class, BindException.class, BusinessException.class })
    @ResponseBody
    public Response handleException(Exception ex) {
        Response response = createResponse(ex);
        return response;
    }
}
复制代码

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的示例:

<error-page>
    <error-code>404</error-code>
    <location>/404</location>
</error-page>
<error-page>
    <exception-type>java.sql.SqlException</exception-type >
    <location>/sqlError</location>
</error-page>
<error-page>
    <location>/error</location>
</error-page>
复制代码

你可以编写一个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/
     |   + <source code>
     +- resources/
         +- templates/
             +- error/
             |   +- 404.vm
             |   +- 5xx.vm
             +- <other templates>
复制代码

当然如果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的缺省异常页面。

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

Spring MVC的异常处理

原文  https://juejin.im/post/5c9dfcab51882567ef13c509
正文到此结束
Loading...