浅聊Spring MVC的异常处理

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

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

Servlet 规范中的异常处理

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

错误页面配置

<?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.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<String, Object> handleException(RuntimeException e) {
        Map<String, Object> 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<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 处理异常。

 

参考:

 

posted @ 2022-01-11 22:15  残城碎梦  阅读(532)  评论(0编辑  收藏  举报