SpringMVC全局异常处理机制以及源码解析

SpringMVC全局异常处理

SpringMVC除了可以做URL映射请求拦截外,还可以做全局异常的处理。全局异常处理可能我们平时比较少机会接触,但是每个项目都肯定会做这个处理。比如在上一间公司,是前后端分离的架构,所以后端只要有运行时异常就会报“系统异常,请稍后再试”。如果想要走上架构师的话,这个肯定是要学会的。

SpringMVC全局异常处理机制

首先,要知道全局异常处理,SpringMVC提供了两种方式:

  • 实现HandlerExceptionResolver接口,自定义异常处理器。
  • 使用HandlerExceptionResolver接口的子类,也就是SpringMVC提供的异常处理器。

所以,总得来说就两种方式,一种是自定义异常处理器,第二种是SpringMVC提供的。接下来先说SpringMVC提供的几种异常处理器的使用方式,然后再讲自定义异常处理器。

SpringMVC提供的异常处理器有哪些呢?我们可以直接看源码的类图。

在这里插入图片描述

可以看出有四种:

  • DefaultHandlerExceptionResolver,默认的异常处理器。根据各个不同类型的异常,返回不同的异常视图。
  • SimpleMappingExceptionResolver,简单映射异常处理器。通过配置异常类和view的关系来解析异常。
  • ResponseStatusExceptionResolver,状态码异常处理器。解析带有@ResponseStatus注释类型的异常。
  • ExceptionHandlerExceptionResolver,注解形式的异常处理器。对@ExceptionHandler注解的方法进行异常解析。

DefaultHandlerExceptionResolver

这个异常处理器是SprngMVC默认的一个处理器,处理一些常见的异常,比如:没有找到请求参数,参数类型转换异常,请求方式不支持等等。

接着我们看DefaultHandlerExceptionResolver类的doResolveException()方法:

@Override

@Nullable

protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response,@Nullable Object handler, Exception ex){

try {

if (ex instanceof HttpRequestMethodNotSupportedException) {

return handleHttpRequestMethodNotSupported((HttpRequestMethodNotSupportedException) ex, request,

response, handler);

}

elseif (ex instanceof HttpMediaTypeNotSupportedException) {

return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, request, response,

handler);

}

elseif (ex instanceof HttpMediaTypeNotAcceptableException) {

return handleHttpMediaTypeNotAcceptable((HttpMediaTypeNotAcceptableException) ex, request, response,

handler);

}

//省略...以下还有十几种异常的else-if

}catch (Exception handlerException) {

//是否打开日志,如果打开,那就记录日志

if (logger.isWarnEnabled()) {

logger.warn("Handling of [" + ex.getClass().getName() + "] resulted in Exception", handlerException);

}

}

returnnull;

}

通过if-else判断,判断继承什么异常就显示对应的错误码和错误提示信息。由此可以知道,处理一般有两步,一是设置响应码,二是在响应头设置异常信息。下面是MissingServletRequestPartException的处理的源码:

protected ModelAndView handleMissingServletRequestPartException(MissingServletRequestPartException ex,

HttpServletRequest request, HttpServletResponse response, @Nullable Object handler)throws IOException {

//设置响应码,设置异常信息,SC_BAD_REQUEST就是400(bad request)

response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());

returnnew ModelAndView();

}

//响应码

publicstaticfinalint SC_BAD_REQUEST = 400;

为什么要存在这个异常处理器呢?

从框架的设计理念来看,这种公共的、常见的异常应该交给框架本身来完成,是一些必需处理的异常。比如参数类型转换异常,如果程序员不处理,还有框架提供默认的处理方式,不至于出现这种错误而无法排查。

SimpleMappingExceptionResolver

这种异常处理器需要提前配置异常类和对应的view视图。一般用于使用JSP的项目中,出现异常则通过这个异常处理器跳转到指定的页面。

怎么配置?首先搭建JSP项目我就不浪费篇幅介绍了。首先要加载一个XML文件。

@SpringBootApplication

//在启动类,加载配置文件

@ImportResource("classpath:spring-config.xml")

publicclassApplication{

publicstaticvoidmain(String[] args){

SpringApplication.run(Application.class, args);

}

}

然后在resources目录下,创建一个spring-config.xml文件,内容如下:

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

<beansxmlns="http://www.springframework.org/schema/beans"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

<beanclass="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">

<!-- 定义默认的异常处理页面 -->

<propertyname="defaultErrorView"value="err"/>

<!-- 定义异常处理页面用来获取异常信息的变量名,默认名为exception -->

<propertyname="exceptionAttribute"value="ex"/>

<!-- 定义需要特殊处理的异常,用类名或完全路径名作为key,异常也页名作为值 -->

<propertyname="exceptionMappings">

<props>

<!-- 数组越界异常 -->

<propkey="java.lang.ArrayIndexOutOfBoundsException">err/arrayIndexOutOfBounds</prop>

<!-- 空指针异常 -->

<propkey="java.lang.NullPointerException">err/nullPointer</prop>

</props>

</property>

</bean>

</beans>

然后在webapp也就是存放JSP页面的目录下,创建两个JSP页面。

arrayIndexOutOfBounds.jsp如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<html>

<head>

<title>数组越界异常</title>

</head>

<body>

<h1>数组越界异常</h1>

<br>

<%-- 打印异常到页面上 --%>

<% Exception ex = (Exception)request.getAttribute("ex"); %>

<br>

<div><%= ex.getMessage() %></div>

<% ex.printStackTrace(new java.io.PrintWriter(out)); %>

</body>

</html>

nullPointer.jsp如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<html>

<head>

<title>空指针异常</title>

</head>

<body>

<h1>空指针异常</h1>

<br>

<%-- 打印异常到页面上 --%>

<% Exception ex = (Exception)request.getAttribute("ex"); %>

<br>

<div><%=ex.getMessage()%></div>

<% ex.printStackTrace(new java.io.PrintWriter(out)); %>

</body>

</html>

接着创建两个Controller,分别抛出空指针异常和数组越界异常。

@Controller

@RequestMapping("/error")

publicclassErrController{

@RequestMapping("/null")

public String err()throws Exception{

String str = null;

//抛出空指针异常

int length = str.length();

System.out.println(length);

return"index";

}

@RequestMapping("/indexOut")

public String indexOut()throws Exception{

int[] nums = newint[2];

for (int i = 0; i < 3; i++) {

//抛出数组越界异常

nums[i] = i;

System.out.println(nums[i]);

}

return"index";

}

}

启动项目后,我们发送两个请求,就可以看到:

在这里插入图片描述

在这里插入图片描述

其实对于现在前后端分离的项目来说,这种异常处理器已经不是很常用了。

ResponseStatusExceptionResolver

这种异常处理器主要用于处理带有@ResponseStatus注释的异常。下面演示一下使用方式。

首先自定义异常类继承Exception,并且使用@ResponseStatus注解修饰。如下:

//value需要使用HttpStatus枚举类型,HttpStatus.FORBIDDEN=403。

@ResponseStatus(value = HttpStatus.FORBIDDEN,reason = "My defined Exception")

publicclassDefinedExceptionextendsException{

}

然后再在Controller层抛出此异常。如下:

@Controller

@RequestMapping("/error")

publicclassErrController{

@RequestMapping("/myException")

public String ex(@RequestParam(name = "num") Integer num) throws Exception {

if (num == 1) {

//抛出自定义异常

thrownew DefinedException();

}

return"index";

}

}

然后启动项目,请求接口,可以看到如下信息:

在这里插入图片描述

使用这种异常处理器,需要自定义一个异常,一定要一直往上层抛出异常,如果不往上层抛出,在service或者dao层就try-catch处理掉的话,是不会触发的。

ExceptionHandlerExceptionResolver

这个异常处理器才是最重要的,也是最常用,最灵活的,因为是使用注解。首先我们还是简单地演示一下怎么使用:

首先需要定义一个全局的异常处理器。

//这里使用了RestControllerAdvice,是@ResponseBody和@ControllerAdvice的结合

//会把实体类转成JSON格式的提示返回,符合前后端分离的架构

@RestControllerAdvice

publicclassGlobalExceptionHandler{

//这里自定义了一个BaseException,当抛出BaseException异常就会被此方法处理

@ExceptionHandler(BaseException.class)

public ErrorInfo errorHandler(HttpServletRequest req, BaseException e)throws Exception {

ErrorInfo r = new ErrorInfo();

r.setMessage(e.getMessage());

r.setCode(ErrorInfo.ERROR);

r.setUrl(req.getRequestURL().toString());

return r;

}

}

然后我们自定义一个自定义异常类BaseException

publicclassBaseExceptionextendsException{

publicBaseException(String message){

super(message);

}

}

然后在Controller层定义一个方法测试:

@Controller

@RequestMapping("/error")

publicclassErrController{

@RequestMapping("/base")

public String base()throws BaseException {

thrownew BaseException("系统异常,请稍后重试。");

}

}

老规矩,启动项目,请求接口可以看到结果:

在这里插入图片描述

你也可以不自定义异常BaseException,而直接拦截常见的各种异常都可以。所以这是一个非常灵活的异常处理器。你也可以做跳转页面,返回ModelAndView即可(以免篇幅过长就不演示了,哈哈)。

小结

经过以上的演示后我们学习了SpringMVC四种异常处理器的工作机制,最后这种作为程序员我觉得是必须掌握的,前面的简单映射异常处理器和状态映射处理器可以选择性掌握,默认的异常处理器了解即可。

那这么多异常处理器,究竟是如何工作的呢?为什么是设计一个接口,下面有一个抽象类加上四个实现子类呢?接下来我们通过源码分析来揭开谜底!

源码分析

源码分析从哪里入手呢?在SpringMVC中,其实你想都不用想,肯定在DispatcherServlet类里。经过我顺藤摸瓜,我定位在了processHandlerException()方法。怎么定位的呢?其实很简单,看源码:

privatevoidprocessDispatchResult(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);

//省略...

}

}

//省略...

}

processHandlerException()

就是这个直接的一个if-else判断,那个processHandlerException()方法又是怎么处理的呢?

@Nullable

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,

@Nullable Object handler, Exception ex)throws Exception {

ModelAndView exMv = null;

//判断异常处理器的集合是否为空

if (this.handlerExceptionResolvers != null) {

//不为空则遍历异常处理器

for (HandlerExceptionResolver handlerExceptionResolver : this.handlerExceptionResolvers) {

//调用异常处理器的resolveException()方法进行处理异常

exMv = handlerExceptionResolver.resolveException(request, response, handler, ex);

//判断返回的ModelAndView是否为null,不为null则跳出循环,为null则继续下一个异常处理器

if (exMv != null) {

break;

}

}

}

//如果ModelAndView不为空

if (exMv != null) {

if (exMv.isEmpty()) {

//设置异常信息提示

request.setAttribute(EXCEPTION_ATTRIBUTE, ex);

returnnull;

}

//如果返回的ModelAndView不包含view

if (!exMv.hasView()) {

//设置一个默认的视图

String defaultViewName = getDefaultViewName(request);

if (defaultViewName != null) {

exMv.setViewName(defaultViewName);

}

}

//省略...

//返回异常的ModelAndView

return exMv;

}

throw ex;

}

这不就是责任链模式吗!提前加载异常处理器到handlerExceptionResolvers集合中,然后遍历去执行,能处理就处理,不能处理就跳到下一个异常处理器处理。如果对责任链模式有疑问的,可以看《责任链模式与SpringMVC》。

那接下来我们就有一个问题了,handlerExceptionResolvers集合是怎么加载异常处理器的?这个问题很简单,就是使用DispatcherServlet.properties配置文件。这个文件真的很重要!!!

org.springframework.web.servlet.HandlerExceptionResolver=org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver,

org.springframework.web.servlet.mvc.annotation.ResponseStatusExceptionResolver,

org.springframework.web.servlet.mvc.support.DefaultHandlerExceptionResolver

默认是加载以上三种异常处理器到集合中,所以只要带有@ControllerAdvice@ExceptionHandler@ResponseStatus注解的都会被扫描。SimpleMappingExceptionResolver则是通过xml文件(当然也可以使用@Configuration)去配置。

resolveException()

其实在resolveException()处理异常的方法中,还使用了模板模式。

@Override

@Nullable

public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response,

@Nullable Object handler, Exception ex){

//省略...

//预处理

prepareResponse(ex, response);

//调用了一个抽象方法,抽象方法由子类去实现

ModelAndView result = doResolveException(request, response, handler, ex);

//省略...

}

抽象方法doResolveException(),由子类实现。

@Nullable

protectedabstract ModelAndView doResolveException(HttpServletRequest request,

HttpServletResponse response, @Nullable Object handler, Exception ex);

怎么识别模板方法,其实很简单,只要看到抽象类,有个具体方法里面调用了抽象方法,那很大可能就是模板模式。抽象方法就是模板方法,由子类实现。

子类我们都知道就是那四个异常处理器实现类了。

总结

用流程图概括一下:

在这里插入图片描述

经过以上的学习后,我们知道只需要把异常处理器加到集合中,就可以执行。

所以我们可以直接实现HandlerExceptionResolver接口来实现异常处理器。

实现HandlerExceptionResolver接口实现全局异常处理

首先自定一个异常类MyException

publicclassMyExceptionextendsException{

publicMyException(String message){

super(message);

}

}

然后实现HandlerExceptionResolver接口定义一个异常处理器。

//注册异常处理器到Spring容器中

@Component

publicclassMyExceptionHandlerimplementsHandlerExceptionResolver{

@Override

public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex){

try {

//如果属于MyException异常,则输出异常提示到页面

if (ex instanceof MyException) {

response.setContentType("text/html;charset=utf-8");

response.getWriter().println(ex.getMessage());

//这里返回null,不做处理。也可以返回ModelAndView跳转页面

returnnull;

}

} catch (IOException e) {

e.printStackTrace();

}

returnnull;

}

}

然后在Controller层定义一个方法测试:

@Controller

@RequestMapping("/error")

publicclassErrController{

@RequestMapping("/myEx")

public String myEx()throws MyException {

System.out.println("执行myEx()");

thrownew MyException("自定义异常提示信息");

}

}

启动项目,请求接口,我们可以看到:

在这里插入图片描述

最后说几句

以上就是我对于SpringMVC全局异常处理机制的理解。

以上是 SpringMVC全局异常处理机制以及源码解析 的全部内容, 来源链接: utcz.com/a/25387.html

回到顶部