SpringMVC异常处理

1、局部异常处理

  • 在J2EE项目的开发中,不管是对底层的数据库操作过程,还是业务层的处理过程,还是控制层的处理过程,都不可避免会遇到各种可预知的、不可预知的异常需要处理。每个过程都单独处理异常,系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大。 而SpringMVC提供了强大的异常处理机制,既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。新建一个SpringMVC项目,基础示例代码为https://perfectcode.top/2020/12/20/SpringMVC%E8%BF%90%E8%A1%8C%E6%B5%81%E7%A8%8B/

  • ①新建一个ExceptionController,编写一个异常请求以及一个异常方法,并在异常方法上标注@ExceptionHandler注解,即告诉SpringMVC此方法专门用来处理ExceptionController类发生的异常。

    • 异常方法只能携带异常信息,参数位置不能写model,但可以返回ModelAndView。
    • 如果有多个@ExceptionHandler方法能处理同一个异常,则精确优先。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Controller
    public class ExceptionController {

    @RequestMapping(value = "/exception")
    public String handler(){
    int i=1/0;
    return "success";
    }

    @ExceptionHandler(value = {NullPointerException.class,ArithmeticException.class})
    public ModelAndView handleException1(Exception exception){
    System.out.println("异常为:" + exception);
    ModelAndView modelAndView = new ModelAndView("error");
    modelAndView.addObject("exception", exception);
    return modelAndView;
    }
    }
  • ②在webapp/WEB-INF/pages下新建一个error.jsp的自定义错误信息页面,并取出错误信息并显示。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    <%@ page contentType="text/html;charset=UTF-8" language="java" %>
    <html>
    <head>
    <title>Title</title>
    </head>
    <body>
    <h1>请求错误</h1>
    错误信息为:${exception}
    </body>
    </html>
  • ③运行项目,访问/exception请求后来到错误页面,并能正确打印错误信息。

2、全局异常处理

  • 虽然以上示例能处理异常,但是会发现每个异常方法都只能处理本类的异常,于是可以改成全局异常处理方式。

    • 新建一个GlobalExceptionHandler类用来处理全局异常,并添加@ControllerAdvice注解,其作用是将处理异常的类加入到ioc容器中以及告诉SpringMVC这是一个全局异常处理类。如果Controller里有@ExceptionHandler方法能处理这个异常,则本类优先,就不会使用全局异常处理。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      @ControllerAdvice
      public class GlobalExceptionHandler {

      @ExceptionHandler(value = {NullPointerException.class,ArithmeticException.class})
      public ModelAndView handleException1(Exception exception){
      System.out.println("全局异常为:" + exception);
      ModelAndView modelAndView = new ModelAndView("error");
      modelAndView.addObject("exception", exception);
      return modelAndView;
      }
      }

3、使用@ResponseStatus

  • @ResponseStatus可以加在自定义运行时异常类上,那么它将会以指定的HTTP状态码和指定的reson响应到浏览器,我们自定义异常的目的就是为了让它正确表述我们的思想,所以给其设置响应状态码和原因让其准确表达我们的目的。注意如果要使@ResponseStatus处理异常能够生效,需不使用@ExceptionHandler。

    1
    2
    3
    @ResponseStatus(reason = "出错了",value = HttpStatus.BAD_REQUEST)
    public class MyErrorException extends RuntimeException {
    }
    1
    2
    3
    4
    5
    6
    7
    8
    @Controller
    public class ExceptionController {

    @RequestMapping(value = "/exception")
    public String handler() {
    throw new MyErrorException();
    }
    }

    启动项目后请求该接口:

4、异常处理原理

  • Spring MVC 通过 HandlerExceptionResolver 处理程序的异常,包括 Handler 映射、数据绑定以及目标方法执行时发生的异常。

    • SpringMVC 提供的 HandlerExceptionResolver 的实现类

    • HandlerExceptionResolver是SpringMVC九大组件之一,能在前端控制器DispatcherServlet类中找到其初始化信息如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      private void initHandlerExceptionResolvers(ApplicationContext context) {
      this.handlerExceptionResolvers = null;

      if (this.detectAllHandlerExceptionResolvers) {
      // Find all HandlerExceptionResolvers in the ApplicationContext, including ancestor contexts.
      Map<String, HandlerExceptionResolver> matchingBeans = BeanFactoryUtils
      .beansOfTypeIncludingAncestors(context, HandlerExceptionResolver.class, true, false);
      if (!matchingBeans.isEmpty()) {
      this.handlerExceptionResolvers = new ArrayList<>(matchingBeans.values());
      // We keep HandlerExceptionResolvers in sorted order.
      AnnotationAwareOrderComparator.sort(this.handlerExceptionResolvers);
      }
      }
      else {
      try {
      HandlerExceptionResolver her =
      context.getBean(HANDLER_EXCEPTION_RESOLVER_BEAN_NAME, HandlerExceptionResolver.class);
      this.handlerExceptionResolvers = Collections.singletonList(her);
      }
      catch (NoSuchBeanDefinitionException ex) {
      // Ignore, no HandlerExceptionResolver is fine too.
      }
      }

      // Ensure we have at least some HandlerExceptionResolvers, by registering
      // default HandlerExceptionResolvers if no other resolvers are found.
      //如果在容器中找不到异常处理器则使用默认的配置
      if (this.handlerExceptionResolvers == null) {
      this.handlerExceptionResolvers = getDefaultStrategies(context, HandlerExceptionResolver.class);
      if (logger.isTraceEnabled()) {
      logger.trace("No HandlerExceptionResolvers declared in servlet '" + getServletName() +
      "': using default strategies from DispatcherServlet.properties");
      }
      }
      }

      对应的默认配置信息在源码的DispatcherServlet.properties中,其位置如下:

      在此配置文件中成功找到默认的三个异常处理器:

      1
      2
      3
      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

      其中ExceptionHandlerExceptionResolver是用来处理@ExceptionHandler注解的;ResponseStatusExceptionResolver是用来处理@ResponseStatus注解的;

      DefaultHandlerExceptionResolver是用来处理SpringMVC自己的异常,其异常有:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      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 (ex instanceof MissingPathVariableException) {
      return handleMissingPathVariable(
      (MissingPathVariableException) ex, request, response, handler);
      }
      else if (ex instanceof MissingServletRequestParameterException) {
      return handleMissingServletRequestParameter(
      (MissingServletRequestParameterException) ex, request, response, handler);
      }
      else if (ex instanceof ServletRequestBindingException) {
      return handleServletRequestBindingException(
      (ServletRequestBindingException) ex, request, response, handler);
      }
      else if (ex instanceof ConversionNotSupportedException) {
      return handleConversionNotSupported(
      (ConversionNotSupportedException) ex, request, response, handler);
      }
      else if (ex instanceof TypeMismatchException) {
      return handleTypeMismatch(
      (TypeMismatchException) ex, request, response, handler);
      }
      else if (ex instanceof HttpMessageNotReadableException) {
      return handleHttpMessageNotReadable(
      (HttpMessageNotReadableException) ex, request, response, handler);
      }
      else if (ex instanceof HttpMessageNotWritableException) {
      return handleHttpMessageNotWritable(
      (HttpMessageNotWritableException) ex, request, response, handler);
      }
      else if (ex instanceof MethodArgumentNotValidException) {
      return handleMethodArgumentNotValidException(
      (MethodArgumentNotValidException) ex, request, response, handler);
      }
      else if (ex instanceof MissingServletRequestPartException) {
      return handleMissingServletRequestPartException(
      (MissingServletRequestPartException) ex, request, response, handler);
      }
      else if (ex instanceof BindException) {
      return handleBindException((BindException) ex, request, response, handler);
      }
      else if (ex instanceof NoHandlerFoundException) {
      return handleNoHandlerFoundException(
      (NoHandlerFoundException) ex, request, response, handler);
      }
      else if (ex instanceof AsyncRequestTimeoutException) {
      return handleAsyncRequestTimeoutException(
      (AsyncRequestTimeoutException) ex, request, response, handler);
      }
    • 现在在DispatcherServlet类的doDispatch方法的processDispatchResult方法处打上断点,运行项目后访问有异常的接口后来到此方法,调用此方法时会传入不为null的Exception而导致会调用processHandlerException进入到处理异常的逻辑:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      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);
      //处理异常后返回一个ModelAndView对象,其中包含了模型数据以及跳转的页面地址
      mv = processHandlerException(request, response, handler, exception);
      errorView = (mv != null);
      }
      }

      // Did the handler return a view to render?
      if (mv != null && !mv.wasCleared()) {
      //页面渲染
      render(mv, request, response);
      if (errorView) {
      WebUtils.clearErrorRequestAttributes(request);
      }
      }
      else {
      if (logger.isTraceEnabled()) {
      logger.trace("No view rendering, null ModelAndView returned.");
      }
      }

      if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
      // Concurrent handling started during a forward
      return;
      }

      if (mappedHandler != null) {
      // Exception (if any) is already handled..
      mappedHandler.triggerAfterCompletion(request, response, null);
      }
      }

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

      // Success and error responses may use different content types
      request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);

      // Check registered HandlerExceptionResolvers...
      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;
      }
      // We might still need view name translation for a plain error model...
      if (!exMv.hasView()) {
      String defaultViewName = getDefaultViewName(request);
      if (defaultViewName != null) {
      exMv.setViewName(defaultViewName);
      }
      }
      if (logger.isTraceEnabled()) {
      logger.trace("Using resolved error view: " + exMv, ex);
      }
      else if (logger.isDebugEnabled()) {
      logger.debug("Using resolved error view: " + exMv);
      }
      WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
      return exMv;
      }

      throw ex;
      }

      其中的handlerExceptionResolvers即为初始化后的默认的三个异常处理器:

      • ①如果是基于@ExceptionHandler注解的异常处理方式,则是使用ExceptionHandlerExceptionResolver来处理的。它会在doResolveException()–>doResolveHandlerMethodException()判断该异常是否符合执行@ExceptionHandler注解标注的方法,如果有则执行被@ExceptionHandler注解标注的自定义方法并返回一个ModelAndView对象,最后调用视图解析器将ModelAndView转换成View后调用其render()方法完成视图渲染从而来到错误界面。

      • ②如果是基于@ResponseStatus注解的异常处理方式,则是使用ResponseStatusExceptionResolver来处理的,它会在doResolveException()中判断当前抛出的异常是否标了@ResponseStatus注解,如果是则通过接连调用resolveResponseStatus()–>applyStatusAndReason()–>response.sendError()使此次请求立即结束并转发出/error请求将错误信息发给tomcat来响应错误页面,最后创建一个空的ModelAndView对象后返回。

      • ③如果出现了SpringMVC框架底层的异常(例如上面列举的MissingServletRequestParameterException异常),则是由DefaultHandlerExceptionResolver来处理的。它会在doResolveException()中判断当前抛出的异常是否是框架底层的异常之一,如果是则进入对应的处理异常的方式并返回一个ModelAndView对象。这里以出现了MissingServletRequestParameterException为例,会调用handleMissingServletRequestParameter()–>response.sendError()使此次请求立即结束并转发出/error请求将错误信息发给tomcat来响应错误页面,最后创建一个空的ModelAndView对象后返回。

      • ④如果以上三个异常解析器都无法处理错误信息,则SpringMVC会抛出异常后底层会转发出/error请求将错误信息发给tomcat来响应错误页面。

    • 自定义异常解析器只需要实现HandlerExceptionResolver接口并实现resolveException()方法后加入ioc容器即可。SpringMVC还有个简单映射异常处理器SimpleMappingExceptionResolver,可以配置哪些异常去哪些页面。在springmvc.xml中配置这个bean:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
      <!--exceptionMappings配置哪些异常去哪些页面-->
      <property name="exceptionMappings">
      <props>
      <!--key是异常全类名,value是指定的页面-->
      <prop key="java.lang.ArithmeticException">error</prop>
      </props>
      </property>
      <!--指定异常信息取出时使用的key,默认是exception-->
      <property name="exceptionAttribute" value="exception"></property>
      </bean>

      需要注意的是加入这个异常处理器后其优先级是最低的,即当前面的三个处理器无法处理时才会轮到它处理,但由于它也实现了Ordered接口,因此可以通过修改order值来改变优先级。