「聊一聊Spring MVC」异常处理


异常处理是几乎所有编程语言都具有的特性,主要是处理程序运行时的非预期行为,保证程序的健壮性。JVM 运行时如果遇到未经处理的异常线程将意外退出,为了避免这种情况需要为线程设置默认的异常处理器。

为了将异常处理与 Web 环境整合到一起,Servlet 规范也定义了一系列异常处理的内容。Spring MVC 在 Servlet 规范的基础上更上一层,结合自身特性又添加了自己全局异常处理的能力

Servlet 规范中的异常处理

Spring MVC 基于 Servlet 规范,因此,在介绍 Spring MVC 异常处理之前先对 Servlet 规范中的异常处理加以介绍。

错误页面配置

<?xml version="1.0" encoding="UTF-8"?>


    
        500
        /error/500
        java.lang.Exception
    
    
    
        404
        /error/404
    
    
    
        /error
    
    

所有错误页面配置都放到 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.status_code java.lang.Integer 响应码
javax.servlet.error.exception_type java.lang.Class 异常类型
javax.servlet.error.message java.lang.String 错误消息
javax.servlet.error.exception java.lang.Throwable 异常实例
javax.servlet.error.request_uri java.lang.String 异常请求路径
javax.servlet.error.servlet_name java.lang.String 发生异常的 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 中的异常未经处理,因此 Tocmat 直接将异常的堆栈信息直接返回,不仅页面丑陋,而且暴露出了后端的代码信息,很明显这不是我们想要的结果。为了避免这种情况,我们配置一个处理异常的错误页面。再次访问结果如下:

??¨è??é???????¥?????????è?°

这里返回了我们自定义的内容,相对来说更为友好。值得注意的是如果发生异常时转发的错误页面由 Servlet 处理,处理异常的 Servlet 发生了异常,容器将忽略错误页面转而返回默认的内容。

Spring MVC 异常解析器

异常解析器作用范围

Thread 默认的异常处理器作用范围为整个 Thread,Servlet 规范中的异常处理作用范围为 Servlet 处理请求过程,而 Spring MVC 中的异常处理作用范围则为 Spring MVC 处理器处理请求的过程,由异常解析器处理异常。

再把 DispatcherServlet 流程图祭出,添加异常处理部分,如下图所示。Spring MVC 的核心就是 DispatcherServlet 流程图:

??¨è??é???????¥?????????è?°

上图中右侧矩形内的流程部分就是异常处理的作用范围了,可以看出,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 handleException(RuntimeException e) {
        Map result = new HashMap<>();
        result.put("message", e.getMessage());
        return result;
    }
}

全局异常处理是我们使用最多的异常处理方式,利用全局异常处理,可以处理我们自定义的业务异常,还可以结合参数校验,优雅的返回校验错误信息。

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 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 处理异常。

吃水不忘挖井人:

  • 异常处理,为你的应用加层防护