jdk中动态代理异常处理分析:UndeclaredThrowableException
背景
在RPC接口调用场景或者使用动态代理的场景中,偶尔会出现UndeclaredThrowableException,又或者在使用反射的场景中,出现InvocationTargetException,这都与我们所期望的异常不一致,且将真实的异常信息隐藏在更深一层的堆栈中。本文将重点分析下UndeclaredThrowableException
先给结论
使用jdk动态代理接口时,若方法执行过程中抛出了受检异常但方法签名又没有声明该异常时则会被代理类包装成UndeclaredThrowableException抛出。
问题还原
//接口定义 publicinterfaceIService{ voidfoo()throwsSQLException; } publicclassServiceImplimplementsIService{ @Override publicvoidfoo()throwsSQLException{ thrownewSQLException("ItestthrowancheckedException"); } } //动态代理 publicclassIServiceProxyimplementsInvocationHandler{ privateObjecttarget; IServiceProxy(Objecttarget){ this.target=target; } @Override publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{ returnmethod.invoke(target,args); } } publicclassMainTest{ publicstaticvoidmain(String[]args){ IServiceservice=newServiceImpl(); IServiceserviceProxy=(IService)Proxy.newProxyInstance(service.getClass().getClassLoader(), service.getClass().getInterfaces(),newIServiceProxy(service)); try{ serviceProxy.foo(); }catch(Exceptione){ e.printStackTrace(); } } }
运行上面的MainTest,得到的异常堆栈为
java.lang.reflect.UndeclaredThrowableException atcom.sun.proxy.$Proxy0.foo(UnknownSource) atcom.learn.reflect.MainTest.main(MainTest.java:16) Causedby:java.lang.reflect.InvocationTargetException atsun.reflect.NativeMethodAccessorImpl.invoke0(NativeMethod) atsun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) atsun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) atjava.lang.reflect.Method.invoke(Method.java:498) atcom.learn.reflect.IServiceProxy.invoke(IServiceProxy.java:19) ...2more Causedby:java.sql.SQLException:ItestthrowancheckedException atcom.learn.reflect.ServiceImpl.foo(ServiceImpl.java:11) ...7more
而我们期望的是
java.sql.SQLException:ItestthrowancheckedException atcom.learn.reflect.ServiceImpl.foo(ServiceImpl.java:11) ...
原因分析
在上述问题还原中,真实的SQLException被包装了两层,先被InvocationTargetException包装,再被UndeclaredThrowableException包装。其中,InvocationTargetException为受检异常,UndeclaredThrowableException为运行时异常。为何会被包装呢,还要从动态代理的生成的代理类说起。
jdk动态代理会在运行时生成委托接口的具体实现类,我们通过ProxyGenerator手动生成下class文件,再利用idea解析class文件得到具体代理类:截取部分:
publicfinalclassIServiceProxy$1extendsProxyimplementsIService{ privatestaticMethodm1; privatestaticMethodm2; privatestaticMethodm3; privatestaticMethodm0; publicIServiceProxy$1(InvocationHandlervar1)throws{ super(var1); } publicfinalvoidfoo()throwsSQLException{ try{ super.h.invoke(this,m3,(Object[])null); }catch(RuntimeException|SQLException|Errorvar2){ throwvar2; }catch(Throwablevar3){ thrownewUndeclaredThrowableException(var3); } } static{ try{ m1=Class.forName("java.lang.Object").getMethod("equals",newClass[]{Class.forName("java.lang.Object")}); m2=Class.forName("java.lang.Object").getMethod("toString",newClass[0]); m3=Class.forName("com.learn.reflect.IService").getMethod("foo",newClass[0]); m0=Class.forName("java.lang.Object").getMethod("hashCode",newClass[0]); }catch(NoSuchMethodExceptionvar2){ thrownewNoSuchMethodError(var2.getMessage()); }catch(ClassNotFoundExceptionvar3){ thrownewNoClassDefFoundError(var3.getMessage()); } } }
在调用“委托类”的foo方法时,实际上调用的代理类IServiceProxy$1的foo方法,而代理类主要逻辑是调用InvocationHandler的invoke方法。异常处理的逻辑是,对RuntimeException、接口已声明的异常、Error直接抛出,其他异常被包装成UndeclaredThrowableException抛出。到这里,或许你已经get了,或许你有疑问,在接口实现中的确是thrownewSQLException,为什么还会被包装呢?再来看IServiceProxy的invoke方法,它就是直接通过反射执行目标方法,问题就在这里了。Method.invoke(Objectobj,Object...args)方法声明中已解释到,若目标方法抛出了异常,会被包装成InvocationTargetException。(具体可查看javadoc)
所以,串起来总结就是:具体方法实现中抛出SQLException被反射包装为会被包装成InvocationTargetException,这是个受检异常,而代理类在处理异常时发现该异常在接口中没有声明,所以包装为UndeclaredThrowableException。
解决方法
在实现InvocationHandler的invoke方法体中,对method.invoke(target,args);调用进行trycatch,重新throwInvocationTargetException的cause。即:
@Override publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{ try{ returnmethod.invoke(target,args); }catch(InvocationTargetExceptione){ throwe.getCause(); } }
题外话
为什么代理类中对未声明的受检异常转为UndeclaredThrowableException?因为Java继承原则:即子类覆盖父类或实现父接口的方法时,抛出的异常必须在原方法支持的异常列表之内。代理类实现了父接口或覆盖父类方法
参考
https://www.ibm.com/developerworks/cn/java/j-lo-proxy1/index.html#icomments
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对毛票票的支持。