解决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; } }
以上为个人经验,希望能给大家一个参考,也希望大家多多支持。