概述
异常处理是几乎所有编程语言都具有的特性,主要是处理程序运行时的非预期行为,保证程序的健壮性。JVM 运行时如果遇到未经处理的异常线程将意外退出,为了避免这种情况需要为线程设置默认的异常处理器。
为了将异常处理与 Web 环境整合到一起,Servlet 规范也定义了一系列异常处理的内容。Spring MVC 在 Servlet 规范的基础上更上一层,结合自身特性又添加了自己全局异常处理的能力。这篇将对 Spring MVC 异常处理详细介绍。
Servlet 规范中的异常处理
Spring MVC 基于 Servlet 规范,因此,在介绍 Spring MVC 异常处理之前先对 Servlet 规范中的异常处理加以介绍。
错误页面配置
为了允许当 Servlet 发生异常时返回自定义的内容作为响应体,Servlet 规范允许在部署描述符文件 web.xml 中配置错误页面。
web.xml 错误页面配置格式如下。
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0"> <error-page> <error-code>500</error-code> <location>/error/500</location> <exception-type>java.lang.Exception</exception-type> </error-page> <error-page> <error-code>404</error-code> <location>/error/404</location> </error-page> <error-page> <location>/error</location> </error-page> </web-app>
所有错误页面配置都放到 error-page 标签下,各子标签的含义如下:
location:表示发生异常时请求转发的地址,可以是静态资源 html 或 jsp,也可以是 Servlet 处理的 url,为必填项。
exception-type:异常类型,当 Servlet 抛出的异常匹配该项时才转发请求到location,非必填项。
error-code:HTTP 响应状态码,同样是非必填项。
请求转发到错误页面指定的地址有两种情况。
第一种情况是 Servlet 抛出异常,容器会根据异常类型查找错误页面,如果找不到将会使用仅配置了 location 的错误页面作为默认错误页面。
第二种情况是用户调用了方法 HttpServletResponse#sendError(int sc) ,容器根据这个方法指定的错误码查找错误页面,error-code 就是用来支持这项特性的。
请求属性设置
容器除了在 Servlet 发生异常时将请求转发到错误页,还会将异常的相关信息设置到请求的属性上,以便处理异常的 Servlet 获取,具体包括如下。
这些属性中,javax.servlet.error.exception 在 Servlet 2.3 引入后,javax.servlet.error.exception_type 和 javax.servlet.error.message仅用于保持向后兼容。Spring Boot 中默认的错误页面就使用这些属性。
Servlet 异常处理实战
创建一个仅抛出异常的 Servlet ,并配置到 web.xml,部署到 Tomcat ,项目启动访问后可以看到如下的报错。
这里返回了我们自定义的内容,相对来说更为友好。值得注意的是如果发生异常时转发的错误页面由 Servlet 处理,处理异常的 Servlet 发生了异常,容器将忽略错误页面转而返回默认的内容。
Spring MVC 异常解析器
异常解析器作用范围
Thread 默认的异常处理器作用范围为整个 Thread,Servlet 规范中的异常处理作用范围为 Servlet 处理请求过程,而 Spring MVC 中的异常处理作用范围则为 Spring MVC 处理器处理请求的过程,由异常解析器处理异常。
再把 DispatcherServlet 流程图祭出,添加异常处理部分,如下图所示。Spring MVC 的核心就是 DispatcherServlet 流程图,如果你不熟悉,可先移步《5 分钟彻底理解 Spring MVC》。
上图中右侧矩形内的流程部分就是异常处理的作用范围了,可以看出,Spring MVC 不仅可以处理 handler 产生的异常,还可以处理 interceptor 产生的异常,简化后的流程图如下。
Spring MVC 中的异常解析器捕获从拦截器预执行到拦截器后执行部分的异常,当发生异常时由异常解析器根据异常产生新的视图页面,然后再进行视图渲染。所以如果过滤器 Filter 产生了异常,这里的异常解析器是无法处理的。
默认异常解析器
异常解析器在 Spring MVC 中使用接口 HandlerExceptionResolver 表示,接口定义如下。
public interface HandlerExceptionResolver { @Nullable ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex); }
默认情况下,Spring MVC 中有三种异常解析器,类图如下。
有多个异常解析器的情况下,Spring 将按照异常解析器的顺序获取视图,如果未获取到则使用下一个解析器获取。不管是基于 xml 配置的 Spring MVC,还是基于注解的 Spring MVC,默认的异常解析器顺序及作用都如下。
ExceptionHandlerExceptionResolver:这是用于支持 @ExceptionHandler 注解标注的异常处理器方法的异常解析器,Spring 根据异常类型查找异常处理器方法处理异常。
ResponseStatusExceptionResolver:从异常中解析出响应码,然后调用response.sendError 方法处理异常。
DefaultHandlerExceptionResolver:处理异常方式与 ResponseStatusExceptionResolver 类似,但是只能解析 Spring 内部定义的若干固定类型的异常。
自定义异常解析器
默认情况下,Spring MVC 的异常处理只是对 Servlet 规范中的异常处理进行增强,使用response.sendError 发送错误,如果没有对应的错误页面,响应仍将返回错误堆栈信息。
如果你想定义自己的异常解析器,可以直接实现 HandlerExceptionResolver,并将自定义的类注册为 Spring Bean。@EnanbleWebMvc 注解就使用了这种特性,并提供了 WebMvcConfigurer#extendHandlerExceptionResolvers 方法添加用户自定义的异常解析器,将多个异常解析器组合为一个然后注册为 bean。
Spring MVC 异常处理器
在注解大行其道的今天,通常情况下,我们不会直接配置异常解析器,而是使用默认的异常解析器 ExceptionHandlerExceptionResolver,然后通过 @ExceptionHandler 定义自己的异常处理器,这就是我们所熟悉的异常处理方式了。
异常处理器的配置有两种方式,包括局部异常处理和全局异常处理。
局部异常处理
处理器或拦截器发生异常时,Spring 优先在当前处理器中查找符合条件的异常处理器方法,这里的异常处理器是局部的,只支持当前处理器。
异常处理器方法上需要添加 @ExceptionHandler 注解,标注能处理的异常类型。示例代码如下。
@RestController public class HelloController { @GetMapping("/hello") public String hello() { throw new RuntimeException("处理器发生异常"); } @ExceptionHandler(RuntimeException.class) public ModelAndView handleException(RuntimeException e) { ModelAndView modelAndView = new ModelAndView("error"); modelAndView.addObject("exception", e); return modelAndView; } }
全局异常处理
Spring 如果在当前处理器中查找不到符合条件的异常处理器方法,将在 @ControllerAdvice bean 中根据 @ExceptionHandler 查找异常处理器方法。
@ControllerAdvice bean 中的异常处理器方法是全局的,能处理所有的处理器或拦截器产生的异常。示例代码如下。
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(RuntimeException.class) public Map<String, Object> handleException(RuntimeException e) { Map<String, Object> result = new HashMap<>(); result.put("message", e.getMessage()); return result; } }
全局异常处理是我们使用最多的异常处理方式,利用全局异常处理,可以处理我们自定义的业务异常,还可以结合参数校验,优雅的返回校验错误信息。关于参数校验和全局异常的整合,可以参考《Spring 参数校验最佳实践及原理解析》。
Spring Boot 异常处理
Spring Boot 2.0 版本开始,并没有为异常处理添加新的异常解析器,而是使用了 Servlet 规范中的异常处理,默认将 /error 路径配置为错误页面,并提供了处理异常的 ErrorController,如果你想自定义错误页面逻辑,将自定义的 Controller 实现 ErrorController 即可。
Spring Boot 中 ErrorController 的默认实现是 BasicErrorController,这个 Controller 会将 Servlet 规范中定义的几个错误有关的 request 属性设置到 Model 中,然后以 html 的形式展示。
如果你想修改错误处理页面,可以配置 Spring 的环境变量 server.error.path,最简单的方式是在 application.properties 文件中指定,如 server.error.path=/error。
BasicErrorController 部分代码如下。
@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { @RequestMapping(produces = MediaType.TEXT_HTML_VALUE) public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections .unmodifiableMap(getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView != null) ? modelAndView : new ModelAndView("error", model); } }
这里返回的视图实现为 ErrorMvcAutoConfiguration.StaticView,由ErrorMvcAutoConfiguration.WhitelabelErrorViewConfiguration 中配置的 BeanNameViewResolver 解析,感兴趣可自行查阅源码。最后再看下默认异常处理的效果。
异常处理源码分析
Spring MVC 异常处理的部分位于DispatcherServlet#processDispatchResult
,这个方法本用于处理 handler 产生的视图,在异常发生时会优先将异常解析为视图,简单看下代码。
public class DispatcherServlet extends FrameworkServlet { private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, @Nullable Exception exception) throws Exception { boolean errorView = false; if (exception != null) { // 异常处理 if (exception instanceof ModelAndViewDefiningException) { logger.debug("ModelAndViewDefiningException encountered", exception); mv = ((ModelAndViewDefiningException) exception).getModelAndView(); } else { Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null); mv = processHandlerException(request, response, handler, exception); errorView = (mv != null); } } // 视图渲染 if (mv != null && !mv.wasCleared()) { render(mv, request, response); if (errorView) { WebUtils.clearErrorRequestAttributes(request); } } ... 省略部分代码 } }
异常处理时调用了方法 #processHandlerException
,实现如下。
public class DispatcherServlet extends FrameworkServlet { @Nullable protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { ModelAndView exMv = null; if (this.handlerExceptionResolvers != null) { // 使用异常解析器进行异常处理 for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) { exMv = resolver.resolveException(request, response, handler, ex); if (exMv != null) { break; } } } if (exMv != null) { if (exMv.isEmpty()) { request.setAttribute(EXCEPTION_ATTRIBUTE, ex); return null; } ... 省略部分代码 return exMv; } throw ex; } }
这里又调用了异常解析器进行异常处理,和我们前面的描述是保持一致的,由于篇幅问题其他地方不再分析,感兴趣可自行查阅。
总结
异常处理的目的是为了增强程序的健壮性,Servlet 规范中定义了错误页面允许 Servlet 处理未捕获的异常,Spring Boot 使用了这个特性,提供了默认的错误页面,Spring MVC 还定义了处理 handler 异常的解析器,并允许用户使用 @ExceptionHandler 处理异常。