问题
在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
4j
public class DubboAspect {
(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
4j
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
12public 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
7public class OrderServiceException extends RuntimeException {
public OrderServiceException(String message) {
super(message);
}
}
单元测试方法:1
2
3
4
5
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
502020-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
55public 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 | // 接口定义 |
运行上面的MainTest,得到的异常堆栈为
1 | 入参:null |
而我们本来期望的是抛出:1
2
3java.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
34package 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
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
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 |
|