jdk動態代理異常處理分析,UndeclaredThrowableException

背景

在RPC接口調用場景或者使用動態代理的場景中,偶爾會出現UndeclaredThrowableException,又或者在使用反射的場景中,出現InvocationTargetException,這都與咱們所指望的異常不一致,且將真實的異常信息隱藏在更深一層的堆棧中。本文將重點分析下UndeclaredThrowableExceptionhtml

先給結論

使用jdk動態代理接口時,若方法執行過程當中拋出了受檢異常但方法簽名又沒有聲明該異常時則會被代理類包裝成UndeclaredThrowableException拋出。java

問題還原

// 接口定義
public interface IService {
    void foo() throws SQLException;
}
public class ServiceImpl implements IService{
    @Override
    public void foo() throws SQLException {
        throw new SQLException("I test throw an checked Exception");
    }
}
// 動態代理
public class IServiceProxy implements InvocationHandler {
    private Object target;

    IServiceProxy(Object target){
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return method.invoke(target, args);
    }
}

public class MainTest {
    public static void main(String[] args) {
        IService service = new ServiceImpl();
        IService serviceProxy = (IService) Proxy.newProxyInstance(service.getClass().getClassLoader(),
                service.getClass().getInterfaces(), new IServiceProxy(service));
        try {
            serviceProxy.foo();
        } catch (Exception e){
            e.printStackTrace();
        }
    }
}

運行上面的MainTest,獲得的異常堆棧爲sql

java.lang.reflect.UndeclaredThrowableException
    at com.sun.proxy.$Proxy0.foo(Unknown Source)
    at com.learn.reflect.MainTest.main(MainTest.java:16)
Caused by: java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.learn.reflect.IServiceProxy.invoke(IServiceProxy.java:19)
    ... 2 more
Caused by: java.sql.SQLException: I test throw an checked Exception
    at com.learn.reflect.ServiceImpl.foo(ServiceImpl.java:11)
    ... 7 more

而咱們指望的是ide

java.sql.SQLException: I test throw an checked Exception
    at com.learn.reflect.ServiceImpl.foo(ServiceImpl.java:11)
    ...

緣由分析

在上述問題還原中,真實的SQLException被包裝了兩層,先被InvocationTargetException包裝,再被UndeclaredThrowableException包裝。
其中,InvocationTargetException爲受檢異常,UndeclaredThrowableException爲運行時異常。
爲什麼會被包裝呢,還要從動態代理的生成的代理類提及。this

jdk動態代理會在運行時生成委託接口的具體實現類,咱們經過ProxyGenerator手動生成下class文件,再利用idea解析class文件獲得具體代理類:
截取部分:idea

public final class IServiceProxy$1 extends Proxy implements IService {
    private static Method m1;
    private static Method m2;
    private static Method m3;
    private static Method m0;

    public IServiceProxy$1(InvocationHandler var1) throws  {
        super(var1);
    }
    
    public final void foo() throws SQLException {
        try {
            super.h.invoke(this, m3, (Object[])null);
        } catch (RuntimeException | SQLException | Error var2) {
            throw var2;
        } catch (Throwable var3) {
            throw new UndeclaredThrowableException(var3);
        }
    }
    static {
        try {
            m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")});
            m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]);
            m3 = Class.forName("com.learn.reflect.IService").getMethod("foo", new Class[0]);
            m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]);
        } catch (NoSuchMethodException var2) {
            throw new NoSuchMethodError(var2.getMessage());
        } catch (ClassNotFoundException var3) {
            throw new NoClassDefFoundError(var3.getMessage());
        }
    }
}

在調用「委託類」的foo方法時,實際上調用的代理類IServiceProxy$1的foo方法,而代理類主要邏輯是調用InvocationHandler的invoke方法。
異常處理的邏輯是,對RuntimeException、接口已聲明的異常、Error直接拋出,其餘異常被包裝成UndeclaredThrowableException拋出。
到這裏,或許你已經get了,或許你有疑問,在接口實現中的確是throw new SQLException,爲何還會被包裝呢?
再來看IServiceProxy的invoke方法,它就是直接經過反射執行目標方法,問題就在這裏了。
Method.invoke(Object obj, Object... args)方法聲明中已解釋到,若目標方法拋出了異常,會被包裝成InvocationTargetException。(具體可查看javadoc)代理

因此,串起來總結就是:
具體方法實現中拋出SQLException被反射包裝爲會被包裝成InvocationTargetException,這是個受檢異常,而代理類在處理異常時發現該異常在接口中沒有聲明,因此包裝爲UndeclaredThrowableException。code

解決方法

在實現InvocationHandler的invoke方法體中,對method.invoke(target, args);調用進行try catch,從新 throw InvocationTargetException的cause。即:htm

@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        try {
            return method.invoke(target, args);
        } catch (InvocationTargetException e){
            throw e.getCause();
        }

    }

題外話

爲何代理類中對未聲明的受檢異常轉爲UndeclaredThrowableException?
由於Java繼承原則:即子類覆蓋父類或實現父接口的方法時,拋出的異常必須在原方法支持的異常列表以內。
代理類實現了父接口或覆蓋父類方法繼承

參考

https://www.ibm.com/developer...

相關文章
相關標籤/搜索