自定義SPI使用JDK動態代理遇到UndeclaredThrowableException異常排查

linyb極客之路發表於2021-12-14

前言

上一篇文章我們聊了聊聊自定義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";
    }

如果使用第三方元件不想方案一,你也可以在第三方元件的基礎上再加一層代理,或者對第三方元件進行切面攔截處理

demo連結

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-spi-enhance/springboot-spi-framework-circuitbreaker

相關文章