使用Spring MVC搭建一个web应用时,我们有很多种办法处理异常并返回异常视图给browser,下面我们分别介绍几种异常的处理方式。
该接口是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一共有以下4个典型的HandlerExceptionResolver实现:
该实现需要你配置一个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并进行必要的配置才可使用。
该实现并没有明确指定返回什么视图给browser,只是根据抛出的Exception类的@ResponseStatus注解,调用HttpServletResponse.sendError方法通知servlet容器处理该response status。
你可以这样声明一个自定义Exception,并在用户无权限时在Controller中抛出:
@ResponseStatus(value = HttpStatus.FORBIDDEN) public class AuthzException extends RuntimeException { //... } 复制代码
需要注意的是,这时如果你没有配置servlet容器的error-page,servlet容器会返回缺省的异常页面给browser。
这往往不是我们希望看到的,所以在使用@ResponseStatus注解时,我们一般要配合error-page或反向代理使用,在下面会有相关的介绍。
这个类实现了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的处理。
这个就是@ExceptionHandler注解的处理实现类,它是一个high-level的实现,下面会专门说。
当然,如果上面的实现都满足不了需求,你也可以自己实现HandlerExceptionResolver,并使用order控制他与其它实现的执行优先顺序。
相对于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之内的异常处理,但是在DispatcherServlet之外也需要处理异常,比如filter Exception和HttpServletResponse.sendError产生的异常response status,这些如何处理呢?
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 | 支持 | 支持 |
反向代理 | 不支持 | 支持 |
以上,欢迎讨论和指正。(* ̄︶ ̄)