前言
上一篇文章我們聊了聊聊自定義SPI如何與sentinel整合實現熔斷限流。在實現整合測試的過程,出現一個有趣的異常java.lang.reflect.UndeclaredThrowableException,當時在程式碼層做了一個全域性異常捕獲,示例如下
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(Exception.class)
public AjaxResult handleException(Exception e) {
String msg = e.getMessage();
return AjaxResult.error(msg,500);
}
@ExceptionHandler(BlockException.class)
public AjaxResult handleBlockException(BlockException e) {
String msg = e.getMessage();
return AjaxResult.error(msg,429);
}
}
本來預期是觸發限流時,就會捕獲BlockException 異常,再封裝一層渲染出去,沒想到死活捕獲不到BlockException 異常。
問題排查
通過debug發現,該問題是由於jdk動態代理引起,後面查詢了一些資料,後面在官方的API文件查到這麼一段話
他的大意大概是如果代理例項的呼叫處理程式的 invoke 方法丟擲一個經過檢查的異常(不可分配給 RuntimeException 或 Error 的 Throwable),且該異常不可分配給該方法的throws子局宣告的任何異常類,則由代理例項上的方法呼叫丟擲UndeclaredThrowableException異常。
這段話我們可以分析出如下場景
1、真實例項方法上沒有宣告異常,代理例項呼叫時丟擲了受檢異常
2、真實例項方法宣告瞭非受檢異常,代理例項呼叫時丟擲了受檢異常
解決方案
方案一:真實例項也宣告受檢異常
示例:
public class SqlServerDialect implements SqlDialect {
@Override
public String dialect() throws Exception{
return "sqlserver";
}
方案二:jdk動態代理的invoke進行捕獲,同時可以自定義異常丟擲
示例:
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
CircuitBreakerInvocation invocation = new CircuitBreakerInvocation(target,method,args);
try {
return new CircuitBreakerInvoker().proceed(invocation);
} catch (Throwable e) {
throw new CircuitBreakerException(429,"too many request");
}
}
方案三:捕獲InvocationTargetException異常,並丟擲真正的異常
為啥要InvocationTargetException,原因是因為我們自定義的異常是會被InvocationTargetException包裹
示例
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
CircuitBreakerInvocation invocation = new CircuitBreakerInvocation(target,method,args);
try {
return new CircuitBreakerInvoker().proceed(invocation);
//用InvocationTargetException包裹是java.lang.reflect.UndeclaredThrowableException問題
} catch (InvocationTargetException e) {
throw e.getTargetException();
}
}
總結
如果是我們自己實現的元件,推薦直接使用方案三,即捕獲InvocationTargetException異常。
如果是用第三方實現的元件,推薦方案一即在呼叫的例項方法宣告異常,比如在使用springcloud alibaba sentinel熔斷降級是有概率會出現UndeclaredThrowableException異常的,因為它也是基於動態代理,他丟擲來的BlockException也是一個受檢異常。示例如下
public class SqlServerDialect implements SqlDialect {
@Override
public String dialect() throws BlockException{
return "sqlserver";
}
如果使用第三方元件不想方案一,你也可以在第三方元件的基礎上再加一層代理,或者對第三方元件進行切面攔截處理