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



相关文章
|
6月前
|
前端开发 Java 测试技术
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
本文介绍了 `@RequestParam` 注解的使用方法及其与 `@PathVariable` 的区别。`@RequestParam` 用于从请求中获取参数值(如 GET 请求的 URL 参数或 POST 请求的表单数据),而 `@PathVariable` 用于从 URL 模板中提取参数。文章通过示例代码详细说明了 `@RequestParam` 的常用属性,如 `required` 和 `defaultValue`,并展示了如何用实体类封装大量表单参数以简化处理流程。最后,结合 Postman 测试工具验证了接口的功能。
315 0
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestParam
|
6月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestBody
`@RequestBody` 是 Spring 框架中的注解,用于将 HTTP 请求体中的 JSON 数据自动映射为 Java 对象。例如,前端通过 POST 请求发送包含 `username` 和 `password` 的 JSON 数据,后端可通过带有 `@RequestBody` 注解的方法参数接收并处理。此注解适用于传递复杂对象的场景,简化了数据解析过程。与表单提交不同,它主要用于接收 JSON 格式的实体数据。
476 0
|
6月前
|
前端开发 Java 微服务
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@PathVariable
`@PathVariable` 是 Spring Boot 中用于从 URL 中提取参数的注解,支持 RESTful 风格接口开发。例如,通过 `@GetMapping(&quot;/user/{id}&quot;)` 可以将 URL 中的 `{id}` 参数自动映射到方法参数中。若参数名不一致,可通过 `@PathVariable(&quot;自定义名&quot;)` 指定绑定关系。此外,还支持多参数占位符,如 `/user/{id}/{name}`,分别映射到方法中的多个参数。运行项目后,访问指定 URL 即可验证参数是否正确接收。
289 0
|
6月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RequestMapping
@RequestMapping 是 Spring MVC 中用于请求地址映射的注解,可作用于类或方法上。类级别定义控制器父路径,方法级别进一步指定处理逻辑。常用属性包括 value(请求地址)、method(请求类型,如 GET/POST 等,默认 GET)和 produces(返回内容类型)。例如:`@RequestMapping(value = &quot;/test&quot;, produces = &quot;application/json; charset=UTF-8&quot;)`。此外,针对不同请求方式还有简化注解,如 @GetMapping、@PostMapping 等。
256 0
|
6月前
|
JSON 前端开发 Java
微服务——SpringBoot使用归纳——Spring Boot中的MVC支持——@RestController
本文主要介绍 Spring Boot 中 MVC 开发常用的几个注解及其使用方式,包括 `@RestController`、`@RequestMapping`、`@PathVariable`、`@RequestParam` 和 `@RequestBody`。其中重点讲解了 `@RestController` 注解的构成与特点:它是 `@Controller` 和 `@ResponseBody` 的结合体,适用于返回 JSON 数据的场景。文章还指出,在需要模板渲染(如 Thymeleaf)而非前后端分离的情况下,应使用 `@Controller` 而非 `@RestController`
201 0
|
2月前
|
前端开发 Java API
Spring Cloud Gateway Server Web MVC报错“Unsupported transfer encoding: chunked”解决
本文解析了Spring Cloud Gateway中出现“Unsupported transfer encoding: chunked”错误的原因,指出该问题源于Feign依赖的HTTP客户端与服务端的`chunked`传输编码不兼容,并提供了具体的解决方案。通过规范Feign客户端接口的返回类型,可有效避免该异常,提升系统兼容性与稳定性。
175 0
|
2月前
|
SQL Java 数据库连接
Spring、SpringMVC 与 MyBatis 核心知识点解析
我梳理的这些内容,涵盖了 Spring、SpringMVC 和 MyBatis 的核心知识点。 在 Spring 中,我了解到 IOC 是控制反转,把对象控制权交容器;DI 是依赖注入,有三种实现方式。Bean 有五种作用域,单例 bean 的线程安全问题及自动装配方式也清晰了。事务基于数据库和 AOP,有失效场景和七种传播行为。AOP 是面向切面编程,动态代理有 JDK 和 CGLIB 两种。 SpringMVC 的 11 步执行流程我烂熟于心,还有那些常用注解的用法。 MyBatis 里,#{} 和 ${} 的区别很关键,获取主键、处理字段与属性名不匹配的方法也掌握了。多表查询、动态
109 0
|
2月前
|
JSON 前端开发 Java
第05课:Spring Boot中的MVC支持
第05课:Spring Boot中的MVC支持
144 0
|
6月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
本文介绍了在Spring Boot项目中如何通过创建`GlobalExceptionHandler`类来全局处理系统异常。通过使用`@ControllerAdvice`注解,可以拦截项目中的各种异常,并结合`@ExceptionHandler`注解针对特定异常(如参数缺失、空指针等)进行定制化处理。文中详细展示了处理参数缺失异常和空指针异常的示例代码,并说明了通过拦截`Exception`父类实现统一异常处理的方法。虽然拦截`Exception`可一劳永逸,但为便于问题排查,建议优先处理常见异常,最后再兜底处理未知异常,确保返回给调用方的信息友好且明确。
744 0
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
|
6月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
251 0