今天在开发环境调试一个Dubbo接口的时候,遇到这样一个问题

1
invoke me.lijf.xxxProvider.xxxMethod({"foo":"bar"})

提示No such method,然而这个方法绝对是存在的,而且这样的调用方式在我的印象中也是可行的。于是换了一个服务进行测试,发现这样的调用方式能够正常返回结果。回想起前段时间同事把这个服务的依赖的Dubbo升级到了2.5.6,而之前依赖的版本是2.5.3,于是感觉是Dubbo的问题。

直接对比两个版本的InvokeTelnetHandler.javatelnet方法中调用findMethod方法,findMethod中调用isMatch方法,

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
private static boolean isMatch(Class<?>[] types, List<Object> args) {
if (types.length != args.size()) {
return false;
}
for (int i = 0; i < types.length; i ++) {
Class<?> type = types[i];
Object arg = args.get(i);
if (ReflectUtils.isPrimitive(arg.getClass())) {
if (! ReflectUtils.isPrimitive(type)) {
return false;
}
} else if (arg instanceof Map) {
String name = (String) ((Map<?, ?>)arg).get("class");
Class<?> cls = arg.getClass();
if (name != null && name.length() > 0) {
cls = ReflectUtils.forName(name);
}
if (! type.isAssignableFrom(cls)) {
return false;
}
} else if (arg instanceof Collection) {
if (! type.isArray() && ! type.isAssignableFrom(arg.getClass())) {
return false;
}
} else {
if (! type.isAssignableFrom(arg.getClass())) {
return false;
}
}
}
return true;
}

到这里发现,两个版本的isMatch实现是一样的,对于传入的JSON对象,需要在JSON对象中额外传入一个class字段,与方法中实际的参数类型进行匹配。如果没有class参数,会走到

1
2
3
if (!type.isAssignableFrom(cls)) {
return false;
}

那么isMatch返回false,因此传入的JSON对象如果没有class字段,是匹配不到对应的方法的。由此可见,即便是在2.5.3版本中,按理来说也是需要class字段的,那么为什么实际调用的时候我们不需要传也能调通呢?问题出在findMethod方法。
来看2.5.3版本的findMethod方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static Method findMethod(Exporter<?> exporter, String method, List<Object> args) {
Invoker<?> invoker = exporter.getInvoker();
Method[] methods = invoker.getInterface().getMethods();
Method invokeMethod = null;
for (Method m : methods) {
// 假设遍历到xxxMethod
if (m.getName().equals(method) && m.getParameterTypes().length == args.size()) {
// 此时invokeMethod必定为定义时的null,根本不会去调用isMatch
if (invokeMethod != null) { // 重载
if (isMatch(invokeMethod.getParameterTypes(), args)) {
invokeMethod = m;
break;
}
} else {
// 代码走到这里,直接赋值成我们的目标method。
// 之后如果没有同名的方法,则会返回这个方法名及参数个数和我们想要调用的方法都相同的方法,只是恰巧它就是我们需要的方法
invokeMethod = m;
}
invoker = exporter.getInvoker();
}
}
return invokeMethod;
}

代码的// 重载注释是源码里就有的,这引起了我的注意,而且这段代码的逻辑本身看起来就比较奇怪,我们再分析一下有重载方法的情况

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
private static Method findMethod(Exporter<?> exporter, String method, List<Object> args) {
Invoker<?> invoker = exporter.getInvoker();
Method[] methods = invoker.getInterface().getMethods();
Method invokeMethod = null;
// 假设有方法xxxMethod(A a)和重载的xxxMethod(B b),我们要调用的目标方法是xxxMethod(B b)
for (Method m : methods) {
// 1. 先假设遍历到xxxMethod(A a)
// 2. 再遍历到xxxMethod(B b)
if (m.getName().equals(method) && m.getParameterTypes().length == args.size()) {
// 1. 此时invokeMethod必定为定义时的null
// 2. 此时invokeMethod为xxxMethod(A a)
if (invokeMethod != null) { // 重载
/**
* 2. 用xxxMethod(A a)的参数类型和我们的入参去做match,自然返回false,
* invokeMethod之后就不再重新赋值了,最终返回的是xxxMethod(A a),
* 那么之后的反射调用肯定会有问题。
* 但是如果的目标方法就是xxxMethod(A a),按这个逻辑走下来就会是正确的
*/

if (isMatch(invokeMethod.getParameterTypes(), args)) {
invokeMethod = m;
break;
}
} else {
// 1. 这里将invokeMethod赋值为xxxMethod(A a)
invokeMethod = m;
}
invoker = exporter.getInvoker();
}
}
return invokeMethod;
}

因此这段代码为了处理重载的情况,实际引入了两个问题,一个是对于参数的解析,一个是重载方法的匹配,增加了代码的复杂度且代码的逻辑非常奇怪,却没有解决问题,反而引入了两个BUG
我们再来看2.5.6版本的findMethod

1
2
3
4
5
6
7
8
9
10
private static Method findMethod(Exporter<?> exporter, String method, List<Object> args) {
Invoker<?> invoker = exporter.getInvoker();
Method[] methods = invoker.getInterface().getMethods();
for (Method m : methods) {
if (m.getName().equals(method) && isMatch(m.getParameterTypes(), args)) {
return m;
}
}
return null;
}

非常简单有效,逻辑清晰。
到这里,文章开始的问题已经得到了答案。总结一下:

  1. 2.5.3版本实际上也需要class字段,即参数类的全类名,但实际上由于Dubbo源码的原因没有用到,导致不在JSON对象中加class字段也恰巧不会有问题。
  2. 2.5.6版本解决了上述问题且同时解决了重载方法的匹配问题。

我在GitHub上查看了dubbogit提交记录,这两个问题实际上在2016年就已经解决了,这里是当时的commit
所以使用开源软件还是要及时更新版本,以免掉到坑里。
文章开头调用的正确姿势应该是这样:

1
invoke me.lijf.xxxProvider.xxxMethod({"foo":"bar", "class":"me.lijf.xxxParam"})