欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2024-01-02 23:15:52
首先说结论,使用@controlleradvice配合@exceptionhandler处理全局controller的异常时,如果想要正确匹配自己的自定义异常,需要在controller的方法上抛出相...

首先说结论,使用@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;
  }
}

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

上一篇:

下一篇: