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

1,802 阅读10分钟

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);
			}
			else if (ex instanceof HttpMediaTypeNotSupportedException) {
				return handleHttpMediaTypeNotSupported((HttpMediaTypeNotSupportedException) ex, request, response,
						handler);
			}
			else if (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);
			}
		}
		return null;
	}

通过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());
		return new ModelAndView();
	}
	
	//响应码
	public static final int SC_BAD_REQUEST = 400;

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

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

SimpleMappingExceptionResolver

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

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

@SpringBootApplication
//在启动类,加载配置文件
@ImportResource("classpath:spring-config.xml")
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="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">
    <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
        <!-- 定义默认的异常处理页面 -->
        <property name="defaultErrorView" value="err"/>
        <!-- 定义异常处理页面用来获取异常信息的变量名,默认名为exception -->
        <property name="exceptionAttribute" value="ex"/>
        <!-- 定义需要特殊处理的异常,用类名或完全路径名作为key,异常也页名作为值 -->
        <property name="exceptionMappings">
            <props>
                <!-- 数组越界异常 -->
                <prop key="java.lang.ArrayIndexOutOfBoundsException">err/arrayIndexOutOfBounds</prop>
                <!-- 空指针异常 -->
                <prop key="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")
public class ErrController {

    @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 = new int[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")
public class DefinedException extends Exception{
}

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

@Controller
@RequestMapping("/error")
public class ErrController {
    @RequestMapping("/myException")
    public String ex(@RequestParam(name = "num") Integer num) throws Exception {
        if (num == 1) {
            //抛出自定义异常
            throw new DefinedException();
        }
        return "index";
    }
}

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

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

ExceptionHandlerExceptionResolver

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

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

//这里使用了RestControllerAdvice,是@ResponseBody和@ControllerAdvice的结合
//会把实体类转成JSON格式的提示返回,符合前后端分离的架构
@RestControllerAdvice
public class GlobalExceptionHandler {
    //这里自定义了一个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

public class BaseException extends Exception {
    public BaseException(String message) {
        super(message);
    }
}

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

@Controller
@RequestMapping("/error")
public class ErrController {
    @RequestMapping("/base")
    public String base() throws BaseException {
        throw new BaseException("系统异常,请稍后重试。");
    }
}

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

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

小结

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

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

源码分析

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

	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);
				//省略...
			}
		}
		//省略...
	}

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);
         return null;
      }
      //如果返回的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
protected abstract ModelAndView doResolveException(HttpServletRequest request,
      HttpServletResponse response, @Nullable Object handler, Exception ex);

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

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

总结

用流程图概括一下:

在这里插入图片描述
经过以上的学习后,我们知道只需要把异常处理器加到集合中,就可以执行。 所以我们可以直接实现HandlerExceptionResolver接口来实现异常处理器。

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

首先自定一个异常类MyException

public class MyException extends Exception {
    public MyException(String message) {
        super(message);
    }
}

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

//注册异常处理器到Spring容器中
@Component
public class MyExceptionHandler implements HandlerExceptionResolver {
    @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跳转页面
                return null;
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

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

@Controller
@RequestMapping("/error")
public class ErrController {
    @RequestMapping("/myEx")
    public String myEx() throws MyException {
        System.out.println("执行myEx()");
        throw new MyException("自定义异常提示信息");
    }
}

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

在这里插入图片描述

最后说几句

以上就是我对于SpringMVC全局异常处理机制的理解。更多的java技术分享,可以关注我的公众号,后续会不断更新。

创作不易,不要白票素质三连,是对我最大的创作动力!有什么疑问可以在评论区留言,非常需要大家的反馈。大佬,你说句话呀

在这里插入图片描述