解决spring @ControllerAdvice处理异常无法正确匹配自定义异常

首先说结论,使用@ControllerAdvice配合@ExceptionHandler处理全局controller的异常时,如果想要正确匹配自己的自定义异常,需要在controller的方法上抛出相应的自定义异常,或者自定义异常继承RuntimeException类。

问题描述:

1、在使用@ControllerAdvice配合@ExceptionHandler处理全局异常时,自定义了一个AppException(extends Exception),由于有些全局的参数需要统一验证,所以在所有controller的方法上加一层AOP校验,如果参数校验没通过也抛出AppException

2、在@ControllerAdvice标记的类上,主要有两个@ExceptionHandler,分别匹配AppException.class和Throwable.class。

3、在测试时,由于全局AOP的参数校验没通过,抛出了AppException,但是发现这个AppException被Throwable.class匹配到了,而不是我们想要的AppException.class匹配上。

分析过程:

一阶段

开始由于一直测试的两个不同的请求(一个通过swagger,一个通过游览器地址输入,两个请求比较相似,我以为是同一个请求),一个方法上抛出了AppException,一个没有,然后发现这个问题时现时不现,因为无法稳定复现问题,我猜测可能是AppException出了问题,所以我修改了AppException,将其父类改为了RuntimeException,然后发现问题解决了

二阶段

问题解决后,我又思考了下为啥会出现这种情况,根据java的异常体系来说,无论是继承Exception还是RuntimeException,都不应该会匹配到Throwable.class上去。

我再次跟踪了异常的执行过程,粗略的过了一遍,发现在下面这个位置出现了差别:

catch (InvocationTargetException ex) {
            // Unwrap for HandlerExceptionResolvers ...
            Throwable targetException = ex.getTargetException();
            if (targetException instanceof RuntimeException) {
                throw (RuntimeException) targetException;
            }
            else if (targetException instanceof Error) {
                throw (Error) targetException;
            }
            else if (targetException instanceof Exception) {
                throw (Exception) targetException;
            }
            else {
                String text = getInvocationErrorMessage("Failed to invoke handler method", args);
                throw new IllegalStateException(text, targetException);
            }
        }

成功的走的是Exception,失败的走的是RuntimeException。

这时候到了@ControllerAdvice标记的类时就会出问题了,因为继承AppException是和RuntimeException是平级,所以如果走runtimeException这个判断条件抛出去的异常注定就不会被AppException匹配上。

这时候再仔细对比下异常类型,可以发现正确的那个异常类型时AppException,而错误的那个异常类型时java.lang.reflect.UndeclaredThrowableException,内部包着AppException。

JDK的java doc是这么解释UndeclaredThrowableException的:如果代理实例的调用处理程序的 invoke 方法抛出一个经过检查的异常(不可分配给 RuntimeException 或 Error 的 Throwable),且该异常不可分配给该方法的throws子局声明的任何异常类,则由代理实例上的方法调用抛出此异常。

因为AppException继承于Exception,所以代理抛出的异常就是包着AppException的UndeclaredThrowableException,在@ControllerAdvice匹配的时候自然就匹配不上了。

而当AppException继承于RuntimeException时,抛出的异常依旧是AppException,所以能够被匹配上。

结论:所以解决方法有两种:AppException继承RuntimeException或者Controller的方法抛出AppException异常。

Spring的@ExceptionHandler和@ControllerAdvice统一处理异常

之前敲代码的时候,避免不了各种try…catch,如果业务复杂一点,就会发现全都是try…catch

try{
    ..........
}catch(Exception1 e){
    ..........
}catch(Exception2 e){
    ...........
}catch(Exception3 e){
    ...........
}

这样其实代码既不简洁好看 ,我们敲着也烦, 一般我们可能想到用拦截器去处理, 但是既然现在Spring这么火,AOP大家也不陌生, 那么Spring一定为我们想好了这个解决办法.果然:

@ExceptionHandler

源码

//该注解作用对象为方法
@Target({ElementType.METHOD})
//在运行时有效
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ExceptionHandler {
 //value()可以指定异常类
    Class<? extends Throwable>[] value() default {};
}

@ControllerAdvice

源码

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
//bean对象交给spring管理生成
@Component
public @interface ControllerAdvice {
    @AliasFor("basePackages")
    String[] value() default {};
    @AliasFor("value")
    String[] basePackages() default {};
    Class<?>[] basePackageClasses() default {};
    Class<?>[] assignableTypes() default {};
    Class<? extends Annotation>[] annotations() default {};
}

从名字上可以看出大体意思是控制器增强

所以结合上面我们可以知道,使用@ExceptionHandler,可以处理异常, 但是仅限于当前Controller中处理异常,

@ControllerAdvice可以配置basePackage下的所有controller. 所以结合两者使用,就可以处理全局的异常了.

一、代码

这里需要声明的是,这个统一异常处理类,也是基于ControllerAdvice,也就是控制层切面的,如果是过滤器抛出的异常,不会被捕获!!!

在@ControllerAdvice注解下的类,里面的方法用@ExceptionHandler注解修饰的方法,会将对应的异常交给对应的方法处理。

@ExceptionHandler({IOException.class})
public Result handleException(IOExceptione) {
    log.error("[handleException] ", e);
    return ResultUtil.failureDefaultError();
  }

比如这个,就是捕获IO异常并处理。

废话不多说,代码:

package com.zgd.shop.core.exception;
import com.zgd.shop.core.error.ErrorCache;
import com.zgd.shop.core.result.Result;
import com.zgd.shop.core.result.ResultUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import javax.validation.ValidationException;
import java.util.Set;
/**
 * GlobalExceptionHandle
 * 全局的异常处理
 *
 * @author zgd
 * @date 2019/7/19 11:01
 */
@ControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionHandle {
  /**
   * 请求参数错误
   */
  private final static String BASE_PARAM_ERR_CODE = "BASE-PARAM-01";
  private final static String BASE_PARAM_ERR_MSG = "参数校验不通过";
  /**
   * 无效的请求
   */
  private final static String BASE_BAD_REQUEST_ERR_CODE = "BASE-PARAM-02";
  private final static String BASE_BAD_REQUEST_ERR_MSG = "无效的请求";
  /**
   * 顶级的异常处理
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.OK)
  @ExceptionHandler({Exception.class})
  public Result handleException(Exception e) {
    log.error("[handleException] ", e);
    return ResultUtil.failureDefaultError();
  }
  /**
   * 自定义的异常处理
   *
   * @param ex
   * @return
   */
  @ResponseStatus(HttpStatus.OK)
  @ExceptionHandler({BizServiceException.class})
  public Result serviceExceptionHandler(BizServiceException ex) {
    String errorCode = ex.getErrCode();
    String msg = ex.getErrMsg() == null ? "" : ex.getErrMsg();
    String innerErrMsg;
    String outerErrMsg;
    if (BASE_PARAM_ERR_CODE.equalsIgnoreCase(errorCode)) {
      innerErrMsg = "参数校验不通过:" + msg;
      outerErrMsg = BASE_PARAM_ERR_MSG;
    } else if (ex.isInnerError()) {
      innerErrMsg = ErrorCache.getInternalMsg(errorCode);
      outerErrMsg = ErrorCache.getMsg(errorCode);
      if (StringUtils.isNotBlank(msg)) {
        innerErrMsg = innerErrMsg + "," + msg;
        outerErrMsg = outerErrMsg + "," + msg;
      }
    } else {
      innerErrMsg = msg;
      outerErrMsg = msg;
    }
    log.info("【错误码】:{},【错误码内部描述】:{},【错误码外部描述】:{}", errorCode, innerErrMsg, outerErrMsg);
    return ResultUtil.failure(errorCode, outerErrMsg);
  }
  /**
   * 缺少servlet请求参数抛出的异常
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({MissingServletRequestParameterException.class})
  public Result handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
    log.warn("[handleMissingServletRequestParameterException] 参数错误: " + e.getParameterName());
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   * 请求参数不能正确读取解析时,抛出的异常,比如传入和接受的参数类型不一致
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.OK)
  @ExceptionHandler({HttpMessageNotReadableException.class})
  public Result handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
    log.warn("[handleHttpMessageNotReadableException] 参数解析失败:", e);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   * 请求参数无效抛出的异常
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({MethodArgumentNotValidException.class})
  public Result handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
    BindingResult result = e.getBindingResult();
    String message = getBindResultMessage(result);
    log.warn("[handleMethodArgumentNotValidException] 参数验证失败:" + message);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  private String getBindResultMessage(BindingResult result) {
    FieldError error = result.getFieldError();
    String field = error != null ? error.getField() : "空";
    String code = error != null ? error.getDefaultMessage() : "空";
    return String.format("%s:%s", field, code);
  }
  /**
   * 方法请求参数类型不匹配异常
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({MethodArgumentTypeMismatchException.class})
  public Result handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
    log.warn("[handleMethodArgumentTypeMismatchException] 方法参数类型不匹配异常: ", e);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   * 请求参数绑定到controller请求参数时的异常
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({BindException.class})
  public Result handleHttpMessageNotReadableException(BindException e) {
    BindingResult result = e.getBindingResult();
    String message = getBindResultMessage(result);
    log.warn("[handleHttpMessageNotReadableException] 参数绑定失败:" + message);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   * javax.validation:validation-api 校验参数抛出的异常
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({ConstraintViolationException.class})
  public Result handleServiceException(ConstraintViolationException e) {
    Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
    ConstraintViolation<?> violation = violations.iterator().next();
    String message = violation.getMessage();
    log.warn("[handleServiceException] 参数验证失败:" + message);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   * javax.validation 下校验参数时抛出的异常
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.BAD_REQUEST)
  @ExceptionHandler({ValidationException.class})
  public Result handleValidationException(ValidationException e) {
    log.warn("[handleValidationException] 参数验证失败:", e);
    return ResultUtil.failure(BASE_PARAM_ERR_CODE, BASE_PARAM_ERR_MSG);
  }
  /**
   * 不支持该请求方法时抛出的异常
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.METHOD_NOT_ALLOWED)
  @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
  public Result handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
    log.warn("[handleHttpRequestMethodNotSupportedException] 不支持当前请求方法: ", e);
    return ResultUtil.failure(BASE_BAD_REQUEST_ERR_CODE, BASE_BAD_REQUEST_ERR_MSG);
  }
  /**
   * 不支持当前媒体类型抛出的异常
   *
   * @param e
   * @return
   */
  @ResponseStatus(HttpStatus.UNSUPPORTED_MEDIA_TYPE)
  @ExceptionHandler({HttpMediaTypeNotSupportedException.class})
  public Result handleHttpMediaTypeNotSupportedException(HttpMediaTypeNotSupportedException e) {
    log.warn("[handleHttpMediaTypeNotSupportedException] 不支持当前媒体类型: ", e);
    return ResultUtil.failure(BASE_BAD_REQUEST_ERR_CODE, BASE_BAD_REQUEST_ERR_MSG);
  }
}

至于返回值,就可以理解为controller层方法的返回值,可以返回@ResponseBody,或者页面。我这里是一个@ResponseBody的Result<>,前后端分离。

我们也可以自己根据需求,捕获更多的异常类型。

包括我们自定义的异常类型。比如:

package com.zgd.shop.core.exception;
import lombok.Data;
/**
 * BizServiceException
 * 业务抛出的异常
 * @author zgd
 * @date 2019/7/19 11:04
 */
@Data
public class BizServiceException extends RuntimeException{
  private String errCode;
  private String errMsg;
  private boolean isInnerError;
  public BizServiceException(){
    this.isInnerError=false;
  }
  public BizServiceException(String errCode){
    this.errCode =errCode;
    this.isInnerError = false;
  }
  public BizServiceException(String errCode,boolean isInnerError){
    this.errCode =errCode;
    this.isInnerError = isInnerError;
  }
  public BizServiceException(String errCode,String errMsg){
    this.errCode =errCode;
    this.errMsg = errMsg;
    this.isInnerError = false;
  }
  public BizServiceException(String errCode,String errMsg,boolean isInnerError){
    this.errCode =errCode;
    this.errMsg = errMsg;
    this.isInnerError = isInnerError;
  }
}

以上为个人经验,希望能给大家一个参考,也希望大家多多支持我们。

(0)

相关推荐

  • Spring MVC中异常处理的三种方式

    前言 在 SpringMVC, SpringBoot 处理 web 请求时, 若遇到错误或者异常,返回给用户一个良好的错误信息比 Whitelabel Error Page 好的多. SpringMVC 提供了三种异常处理方式, 良好的运用它们可以给用户提供可读的错误信息. 1. 实现 HandlerExceptionResolver public class AppHandlerExceptionResolver implements HandlerExceptionResolver { @O

  • SpringBoot @ControllerAdvice 拦截异常并统一处理

    在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler.@InitBinder.@ModelAttribute,并应用到所有@RequestMapping中.参考:@ControllerAdvice 文档 一.介绍 创建 MyControllerAdvice,并添加 @ControllerAdvice注解. package com.sam.demo.controller; import org.springframework.ui

  • SpringBoot错误处理机制以及自定义异常处理详解

    上篇文章我们讲解了使用Hibernate Validation来校验数据,当校验完数据后,如果发生错误我们需要给客户返回一个错误信息,因此这节我们来讲解一下SpringBoot默认的错误处理机制以及如何自定义异常来处理请求错误. 一.SpringBoot默认的错误处理机制 我们在发送一个请求的时候,如果发生404 SpringBoot会怎么处理呢?我们来发送一个不存在的请求来验证一下看看页面结果.如下所示: 当服务器内部发生错误的时候,页面会返回什么呢? @GetMapping("/user/{

  • Springboot之自定义全局异常处理的实现

    前言: 在实际的应用开发中,很多时候往往因为一些不可控的因素导致程序出现一些错误,这个时候就要及时把异常信息反馈给客户端,便于客户端能够及时地进行处理,而针对代码导致的异常,我们一般有两种处理方式,一种是throws直接抛出,一种是使用try..catch捕获,一般的话,如果逻辑的异常,需要知道异常信息,我们往往选择将异常抛出,如果只是要保证程序在出错的情况下 依然可以继续运行,则使用try..catch来捕获. 但是try..catch会导致代码量的增加,让后期我们的代码变得臃肿且难以维护.当

  • 浅谈SpringBoot 中关于自定义异常处理的套路

    在 Spring Boot 项目中 ,异常统一处理,可以使用 Spring 中 @ControllerAdvice 来统一处理,也可以自己来定义异常处理方案.Spring Boot 中,对异常的处理有一些默认的策略,我们分别来看. 默认情况下,Spring Boot 中的异常页面 是这样的: 我们从这个异常提示中,也能看出来,之所以用户看到这个页面,是因为开发者没有明确提供一个 /error 路径,如果开发者提供了 /error 路径 ,这个页面就不会展示出来,不过在 Spring Boot 中

  • 解决spring @ControllerAdvice处理异常无法正确匹配自定义异常

    首先说结论,使用@ControllerAdvice配合@ExceptionHandler处理全局controller的异常时,如果想要正确匹配自己的自定义异常,需要在controller的方法上抛出相应的自定义异常,或者自定义异常继承RuntimeException类. 问题描述: 1.在使用@ControllerAdvice配合@ExceptionHandler处理全局异常时,自定义了一个AppException(extends Exception),由于有些全局的参数需要统一验证,所以在所有

  • Spring Boot2深入分析解决java.lang.ArrayStoreException异常

    将某个项目从Spring Boot1升级Spring Boot2之后出现如下报错,查了很多不同的解决方法都没有解决: Spring boot2项目启动时遇到了异常: java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExceptionProxy Caused by: java.lang.ArrayStoreException: sun.reflect.annotation.TypeNotPresentExcep

  • Spring myBatis数据库连接异常问题及解决

    目录 spring myBatis数据库连接异常 报错如下 myBatis连接数据库时报错原因归纳 报错信息 spring myBatis数据库连接异常 报错如下 org.mybatis.spring.MyBatisSystemException: nested exception is org.apache.ibatis.exceptions.PersistenceException: ### Error querying database.  Cause: org.springframewo

  • 完美解决Spring声明式事务不回滚的问题

    疑问,确实像往常一样在service上添加了注解 @Transactional,为什么查询数据库时还是发现有数据不一致的情况,想想肯定是事务没起作用,出现异常的时候数据没有回滚.于是就对相关代码进行了一番测试,结果发现一下踩进了两个坑,确实是事务未回滚导致的数据不一致. 下面总结一下经验教训: Spring事务的管理操作方法 编程式的事务管理 实际应用中很少使用 通过使用TransactionTemplate 手动管理事务 声明式的事务管理 开发中推荐使用(代码侵入最少) Spring的声明式事

  • 解决spring cloud gateway 获取body内容并修改的问题

    之前写过一篇文章,如何获取body的内容. Spring Cloud Gateway获取body内容,不影响GET请求 确实能够获取所有body的内容了,不过今天终端同学调试接口的时候和我说,遇到了400的问题,报错是这样的HTTP method names must be tokens,搜了一下,都是说https引起的.可我的项目还没用https,排除了. 想到是不是因为修改了body内容导致的问题,试着不修改body的内容,直接传给微服务,果然没有报错了. 问题找到,那就好办了,肯定是我新构

  • 解决Spring Security 用户帐号已被锁定问题

    1.问题描述 主要就是org.springframework.security.authentication.LockedException: 用户帐号已被锁定这个异常,完整异常如下: [2020-05-09 16:07:00 下午]:DEBUG org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider$DefaultPreAuthenticationChecks.check

  • 彻底解决Spring MVC中文乱码问题的方案

    乱码是让人很头疼的一件事,本文介绍了彻底解决Spring MVC中文乱码问题的方案,具体如下:  1:表单提交controller获得中文参数后乱码解决方案 注意:  jsp页面编码设置为UTF-8 form表单提交方式为必须为post,get方式下面spring编码过滤器不起效果 <%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%> <form

  • 解决spring boot 1.5.4 配置多数据源的问题

    spring boot 已经支持多数据源配置了,无需网上好多那些编写什么类的,特别麻烦,看看如下解决方案,官方的,放心! 1.首先定义数据源配置 #=====================multiple database config============================ #ds1 first.datasource.url=jdbc:mysql://localhost/test?characterEncoding=utf8&useSSL=true first.datasou

  • 解决vue2.0路由跳转未匹配相应用路由避免出现空白页面的问题

    在做项目的时候,遇到需要做路由跳转,但当用户输入错误url地址,或是其它非法url路由地址,我们或许会想到跳转至404页面.不管你有没有写一个404页面,当出现未匹配路由都需重新指定页面跳转.可能大家首先想到会是路由重定向,redirect来解决这个问题.但实际上通过redirect是没办法更好解决这个问题的. 看代码红色部分 import Vue from 'vue' import Router from 'vue-router' import Hello from '@/components

随机推荐