@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。



相关文章
|
27天前
|
缓存 前端开发 Java
Spring MVC 面试题及答案整理,最新面试题
Spring MVC 面试题及答案整理,最新面试题
82 0
|
26天前
|
SQL JavaScript Java
springboot+springm vc+mybatis实现增删改查案例!
springboot+springm vc+mybatis实现增删改查案例!
22 0
|
26天前
|
SQL Java 数据库连接
挺详细的spring+springmvc+mybatis配置整合|含源代码
挺详细的spring+springmvc+mybatis配置整合|含源代码
33 1
|
4天前
|
数据采集 前端开发 Java
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
数据塑造:Spring MVC中@ModelAttribute的高级数据预处理技巧
18 3
|
4天前
|
存储 前端开发 Java
会话锦囊:揭示Spring MVC如何巧妙使用@SessionAttributes
会话锦囊:揭示Spring MVC如何巧妙使用@SessionAttributes
12 1
|
4天前
|
前端开发 Java Spring
数据之桥:深入Spring MVC中传递数据给视图的实用指南
数据之桥:深入Spring MVC中传递数据给视图的实用指南
20 3
|
13天前
|
前端开发 安全 Java
使用Java Web框架:Spring MVC的全面指南
【4月更文挑战第3天】Spring MVC是Spring框架的一部分,用于构建高效、模块化的Web应用。它基于MVC模式,支持多种视图技术。核心概念包括DispatcherServlet(前端控制器)、HandlerMapping(请求映射)、Controller(处理请求)、ViewResolver(视图解析)和ModelAndView(模型和视图容器)。开发流程涉及配置DispatcherServlet、定义Controller、创建View、处理数据、绑定模型和异常处理。
使用Java Web框架:Spring MVC的全面指南
|
20天前
|
敏捷开发 监控 前端开发
Spring+SpringMVC+Mybatis的分布式敏捷开发系统架构
Spring+SpringMVC+Mybatis的分布式敏捷开发系统架构
46 0
|
3月前
|
开发框架 前端开发 .NET
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
ASP.NET CORE 3.1 MVC“指定的网络名不再可用\企图在不存在的网络连接上进行操作”的问题解决过程
38 0
|
8月前
|
存储 开发框架 前端开发
[回馈]ASP.NET Core MVC开发实战之商城系统(五)
经过一段时间的准备,新的一期【ASP.NET Core MVC开发实战之商城系统】已经开始,在之前的文章中,讲解了商城系统的整体功能设计,页面布局设计,环境搭建,系统配置,及首页【商品类型,banner条,友情链接,降价促销,新品爆款】,商品列表页面,商品详情等功能的开发,今天继续讲解购物车功能开发,仅供学习分享使用,如有不足之处,还请指正。
112 0