Hello World

吞风吻雨葬落日 欺山赶海踏雪径

0%

Groovy升级遇到的问题记录

起因

groovy jar包升级groovy-all-2.0.5.jar -> groovy-all-2.4.7.jar后验证发现预发服务器有报错:

1
2
3
4
Ambiguous method overloading for method org.springframework.util.CollectionUtils#isEmpty.
Cannot resolve which method to invoke for [null] due to overlapping prototypes between:
[interface java.util.Collection]
[interface java.util.Map]

排查原因

首先找到报错代码:

1
2
3
4
5
def result = service.query(xxx);
List<ProductModel> productModelList = result.datas;
if(!org.springframework.util.CollectionUtils.isEmpty(productModelList)) {
...
}

这段代码在CollectionUtils.isEmpty行报的以上异常,“不明确的方法调用”。Spring的CollectionUtils代码的确有两个相同参数个数的重载方法:

1
2
3
4
5
6
7
public static boolean isEmpty(Collection<?> collection) {
return collection == null || collection.isEmpty();
}

public static boolean isEmpty(Map<?, ?> map) {
return map == null || map.isEmpty();
}

但是productModelList的类型很明确是定义成了List类型,java代码这么写是不错报错的。而且升级之前也是可以正常运行的,那为什么升级之后就报错了呢? 带着这个疑问我对比分析了groovy的源码,发现了版本的差异点(由Object chooseMethod(String methodName, Object methodOrList, Class[] arguments)入口调用,且arguments参数数组的个数为1,值为null 并没有包含类型数据。):
2.0.5版本源码groovy.lang.MetaClassImpl#chooseMethodInternal

1
2
3
4
5
6
7
8
Object answer;
if (arguments == null || arguments.length == 0) {
answer = MetaClassHelper.chooseEmptyMethodParams(methods);
} else if (arguments.length == 1 && arguments[0] == null) {
answer = MetaClassHelper.chooseMostGeneralMethodWith1NullParam(methods);
} else {
...
}

2.4.7版本源码groovy.lang.MetaClassImpl#chooseMethodInternal

1
2
3
4
5
6
Object answer;
if (arguments == null || arguments.length == 0) {
answer = MetaClassHelper.chooseEmptyMethodParams(methods);
} else {
...
}

少了一段对参数个数是1个,且值为null的情况的特殊处理,在随后的逻辑中,选取对应执行方法的时候根据签名发现存在两个可以执行的候选方法,抛出异常。
判断得出两个匹配的方法逻辑主要是在org.codehaus.groovy.reflection.ParameterTypes#isValidMethod(java.lang.Class[])方法逻辑中,而核心判断逻辑是由org.codehaus.groovy.reflection.CachedClass#isAssignableFrom方法决定的

1
2
3
4
5
6
7
8
9
10
public boolean isAssignableFrom(Class argument) {
return argument == null || ReflectionCache.isAssignableFrom(getTheClass(), argument);
}

//ReflectionCache.isAssignableFrom
public static boolean isAssignableFrom(Class klazz, Class aClass) {
if (klazz == aClass)
return true;
return klazz.isAssignableFrom(aClass);
}

对于argument为null的情况直接返回了true

为什么版本更新后会有这样的改动呢? 查看源码可以发现,原先chooseMostGeneralMethodWith1NullParam方法对于执行方法的选择逻辑是选取方法列表methods中第一个满足可执行的方法返回,而methods中的顺序是由CachedClass类的以下代码确定的。

1
final Method[] dm = getTheClass().getDeclaredMethods();

所以最终决定执行哪个方法是由getDeclaredMethods返回的方法顺序决定的,而getDeclaredMethods在官方的注释中已经明确说明了返回数组中元素的顺序是不确定的!The elements in the returned array are not sorted and are not in any particular order. 所以groovy社区才会认为这是一个BUG: https://issues.apache.org/jira/browse/GROOVY-6289 ,而且这个BUG在2.1.7版本中被修复了(删除了chooseMostGeneralMethodWith1NullParam方法的调用)。

结论

虽然这次的异常是针对CollectionUtils.isEmpty工具类的使用异常情况,但是在了解原理后可以推断出对参数值为null的重载方法调用都会抛出异常。本次升级风险也比较高,需要判断出groovy中哪些调用了重载方法且可能传入的值为null 进行改动(比如在调用中加上强制类型转化)。