jdk中動態(tài)代理異常處理分析:UndeclaredThrowableException
背景
在RPC接口調(diào)用場景或者使用動態(tài)代理的場景中,偶爾會出現(xiàn)UndeclaredThrowableException,又或者在使用反射的場景中,出現(xiàn)InvocationTargetException,這都與我們所期望的異常不一致,且將真實的異常信息隱藏在更深一層的堆棧中。本文將重點分析下UndeclaredThrowableException
先給結論
使用jdk動態(tài)代理接口時,若方法執(zhí)行過程中拋出了受檢異常但方法簽名又沒有聲明該異常時則會被代理類包裝成UndeclaredThrowableException拋出。
問題還原
// 接口定義
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");
}
}
// 動態(tài)代理
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,得到的異常堆棧為
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
而我們期望的是
java.sql.SQLException: I test throw an checked Exception at com.learn.reflect.ServiceImpl.foo(ServiceImpl.java:11) ...
原因分析
在上述問題還原中,真實的SQLException被包裝了兩層,先被InvocationTargetException包裝,再被UndeclaredThrowableException包裝。 其中,InvocationTargetException為受檢異常,UndeclaredThrowableException為運行時異常。 為何會被包裝呢,還要從動態(tài)代理的生成的代理類說起。
jdk動態(tài)代理會在運行時生成委托接口的具體實現(xiàn)類,我們通過ProxyGenerator手動生成下class文件,再利用idea解析class文件得到具體代理類: 截取部分:
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());
}
}
}
在調(diào)用“委托類”的foo方法時,實際上調(diào)用的代理類IServiceProxy$1的foo方法,而代理類主要邏輯是調(diào)用InvocationHandler的invoke方法。 異常處理的邏輯是,對RuntimeException、接口已聲明的異常、Error直接拋出,其他異常被包裝成UndeclaredThrowableException拋出。 到這里,或許你已經(jīng)get了,或許你有疑問,在接口實現(xiàn)中的確是throw new SQLException,為什么還會被包裝呢? 再來看IServiceProxy的invoke方法,它就是直接通過反射執(zhí)行目標方法,問題就在這里了。 Method.invoke(Object obj, Object... args)方法聲明中已解釋到,若目標方法拋出了異常,會被包裝成InvocationTargetException。(具體可查看javadoc)
所以,串起來總結就是: 具體方法實現(xiàn)中拋出SQLException被反射包裝為會被包裝成InvocationTargetException,這是個受檢異常,而代理類在處理異常時發(fā)現(xiàn)該異常在接口中沒有聲明,所以包裝為UndeclaredThrowableException。
解決方法
在實現(xiàn)InvocationHandler的invoke方法體中,對method.invoke(target, args);調(diào)用進行try catch,重新 throw InvocationTargetException的cause。即:
@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繼承原則:即子類覆蓋父類或?qū)崿F(xiàn)父接口的方法時,拋出的異常必須在原方法支持的異常列表之內(nèi)。 代理類實現(xiàn)了父接口或覆蓋父類方法
參考
https://www.ibm.com/developerworks/cn/java/j-lo-proxy1/index.html#icomments
總結
以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對大家的學習或者工作具有一定的參考學習價值,如果有疑問大家可以留言交流,謝謝大家對腳本之家的支持。

