@ExceptionHandler or HandlerExceptionResolver?如何优雅处理全局异常?【享学Spring MVC】(上)

简介: @ExceptionHandler or HandlerExceptionResolver?如何优雅处理全局异常?【享学Spring MVC】(上)

前言


阅读上文,了解到了可以通过自定义HandlerExceptionResolver实现来处理程序异常,当然Spring MVC也内置了一些实现来对异常处理进行支持。但是作为新时代的程序员,我估计已经很少人知道HandlerExceptionResolver这个异常处理器接口(更有甚者连ModelAndView都没听说过也大有人在啊),虽然这不应该,但存在即合理。因此从现象上可以认为使用自定义HandlerExceptionResolver实现的方式去处理异常已经out了,它已经被新的方式所取代:@ExceptionHandler方式,这就是本章节的核心议题,来探讨它的使用以及原理。


回忆上篇文章讲述HandlerExceptionResolver,你是否疑问过这个问题:通过HandlerExceptionResolver如何返回一个json串呢?其实这个问题雷同于:源生Servlet如何给前端返回一个json串呢?因为上文的示例都是返回的一个ModelAndView页面,so本文在最开头先解决这个疑问,为下面内容做个铺垫吧。

HandlerExceptionResolver如何返回JSON格式数据?


基于上篇文章案例自定义了一个异常处理器来处理Handler抛出的异常,示例中返回的是一个页面ModelAndView。但是通常情况下我们的应用都是REST应用,我们的接口返回的都是一个JSON串,那么若接口抛出异常的话我们处理好后也同样的返回一个JSON串比返回一个页面更为合适。

这时若你项目较老,使用的仍旧是HandlerExceptionResolver方式处理异常的话,我在本处提供两种处理方式,供以参考:


方式一:response直接输出json


自定义异常处理器(匿名实现):

@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        // 自定义异常处理器一般请放在首位
        exceptionResolvers.add(0, new AbstractHandlerExceptionResolver() {
            @Override
            protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
                response.setContentType("application/json;charset=UTF-8");
                response.setCharacterEncoding("UTF-8");
                try {
                    String jsonStr = "";
                    if (ex instanceof BusinessException) {
                        response.setStatus(HttpStatus.OK.value());
                        jsonStr = "{'code':100001,'message':'业务异常,请联系客服处理'}";
                    } else {
                        response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
                        jsonStr = "{'code':500,'message':'服务器未知异常'}";
                    }
                    response.getWriter().print(jsonStr);
                    response.getWriter().flush();
                    response.getWriter().close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
                return null;
            }
        });
    }
}


访问截图如下:


image.png


注意事项:


  1. 因为return null,所以后面若还有处理器将继续执行。但因为本处已把response close了,因此请确保后面不会再使用此response
  2. 若所有Resolver处理完后还是return null,那Spring MVC将直接throw ex,因此你看到的效果是:控制台上有异常栈,但是前段页面上显示是友好的json串。
  3. 因为木有ModelAndView(值为null),所以不会有渲染步骤,因此后续步骤Spring MVC也不会再使用到response(自定义的拦截器除外~)。


方式二:借助MappingJackson2JsonView


自定义异常处理器,借助MappingJackson2JsonView这个json视图实现:


@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void extendHandlerExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers) {
        // 自定义异常处理器一般请放在首位
        exceptionResolvers.add(0, new AbstractHandlerExceptionResolver() {
            @Override
            protected ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
                ModelAndView mv = new ModelAndView();
                MappingJackson2JsonView view = new MappingJackson2JsonView();
                view.setJsonPrefix("fsxJson"); // 设置JSON前缀,有的时候很好用的哦
                //view.setModelKey(); // 让只序列化指定的key
                mv.setView(view);
                // 这样添加key value就非常方便
                mv.addObject("code", "100001");
                mv.addObject("message", "业务异常,请联系客服处理");
                return mv;
            }
        });
    }
}


访问截图如下:image.png


显然这种使用JsonView的方式代码看起来更加舒服,使用起来更加的面向对象。


这两种方式都是基于自定义HandlerExceptionResolver实现类的方式来处理异常,最终给前端返回一个json串。虽然方式二看起来步骤也不麻烦,也够面向对象,但接下来的@ExceptionHandler方式可谓是杀手级的应用~


@ExceptionHandler


此注解是Spring 3.0后提供的处理异常的注解,整个Spring在3.0+中新增了大量的能力来对REST应用提供支持,此注解便是其中之一。

它(只能)标注在方法上,可以使得这个方法成为一个异常处理器,处理指定的异常类型。


// @since 3.0
@Target(ElementType.METHOD) // 只能标注在方法上
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
  // 指定异常类型,可以多个
  Class<? extends Throwable>[] value() default {};
}


上篇讲解HandlerExceptionResolver的原理部分讲到了,DispatcherServlet对异常的处理最终都是无一例外的交给了HandlerExceptionResolver异常处理器,因此很容易想到@ExceptionHandler它的底层实现原理其实也是一个异常处理器,它便是:ExceptionHandlerExceptionResolver。


在分析它之前,需要先前置介绍两个类:AbstractHandlerMethodExceptionResolver和ExceptionHandlerMethodResolver


AbstractHandlerMethodExceptionResolver


它是ExceptionHandlerExceptionResolver的抽象父类,服务于处理器类型是HandlerMethod类型的抛出的异常,它并不规定实现方式必须是@ExceptionHandler。它复写了抽象父类AbstractHandlerExceptionResolver的shouldApplyTo方法:


// @since 3.1 专门处理HandlerMethod类型是HandlerMethod类型的异常
public abstract class AbstractHandlerMethodExceptionResolver extends AbstractHandlerExceptionResolver {
  // 只处理HandlerMethod这种类型的处理器抛出的异常~~~~~~
  @Override
  protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
    if (handler == null) {
      return super.shouldApplyTo(request, null);
    } else if (handler instanceof HandlerMethod) {
      HandlerMethod handlerMethod = (HandlerMethod) handler;
      // 可以看到最终getBean表示最终哪去验证的是它所在的Bean类,而不是方法本身
      // 所以异常的控制是针对于Controller这个类的~
      handler = handlerMethod.getBean(); 
      return super.shouldApplyTo(request, handler);
    } else {
      return false;
    }
  }
  @Override
  @Nullable
  protected final ModelAndView doResolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
    return doResolveHandlerMethodException(request, response, (HandlerMethod) handler, ex);
  }
  @Nullable
  protected abstract ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception ex);
}


此抽象类非常简单:规定了只处理HandlerMethod抛出的异常。

ExceptionHandlerMethodResolver(重要)


它是一个会在Class及Class的父类中找出带有@ExceptionHandler注解的类,该类带有key为Throwable,value为Method的缓存属性,提供匹配效率。

// @since 3.1
public class ExceptionHandlerMethodResolver {
  // A filter for selecting {@code @ExceptionHandler} methods.
  public static final MethodFilter EXCEPTION_HANDLER_METHODS = method -> AnnotatedElementUtils.hasAnnotation(method, ExceptionHandler.class);
  // 两个缓存:key:异常类型   value:目标方法Method
  private final Map<Class<? extends Throwable>, Method> mappedMethods = new HashMap<>(16);
  private final Map<Class<? extends Throwable>, Method> exceptionLookupCache = new ConcurrentReferenceHashMap<>(16);
  // 唯一构造函数
  // detectExceptionMappings:传入method,找到这个Method可以处理的所有的异常类型们(注意此方法的逻辑)
  // addExceptionMapping:把异常类型和Method缓存进mappedMethods里
  public ExceptionHandlerMethodResolver(Class<?> handlerType) {
    for (Method method : MethodIntrospector.selectMethods(handlerType, EXCEPTION_HANDLER_METHODS)) {
      for (Class<? extends Throwable> exceptionType : detectExceptionMappings(method)) {
        addExceptionMapping(exceptionType, method);
      }
    }
  }
  // 找到此Method能够处理的所有的异常类型
  // 1、detectAnnotationExceptionMappings:本方法或者父类的方法上标注有ExceptionHandler注解,然后读取出其value值就是它能处理的异常们
  // 2、若value值木有指定,那所有的方法入参们的异常类型,就是此方法能够处理的所有异常们
  // 3、若最终还是空,那就抛出异常:No exception types mapped to " + method
  private List<Class<? extends Throwable>> detectExceptionMappings(Method method) {
    List<Class<? extends Throwable>> result = new ArrayList<>();
    detectAnnotationExceptionMappings(method, result);
    if (result.isEmpty()) {
      for (Class<?> paramType : method.getParameterTypes()) {
        if (Throwable.class.isAssignableFrom(paramType)) {
          result.add((Class<? extends Throwable>) paramType);
        }
      }
    }
    if (result.isEmpty()) {
      throw new IllegalStateException("No exception types mapped to " + method);
    }
    return result;
  }
  // 对于添加方法一样有一句值得说的:
  // 若不同的Method表示可以处理同一个异常,那是不行的:"Ambiguous @ExceptionHandler method mapped for [" 
  // 注意:此处必须是同一个异常(比如Exception和RuntimeException不属于同一个...)
  private void addExceptionMapping(Class<? extends Throwable> exceptionType, Method method) {
    Method oldMethod = this.mappedMethods.put(exceptionType, method);
    if (oldMethod != null && !oldMethod.equals(method)) {
      throw new IllegalStateException("Ambiguous @ExceptionHandler method mapped for [" + exceptionType + "]: {" + oldMethod + ", " + method + "}");
    }
  }
  // 给指定的异常exception匹配上一个Method方法来处理
  // 若有多个匹配上的:使用ExceptionDepthComparator它来排序。若木有匹配的就返回null
  @Nullable
  public Method resolveMethod(Exception exception) {
    return resolveMethodByThrowable(exception);
  }
  // @since 5.0 递归到了couse异常类型 也会处理
  @Nullable
  public Method resolveMethodByThrowable(Throwable exception) {
    Method method = resolveMethodByExceptionType(exception.getClass());
    if (method == null) {
      Throwable cause = exception.getCause();
      if (cause != null) {
        method = resolveMethodByExceptionType(cause.getClass());
      }
    }
    return method;
  }
  //1、先去exceptionLookupCache找,若匹配上了直接返回
  // 2、再去mappedMethods这个缓存里找。很显然可能匹配上多个,那就用ExceptionDepthComparator排序匹配到一个最为合适的
  // 3、匹配上后放进缓存`exceptionLookupCache`,所以下次进来就不需要再次匹配了,这就是缓存的效果
  // ExceptionDepthComparator的基本理论上:精确匹配优先(按照深度比较)
  @Nullable
  public Method resolveMethodByExceptionType(Class<? extends Throwable> exceptionType) {
    Method method = this.exceptionLookupCache.get(exceptionType);
    if (method == null) {
      method = getMappedMethod(exceptionType);
      this.exceptionLookupCache.put(exceptionType, method);
    }
    return method;
  }
}


对于本类的功能,可总结如下:


1.找到指定Class类(可能是Controller本身,也可能是@ControllerAdvice)里面所有标注有@ExceptionHandler的方法们


2.同一个Class内,不能出现同一个(注意理解同一个的含义)异常类型被多个Method处理的情况,否则抛出异常:Ambiguous @ExceptionHandler method mapped for ...

1. 相同异常类型处在不同的Class内的方法上是可以的,比如常见的一个在Controller内,一个在@ControllerAdvice内~


3.提供缓存:

    1. mappedMethods:每种异常对应的处理方法(直接映射代码上书写的异常-方法映射)

    2. exceptionLookupCache:经过按照深度逻辑精确匹配上的Method方法

4.既能处理本身的异常,也能够处理getCause()导致的异常

5.ExceptionDepthComparator的匹配逻辑是按照深度匹配。比如发生的是NullPointerException,但是声明的异常有Throwable和Exception,这是它会根据异常的最近继承关系找到继承深度最浅的那个异常,即Exception。



相关文章
|
13天前
|
Dubbo Java 应用服务中间件
深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案
本文深入探讨了“dubbo+nacos+springboot3的native打包成功后运行出现异常”的原因及解决方案。通过检查GraalVM版本兼容性、配置反射列表、使用代理类、检查配置文件、禁用不支持的功能、查看日志文件、使用GraalVM诊断工具和调整GraalVM配置等步骤,帮助开发者快速定位并解决问题,确保服务的正常运行。
28 1
|
1月前
|
JSON 前端开发 Java
SSM:SpringMVC
本文介绍了SpringMVC的依赖配置、请求参数处理、注解开发、JSON处理、拦截器、文件上传下载以及相关注意事项。首先,需要在`pom.xml`中添加必要的依赖,包括Servlet、JSTL、Spring Web MVC等。接着,在`web.xml`中配置DispatcherServlet,并设置Spring MVC的相关配置,如组件扫描、默认Servlet处理器等。然后,通过`@RequestMapping`等注解处理请求参数,使用`@ResponseBody`返回JSON数据。此外,还介绍了如何创建和配置拦截器、文件上传下载的功能,并强调了JSP文件的放置位置,避免404错误。
|
1月前
|
Java API Spring
springBoot:注解&封装类&异常类&登录实现类 (八)
本文介绍了Spring Boot项目中的一些关键代码片段,包括使用`@PathVariable`绑定路径参数、创建封装类Result和异常处理类GlobalException、定义常量接口Constants、自定义异常ServiceException以及实现用户登录功能。通过这些代码,展示了如何构建RESTful API,处理请求参数,统一返回结果格式,以及全局异常处理等核心功能。
|
1月前
|
前端开发 Java 应用服务中间件
【Spring】Spring MVC的项目准备和连接建立
【Spring】Spring MVC的项目准备和连接建立
54 2
|
1月前
|
Java 关系型数据库 数据库连接
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第3天】Spring Boot项目中数据库连接问题可能源于配置错误或依赖缺失。YAML配置文件的格式不正确,如缩进错误,会导致解析失败;而数据库驱动不匹配、连接字符串或认证信息错误同样引发连接异常。解决方法包括检查并修正YAML格式,确认配置属性无误,以及添加正确的数据库驱动依赖。利用日志记录和异常信息分析可辅助问题排查。
184 10
|
2月前
|
缓存 前端开发 Java
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
Soring Boot的起步依赖、启动流程、自动装配、常用的注解、Spring MVC的执行流程、对MVC的理解、RestFull风格、为什么service层要写接口、MyBatis的缓存机制、$和#有什么区别、resultType和resultMap区别、cookie和session的区别是什么?session的工作原理
【Java面试题汇总】Spring,SpringBoot,SpringMVC,Mybatis,JavaWeb篇(2023版)
|
1月前
|
Java 关系型数据库 MySQL
SpringBoot项目使用yml文件链接数据库异常
【10月更文挑战第4天】本文分析了Spring Boot应用在连接数据库时可能遇到的问题及其解决方案。主要从四个方面探讨:配置文件格式错误、依赖缺失或版本不兼容、数据库服务问题、配置属性未正确注入。针对这些问题,提供了详细的检查方法和调试技巧,如检查YAML格式、验证依赖版本、确认数据库服务状态及用户权限,并通过日志和断点调试定位问题。
|
1月前
|
XML 前端开发 Java
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
本文阐述了Spring、Spring Boot和Spring MVC的关系与区别,指出Spring是一个轻量级、一站式、模块化的应用程序开发框架,Spring MVC是Spring的一个子框架,专注于Web应用和网络接口开发,而Spring Boot则是对Spring的封装,用于简化Spring应用的开发。
116 0
Spring,SpringBoot和SpringMVC的关系以及区别 —— 超准确,可当面试题!!!也可供零基础学习
|
2月前
|
XML 缓存 前端开发
springMVC02,restful风格,请求转发和重定向
文章介绍了RESTful风格的基本概念和特点,并展示了如何使用SpringMVC实现RESTful风格的请求处理。同时,文章还讨论了SpringMVC中的请求转发和重定向的实现方式,并通过具体代码示例进行了说明。
springMVC02,restful风格,请求转发和重定向
|
3月前
|
前端开发 小程序 Java
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅
本文详细介绍了如何在SpringBoot项目中统一处理接口返回结果及全局异常。首先,通过封装`ResponseResult`类,实现了接口返回结果的规范化,包括状态码、状态信息、返回信息和数据等字段,提供了多种成功和失败的返回方法。其次,利用`@RestControllerAdvice`和`@ExceptionHandler`注解配置全局异常处理,捕获并友好地处理各种异常信息。
1153 0
【规范】SpringBoot接口返回结果及异常统一处理,这样封装才优雅