1 背景
- 1.1 创建了一个
public interface AbstractDao,作为业务mapper的父类interface,通用方法使用default方式提供:public interface AbstractDao { public default <R, P extends PageParam> Page<R> findPage(....){ //do something } } - 1.2 Mybatis版本:3.4.5
- 1.3 测试时抛出异常
UndeclaredThrowableException:java.lang.reflect.UndeclaredThrowableException at jdk.proxy2/jdk.proxy2.$Proxy28.findPage(Unknown Source) .... Caused by: java.lang.NoSuchMethodException: java.lang.invoke.MethodHandles$Lookup.<init>(java.lang.Class,int) at java.base/java.lang.Class.getConstructor0(Class.java:3517) at java.base/java.lang.Class.getDeclaredConstructor(Class.java:2691) at org.apache.ibatis.binding.MapperProxy.invokeDefaultMethod(MapperProxy.java:75) at org.apache.ibatis.binding.MapperProxy.invoke(MapperProxy.java:53) ... 2 more
2 初步判断
- 2.1 MyBatis本身是通过InvocationHander,实现对Interface方法的动态代理。实现类是
org.apache.ibatis.binding.MapperProxy。MapperProxy相关可参考:MyBatis-一次Select查询过程。
- 2.2 根据异常信息的提示,是MyBatis在执行
MapperProxy.invoke的时候抛出了异常。
3 问题跟进
-
3.1 MyBatis的3.4.5版本的MapperProxy.invoke的实现中,对于default方法是单独处理的:
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { try { if (Object.class.equals(method.getDeclaringClass())) { // method是Object类声明的方法 return method.invoke(this, args); } else if (isDefaultMethod(method)) { // 判断是default方法,调用invokeDefaultMethod; // 上面定义的default findPage,会执行到这里。 return invokeDefaultMethod(proxy, method, args); } } catch (Throwable t) { throw ExceptionUtil.unwrapThrowable(t); } // 获取MapperMethod并执行 final MapperMethod mapperMethod = cachedMapperMethod(method); return mapperMethod.execute(sqlSession, args); } // MapperProxy中default的判断方法 private boolean isDefaultMethod(Method method) { return ((method.getModifiers() & (Modifier.ABSTRACT | Modifier.PUBLIC | Modifier.STATIC)) == Modifier.PUBLIC) && method.getDeclaringClass().isInterface(); } -
3.2 调用default方法
/* invokeDefaultMethod上标注了在jdk7上使用, 当前运行环境是jdk8,不排除会有问题。 */ @UsesJava7 private Object invokeDefaultMethod(Object proxy, Method method, Object[] args) throws Throwable { /* 通过getDeclaredConstructor方法获取MethodHandles.Lookup的构造函数 提供的参数是两个 Class.class, int.class */ final Constructor<MethodHandles.Lookup> constructor = MethodHandles.Lookup.class .getDeclaredConstructor(Class.class, int.class); .... }-
3.2.1 跟进
Class.getDeclaredConstructor方法,可以看到,在执行getConstructor0的时候,如果没有找到符合上面提供的参数的构造函数,则会抛出NoSuchMethodException:private Constructor<T> getConstructor0(Class<?>[] parameterTypes, int which) throws NoSuchMethodException { ReflectionFactory fact = getReflectionFactory(); Constructor<T>[] constructors = privateGetDeclaredConstructors((which == Member.PUBLIC)); for (Constructor<T> constructor : constructors) { if (arrayContentsEq(parameterTypes, fact.getExecutableSharedParameterTypes(constructor))) { return constructor; } } // 没有找到符合参数`Class<?>[] parameterTypes`的构造函数,抛出NoSuchMethodException异常。 throw new NoSuchMethodException(methodToString("<init>", parameterTypes)); }打开JDK8中的MethodHandles.Lookup,可以看到只有两种构造函数:
Lookup(Class<?>)和Lookup(Class<?>, Class<?>, int),并没有Lookup(Class<?>, int),所以这里就抛出了NoSuchMethodException。 -
3.2.2 为什么抛出的是
UndeclaredThrowableException呢?打开InvocationHandler.invoke方法的注释,可以看到这样一段描述:
If a checked exception is thrown by this method that is not assignable to any of the exception types declared in the throws clause of the interface method, then an UndeclaredThrowableException containing the exception that was thrown by this method will be thrown by the method invocation on the proxy instance.而NoSuchMethodException的继承关系是
NoSuchMethodException -> ReflectiveOperationException -> Exception,所以最终抛出了UndeclaredThrowableException。
-
4 总结
- 1 Mybatis的3.4.5版本,在执行interface的
default方法时,使用了jdk7的构造函数参数去获取MethodHandles.Lookup的构造函数,会抛出NoSuchMethodException,最后抛出UndeclaredThrowableException。- 在jdk8下遇到此类问题,升级Mybatis版本即可。
- 2 在使用InvocationHandler实现动态代理的时候,如果invoke执行的代码中抛出了
checked exception,而这个异常又没有在代理的interface的方法中声明,则会UndeclaredThrowableException;比如NoSuchMethodException。- 想要避免抛出UndeclaredThrowableException,可以在执行invoke的代码中,异常都以
RuntimeException的方式抛出。 - 也可以通过
UndeclaredThrowableException.getUndeclaredThrowable()获取底层异常。
- 想要避免抛出UndeclaredThrowableException,可以在执行invoke的代码中,异常都以