Java进阶—动态代理场景下抛出UndeclaredThrowableException

问题

在SpringBoot项目中调用dubbo服务时,使用了Spring的aop对dubbo调用增加了切面功能。在切面类中本来想对异常进行统一处理,抛出项目的统一异常ServiceException,但切面方法抛出的确是UndeclaredThrowableException异常。

几个关键的Java类如下:
切面类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Aspect
@Component
@Slf4j
public class DubboAspect {


@Around(value = "execution(public * com.lzumetal.springboot.aop.dubbo.OrderInvoker.*(..))")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
String name = joinPoint.getSignature().getName();
Object proceed;
try {
proceed = joinPoint.proceed();
} catch (Exception e) {
String errMsg = "服务器错误";
if (e instanceof OrderServiceException) {
errMsg = e.getMessage();
}
throw new ServiceException(500, errMsg);
} finally {
long end = System.currentTimeMillis();
long time = end - start;
log.info("dubbo服务调用耗时|method={}|{}ms", name, time);
}
return proceed;
}


}

dubbo服务调用类(模拟):

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
@Slf4j
public class OrderInvoker {



public Order queryById(Long id) {
log.info("模拟调用dubbo服务...");
throw new OrderServiceException("订单不存在");
}


}

项目统一异常:

1
2
3
4
5
6
7
8
9
10
11
12
public class ServiceException extends Exception {

private int code;

private String msg;

public ServiceException(int code, String msg) {
super(msg);
this.code = code;
}

}

dubbo服务抛出的异常:

1
2
3
4
5
6
7
public class OrderServiceException extends RuntimeException {

public OrderServiceException(String message) {
super(message);
}

}

单元测试方法:

1
2
3
4
5
@Test
public void testQueryOrder() {
Order order = orderInvoker.queryById(1L);
log.info("queryOrder|{}", order);
}

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
2020-07-08 09:28:30.740  INFO 39232 --- [           main] c.l.springboot.aop.dubbo.OrderInvoker    : 模拟调用dubbo服务...
2020-07-08 09:28:30.740 INFO 39232 --- [ main] c.l.springboot.aop.aspect.DubboAspect : dubbo服务调用耗时|method=queryById|10ms

java.lang.reflect.UndeclaredThrowableException
at com.lzumetal.springboot.aop.dubbo.OrderInvoker$$EnhancerBySpringCGLIB$$ce9b2886.queryById(<generated>)
at com.lzumetal.springboot.aop.test.MainTest.testQueryOrder(MainTest.java:29)
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 org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74)
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84)
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268)
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)
Caused by: com.lzumetal.springboot.aop.exception.ServiceException: 订单不存在
at com.lzumetal.springboot.aop.aspect.DubboAspect.aroundAdvice(DubboAspect.java:33)
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 org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethodWithGivenArgs(AbstractAspectJAdvice.java:644)
at org.springframework.aop.aspectj.AbstractAspectJAdvice.invokeAdviceMethod(AbstractAspectJAdvice.java:633)
at org.springframework.aop.aspectj.AspectJAroundAdvice.invoke(AspectJAroundAdvice.java:70)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:93)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:688)
... 32 more

原因

因为Spring aop中的切面类用到了jdk中的动态代理,而在动态代理的实现中,如果调用过程中抛出的异常是检查型异常,并且没有在被代理的接口中声明,则会将异常包装成UndeclaredThrowableException并抛出。

查看InvocationHandler接口的源码,可以看到invoke方法的javadoc注释对抛出的Throwable有明确说明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public interface InvocationHandler {

/**
* Processes a method invocation on a proxy instance and returns
* the result. This method will be invoked on an invocation handler
* when a method is invoked on a proxy instance that it is
* associated with.
*
* @param proxy the proxy instance that the method was invoked on
*
* @param method the {@code Method} instance corresponding to
* the interface method invoked on the proxy instance. The declaring
* class of the {@code Method} object will be the interface that
* the method was declared in, which may be a superinterface of the
* proxy interface that the proxy class inherits the method through.
*
* @param args an array of objects containing the values of the
* arguments passed in the method invocation on the proxy instance,
* or {@code null} if interface method takes no arguments.
* Arguments of primitive types are wrapped in instances of the
* appropriate primitive wrapper class, such as
* {@code java.lang.Integer} or {@code java.lang.Boolean}.
*
* @return the value to return from the method invocation on the
* proxy instance. If the declared return type of the interface
* method is a primitive type, then the value returned by
* this method must be an instance of the corresponding primitive
* wrapper class; otherwise, it must be a type assignable to the
* declared return type. If the value returned by this method is
* {@code null} and the interface method's return type is
* primitive, then a {@code NullPointerException} will be
* thrown by the method invocation on the proxy instance. If the
* value returned by this method is otherwise not compatible with
* the interface method's declared return type as described above,
* a {@code ClassCastException} will be thrown by the method
* invocation on the proxy instance.
*
* @throws Throwable the exception to throw from the method
* invocation on the proxy instance. The exception's type must be
* assignable either to any of the exception types declared in the
* {@code throws} clause of the interface method or to the
* unchecked exception types {@code java.lang.RuntimeException}
* or {@code java.lang.Error}. If a checked exception is
* thrown by this method that is not assignable to any of the
* exception types declared in the {@code throws} clause of
* the interface method, then an
* {@link UndeclaredThrowableException} containing the
* exception that was thrown by this method will be thrown by the
* method invocation on the proxy instance.
*
* @see UndeclaredThrowableException
*/
public Object invoke(Object proxy, Method method, Object[] args)
throws Throwable;
}

现在,我们来通过简单代码演示一下动态代理抛出UndeclaredThrowableException的过程,还原前面在项目中遇到的那个问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// 接口定义
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 {
System.out.println("入参:" + Arrays.toString(args));
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,得到的异常堆栈为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
入参:null
java.lang.reflect.UndeclaredThrowableException
at com.sun.proxy.$Proxy0.foo(Unknown Source)
at com.lzumetal.java.learn.proxy.MainTest.main(MainTest.java:20)
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.lzumetal.java.learn.proxy.IServiceProxy.invoke(IServiceProxy.java:23)
... 2 more
Caused by: java.sql.SQLException: throw an unchecked exception
at com.lzumetal.java.learn.proxy.service.ServiceImpl.foo(ServiceImpl.java:13)
... 7 more

而我们本来期望的是抛出:

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

为什么异常会这样抛出呢?为了再进一步找到更深层次的原因,我们使用JDK自带的ProxyGenerator类的generateProxyClass()方法来生成代理类的字节码(.class文件)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package com.lzumetal.java.learn.proxy;

import com.lzumetal.java.learn.proxy.service.ServiceImpl;
import sun.misc.ProxyGenerator;

import java.io.FileOutputStream;

public class ProxyGeneratorUtil {

private static String DEFAULT_CLASS_NAME = "$Proxy";

public static byte[] saveGenerateProxyClass(String path, Class<?>[] interfaces) {

byte[] classFile = ProxyGenerator.generateProxyClass(DEFAULT_CLASS_NAME, interfaces);
String filePath = path + "/" + DEFAULT_CLASS_NAME + ".class";
try (FileOutputStream out = new FileOutputStream(filePath)) {
out.write(classFile);
out.flush();
} catch (Exception e) {
e.printStackTrace();
}
return classFile;
}

public static void main(String[] args) {

Class<?> interfaces[] = {ServiceImpl.class};
//运行时,确保目录存在
saveGenerateProxyClass("d:\\", interfaces);


}

}

通过Luyten工具,查看字节码反编译后的代码,截取一部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
 public final void foo() throws SQLException {
try {
super.h.invoke(this, $Proxy.m3, null);
}
catch (Error | RuntimeException | SQLException error) {
throw;
}
catch (Throwable t) {
throw new UndeclaredThrowableException(t);
}
}

static {
try {
$Proxy.m1 = Class.forName("java.lang.Object").getMethod("equals", Class.forName("java.lang.Object"));
$Proxy.m8 = Class.forName("com.lzumetal.java.learn.proxy.service.ServiceImpl").getMethod("notify", (Class<?>[])new Class[0]);
$Proxy.m2 = Class.forName("java.lang.Object").getMethod("toString", (Class<?>[])new Class[0]);
$Proxy.m3 = Class.forName("com.lzumetal.java.learn.proxy.service.ServiceImpl").getMethod("foo", (Class<?>[])new Class[0]);
$Proxy.m6 = Class.forName("com.lzumetal.java.learn.proxy.service.ServiceImpl").getMethod("wait", Long.TYPE);
$Proxy.m5 = Class.forName("com.lzumetal.java.learn.proxy.service.ServiceImpl").getMethod("wait", Long.TYPE, Integer.TYPE);
$Proxy.m7 = Class.forName("com.lzumetal.java.learn.proxy.service.ServiceImpl").getMethod("getClass", (Class<?>[])new Class[0]);
$Proxy.m9 = Class.forName("com.lzumetal.java.learn.proxy.service.ServiceImpl").getMethod("notifyAll", (Class<?>[])new Class[0]);
$Proxy.m0 = Class.forName("java.lang.Object").getMethod("hashCode", (Class<?>[])new Class[0]);
$Proxy.m4 = Class.forName("com.lzumetal.java.learn.proxy.service.ServiceImpl").getMethod("wait", (Class<?>[])new Class[0]);
}
catch (NoSuchMethodException ex) {
throw new NoSuchMethodError(ex.getMessage());
}
catch (ClassNotFoundException ex2) {
throw new NoClassDefFoundError(ex2.getMessage());
}
}

从上面的字节码可以看出,代理类抛出异常的逻辑和invoke(Object proxy, Method method, Object[] args)方法上的注释是一致的:对RuntimeException、接口已声明的异常、Error直接抛出,其他异常被包装成UndeclaredThrowableException抛出。
但另一个疑问也来了,SQLException是一个检查型异常,但我们在接口的方法中命名已经申明了抛出SQLException,为什么最终还是抛出了UndeclaredThrowableException?其原因其实在打印的堆栈中也能看出端倪了,即代理类在重写父接口的invoke(Object proxy, Method method, Object[] args)方法时,会去调用反射的method.invoke(target, args)方法,而这个方法是可能会抛出InvocationTargetException异常的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@CallerSensitive
public Object invoke(Object obj, Object... args)
throws IllegalAccessException, IllegalArgumentException,
InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor; // read volatile
if (ma == null) {
ma = acquireMethodAccessor();
}
return ma.invoke(obj, args);
}

当Method类的invoke方法抛出了InvocationTargetException异常,这个异常是受检异常,而代理类在处理异常时发现该异常在接口中没有声明,所以包装为UndeclaredThrowableException,并最终在代理类中抛出。

解决方法

知道了异常抛出的过程,就可以针对原因进行相应的解决了,比如上面的示例,可以在代理类的invoke(Object proxy, Method method, Object[] args)方法中对method.invoke(target, args)进行try-catch,抛出 InvocationTargetException的cause。

1
2
3
4
5
6
7
8
9
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("入参:" + Arrays.toString(args));
try {
return method.invoke(target, args);
} catch (InvocationTargetException e) {
throw e.getCause();
}
}

而对于Spring AOP中代理类中抛出的UndeclaredThrowableException,因为这个代理类是Spring帮我们生成的,我们无法直接修改代理类,所以可以通过Spring的统一异常处理器进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@ControllerAdvice
@Order(value = -9)
@Slf4j
public class DefaultExcpetionHandler {

/**
* aop中因动态代理抛出的自定义ServiceException异常会转换成UndeclaredThrowableException
*
* @param e
* @return
*/
@ExceptionHandler(UndeclaredThrowableException.class)
@ResponseBody
public Response handleUndeclaredThrowableException(UndeclaredThrowableException e) {
Throwable throwable = e.getUndeclaredThrowable();
log.error("处理UndeclaredThrowableException|", throwable);
if (throwable instanceof ServiceException) {
ServiceException exception = (ServiceException) e.getUndeclaredThrowable();
return Response.data(exception.getErrorCode(), exception.getMessage(), null);
}
return Response.data(500, "服务器错误", null);
}
}
------ 本文完 ------