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

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

ExceptionHandlerExceptionResolver(重要)


该子类实现就是用于处理标注有@ExceptionHandler注解的HandlerMethod方法的,是@ExceptionHandler功能的实现部分。


请注意命名上和ExceptionHandlerMethodResolver做区分~


// @since 3.1
public class ExceptionHandlerExceptionResolver extends AbstractHandlerMethodExceptionResolver implements ApplicationContextAware, InitializingBean {
  // 这个熟悉:用于处理方法入参的(比如支持入参里可写HttpServletRequest等等)
  @Nullable
  private List<HandlerMethodArgumentResolver> customArgumentResolvers;
  @Nullable
  private HandlerMethodArgumentResolverComposite argumentResolvers;
  // 用于处理方法返回值(ModelAndView、@ResponseBody、@ResponseStatus等)
  @Nullable
  private List<HandlerMethodReturnValueHandler> customReturnValueHandlers;
  @Nullable
  private HandlerMethodReturnValueHandlerComposite returnValueHandlers;
  // 消息处理器和内容协商管理器
  private List<HttpMessageConverter<?>> messageConverters;
  private ContentNegotiationManager contentNegotiationManager = new ContentNegotiationManager();
  // 通知(因为异常是可以做全局效果的)
  private final List<Object> responseBodyAdvice = new ArrayList<>();
  @Nullable
  private ApplicationContext applicationContext;
  // 缓存:异常类型对应的处理器
  // 它缓存着Controller本类,对应的异常处理器(多个@ExceptionHandler)~~~~
  private final Map<Class<?>, ExceptionHandlerMethodResolver> exceptionHandlerCache = new ConcurrentHashMap<>(64);
  // 它缓存ControllerAdviceBean对应的异常处理器(@ExceptionHandler)
  private final Map<ControllerAdviceBean, ExceptionHandlerMethodResolver> exceptionHandlerAdviceCache = new LinkedHashMap<>();
  // 唯一构造函数:注册上默认的消息转换器
  public ExceptionHandlerExceptionResolver() {
    StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
    ...
    this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());
  }
  ... // 省略所有的get/set方法
  @Override
  public void afterPropertiesSet() {
    // Do this first, it may add ResponseBodyAdvice beans
    // 这一步骤同RequestMappingHandlerAdapter#initControllerAdviceCache
    // 目的是找到项目中所有的`ResponseBodyAdvice`,然后缓存起来。
    // 并且把它里面所有的标注有@ExceptionHandler的方法都解析保存起来
    // exceptionHandlerAdviceCache:每个advice切面对应哪个ExceptionHandlerMethodResolver(含多个@ExceptionHandler处理方法)
    //并且,并且若此Advice还实现了接口:ResponseBodyAdvice。那就还可干预到异常处理器的返回值处理上(基于body)
    //可见:若你想干预到异常处理器的返回值body上,可通过ResponseBodyAdvice来实现哟~~~~~~~~~ 
    // 可见ResponseBodyAdvice连异常处理方法也是生效的,但是`RequestBodyAdvice`可就木有啦。
    initExceptionHandlerAdviceCache();
    // 注册默认的参数处理器。支持到了@SessionAttribute、@RequestAttribute
    // ServletRequest/ServletResponse/RedirectAttributes/ModelMethod等等(当然你还可以自定义)
    if (this.argumentResolvers == null) {
      List<HandlerMethodArgumentResolver> resolvers = getDefaultArgumentResolvers();
      this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers);
    }
    // 支持到了:ModelAndView/Model/View/HttpEntity/ModelAttribute/RequestResponseBody
    // ViewName/Map等等这些返回值 当然还可以自定义
    if (this.returnValueHandlers == null) {
      List<HandlerMethodReturnValueHandler> handlers = getDefaultReturnValueHandlers();
      this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers);
    }
  }
  ...
  // 处理HandlerMethod类型的异常。它的步骤是找到标注有@ExceptionHandler匹配的方法
  // 然后执行此方法来处理所抛出的异常
  @Override
  @Nullable
  protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
    // 这个方法是精华,是关键。它最终返回的是一个ServletInvocableHandlerMethod可执行的方法处理器
    // 也就是说标注有@ExceptionHandler的方法最终会成为它
    // 1、本类能够找到处理方法,就在本类里找,找到就返回一个ServletInvocableHandlerMethod
    // 2、本类木有,就去ControllerAdviceBean切面里找,匹配上了也是欧克的
    //   显然此处会判断:advice.isApplicableToBeanType(handlerType) 看此advice是否匹配
    // 若两者都木有找到,那就返回null。这里的核心其实是ExceptionHandlerMethodResolver这个类
    ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception);
    if (exceptionHandlerMethod == null) {
      return null;
    }
    // 给该执行器设置一些值,方便它的指定(封装参数和处理返回值)
    if (this.argumentResolvers != null) {
      exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
    }
    if (this.returnValueHandlers != null) {
      exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
    }
  }
  ...
  // 执行此方法的调用(比couse也传入进去了)
  exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, exception, cause, handlerMethod);
  ... // 下面处理model、ModelAndView、view等等。最终返回一个ModelAndView
  // 这样异常梳理完成。
}


对它的功能,总结如下:


  1. @ExceptionHandler的处理和执行是由本类完成的,同一个Class上的所有@ExceptionHandler方法对应着同一个ExceptionHandlerExceptionResolver,不同Class上的对应着不同的~
  2. 标注有@ExceptionHandler的方法入参上可写:具体异常类型、ServletRequest/ServletResponse/RedirectAttributes/ModelMethod等等
  3. 1. 注意:入参写具体异常类型时只能够写一个类型。(若有多种异常,请写公共父类,你再用instanceof来辨别,而不能直接写多个)
  4. 返回值可写:ModelAndView/Model/View/HttpEntity/ModelAttribute/RequestResponseBody/@ResponseStatus等等
  5. @ExceptionHandler只能标注在方法上。既能标注在Controller本类内的方法上(只对本类生效),也可配合@ControllerAdvice一起使用(对全局生效)
  6. 对步骤4的两种情况,执行时的匹配顺序如下:优先匹配本类(本Controller),再匹配全局的。
  7. 有必要再强调一句:@ExceptionHandler方式并不是只能返回JSON串,步骤4也说了,它返回一个ModelAndView也是ok的


异常处理优先级


上篇文章 加上本文介绍了多种处理异常的方案,在实际生成环境中,我们的项目中一般确实也会存在多个HandlerExceptionResolver异常处理器,那么对于抛出的一个异常,它的处理顺序到底是怎样的呢?


理解了DispatcherServlet默认注册的异常处理器们和它们的执行原理后,再去解答这个问题就易如反掌了。这是DispatcherServlet默认注册的异常处理器们:

image.png


所以在我们没有自定义HandlerExceptionResolver来干扰这种顺序的情况下(绝大部分情况下我们都不会干扰它),最最最最先执行的便是@ExceptionHandler方式的异常处理器,只有匹配不上才会继续执行其它的处理器。

根据此规律,我从使用层面总结出一个结论,供现在还不想深入理解原理的小伙伴参考和记忆:


  1. @Controller + @ExceptionHandler优先级最高
  2. @ControllerAdvice + @ExceptionHandler次之
  3. HandlerExceptionResolver最后(一般是DefaultHandlerExceptionResolver)


全局异常示例


在很多Spring MVC项目中你或许都可以看到一个名字叫GlobalExceptionHandler(名字大同小异)的类,它的作用一般被标注上了@ControllerAdvice/@RestControllerAdvice用于处理全局异常。

但据我了解,大多数项目对此类的设计是相当不完善的,它只做了一个通用处理:处理Exception类型。显然这种宽泛的处理是很不优雅的,理应做细分,那么读了本文后我相信你已有能力去协助完善这一块内容,为你的团队带来改变哈。


此处我给出示例代码,抛砖引玉仅供参考:


@Slf4j
@RestControllerAdvice // 全部返回JSON格式,因为大都是REST项目
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
    // 处理所有不可知的异常,作为全局的兜底
    @ExceptionHandler(Exception.class)
    AppResponse handleException(Exception e){
        log.error(e.getMessage(), e);
        AppResponse response = new AppResponse();
        response.setFail("未知错误,操作失败!");
        return response;
    }
    // 处理所有业务异常(一般为手动抛出)
    @ExceptionHandler(BusinessException.class)
    AppResponse handleBusinessException(BusinessException e){
        log.error(e.getMessage(), e);
        AppResponse response = new AppResponse();
        response.setFail(e.getMessage());
        return response;
    }
    // 处理所有接口参数的数据验证异常(此处特殊处理了这个异常)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    AppResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e){
        log.warn(e.getMessage(), e); //此处我不建议使用error异常...
    // 关于校验的错误信息的返回,此处我知识简单处理,具体你可以加强
        AppResponse response = new AppResponse();
        response.setFail(e.getBindingResult().getAllErrors().get(0).getDefaultMessage());
        return response;
    }
    // 自己定制化处理HttpRequestMethodNotSupportedException这个异常类型喽
    @Override
    protected ResponseEntity<Object> handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        log.warn(ex.getMessage());
        String method = ex.getMethod();
        String[] supportedMethods = ex.getSupportedMethods();
        Map<String, Object> map = new HashMap<>();
        map.put("code", status.value());
        map.put("message", "不支持的请求类型:" + method + ",支持的请求类型:" + Arrays.toString(supportedMethods));
        return super.handleExceptionInternal(ex, map, headers, status, request);
    }
}


可能有人并不清楚为何我要继承ResponseEntityExceptionHandler这个类,下面我就简单介绍一下它。


ResponseEntityExceptionHandler


它是个抽象类,可谓是Spring 3.2后对REST应用异常支持的一个暖心举动。它包装了各种Spring MVC在处理请求时可能抛出的异常的处理,处理结果都是封装成一个ResponseEntity对象。通过ResponseEntity我们可以指定需要响应的状态码、header和body等信息~


因为它是个抽象类,所以我们要使用它只需要定义一个标注有@ControllerAdvice的类继承于它便可(如上示例):

加上全局处理前(被DefaultHandlerExceptionResolver处理的结果):


image.png


加上后:


image.png


因此个人建议若你是REST应用,可以在全局异常处理类上都设计为继承自此类,做兜底使用。它能处理的异常类型如下(同DefaultHandlerExceptionResolver处理的异常类型)


ResponseEntityExceptionHandler:
  @ExceptionHandler({
      HttpRequestMethodNotSupportedException.class,
      HttpMediaTypeNotSupportedException.class,
      HttpMediaTypeNotAcceptableException.class,
      MissingPathVariableException.class,
      MissingServletRequestParameterException.class,
      ServletRequestBindingException.class,
      ConversionNotSupportedException.class,
      TypeMismatchException.class,
      HttpMessageNotReadableException.class,
      HttpMessageNotWritableException.class,
      MethodArgumentNotValidException.class,
      MissingServletRequestPartException.class,
      BindException.class,
      NoHandlerFoundException.class,
      AsyncRequestTimeoutException.class
    })
  @Nullable
  public final ResponseEntity<Object> handleException(Exception ex, WebRequest request) throws Exception { ... }


处理异常时又发生了异常怎么办呢?


这个问题你是否曾思考过呢?其实在理解了上文和本文的内容后,此问题的答案也就浮出水面了,强烈建议有兴趣的同学在本地调试出这种case,有助于你的理解~


结论:若处理器内部又抛出异常,一般就会交给tomcat处理把异常栈输出到前端,显示非常不友好的页面。因此:请务必保证你的异常处理程序中不要出现任何异常,保证健壮性。(当然最最最最为兜底的方案就是架构师统一设计一个HandlerExceptionResolver放在末位,用最简单、最不会出bug的代码来处理一切前面不能处理的异常)

相关文章
|
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 格式的实体数据。
477 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 等。
257 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支持
148 0
|
6月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
本文介绍了在Spring Boot项目中如何通过创建`GlobalExceptionHandler`类来全局处理系统异常。通过使用`@ControllerAdvice`注解,可以拦截项目中的各种异常,并结合`@ExceptionHandler`注解针对特定异常(如参数缺失、空指针等)进行定制化处理。文中详细展示了处理参数缺失异常和空指针异常的示例代码,并说明了通过拦截`Exception`父类实现统一异常处理的方法。虽然拦截`Exception`可一劳永逸,但为便于问题排查,建议优先处理常见异常,最后再兜底处理未知异常,确保返回给调用方的信息友好且明确。
749 0
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——处理系统异常
|
6月前
|
JSON Java 数据格式
微服务——SpringBoot使用归纳——Spring Boot中的全局异常处理——拦截自定义异常
本文介绍了在实际项目中如何拦截自定义异常。首先,通过定义异常信息枚举类 `BusinessMsgEnum`,统一管理业务异常的代码和消息。接着,创建自定义业务异常类 `BusinessErrorException`,并在其构造方法中传入枚举类以实现异常信息的封装。最后,利用 `GlobalExceptionHandler` 拦截并处理自定义异常,返回标准的 JSON 响应格式。文章还提供了示例代码和测试方法,展示了全局异常处理在 Spring Boot 项目中的应用价值。
252 0