始于 Dubbo 2.0.6 的 Telnet Command 是一个令人兴奋的特性,极大地降低了服务化测试的成本,但是,寥寥数行的可怜文档无形地为使用增加了成本。此前虽然一直在使用 Telnet Command,但基本上是浅尝辄止,字符集的问题、重载方法的错误筛选等,都让我不得不对这个特性敬而远之,无法作为高频的生产力工具。最近,频繁出现的调试需求让我不得不尝试接受并熟悉 Dubbo Telnet Command。

本文只针对 invoke 命令,基于 Dubbo 版本 2.6.7

Dubbo Telnet Command invoke 命令的一般格式为 invoke <全限定名>.<方法名>(<参数>,...,<参数>)。其中参数需要能被 JSON 解析,即提取命令中的 <参数>,...,<参数> 部分,并包装上 [] 构成 [<参数>,...,<参数>] ,需要保证这个串是一个合法的 JSON Array。

本文提供的示例均可在 dubbo-telnet-playground 中找到。

一、基本类型

八大基本类型,映射到 JSON 对应为 number, "true"/"false"

telnet 127.0.0.1 20880
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.

# bool 类型
dubbo>invoke com.ffutop.playground.api.PrimitiveService.boolMethod(false)
"com.ffutop.playground.service.PrimitiveServiceImpl#boolMethod(false)"
elapsed: 2 ms.

# int 类型
dubbo>invoke com.ffutop.playground.api.PrimitiveService.intMethod(123456)
"com.ffutop.playground.service.PrimitiveServiceImpl#intMethod(123456)"
elapsed: 0 ms.

# byte 类型
dubbo>invoke com.ffutop.playground.api.PrimitiveService.byteMethod(1)
"com.ffutop.playground.service.PrimitiveServiceImpl#byteMethod(1)"
elapsed: 0 ms.

# short 类型
dubbo>invoke com.ffutop.playground.api.PrimitiveService.shortMethod(10000)
"com.ffutop.playground.service.PrimitiveServiceImpl#shortMethod(10000)"
elapsed: 0 ms.

# long 类型
dubbo>invoke com.ffutop.playground.api.PrimitiveService.longMethod(1234567892346754)
"com.ffutop.playground.service.PrimitiveServiceImpl#longMethod(1234567892346754)"
elapsed: 0 ms.

# double 类型
dubbo>invoke com.ffutop.playground.api.PrimitiveService.doubleMethod(1.0022)
"com.ffutop.playground.service.PrimitiveServiceImpl#doubleMethod(1.0022)"
elapsed: 0 ms.

# char 类型
dubbo>invoke com.ffutop.playground.api.PrimitiveService.charMethod('a')
"com.ffutop.playground.service.PrimitiveServiceImpl#charMethod(a)"
elapsed: 0 ms.

# float 类型
dubbo>invoke floatMethod(1.234)
"com.ffutop.playground.service.PrimitiveServiceImpl#floatMethod(1.234)"
elapsed: 0 ms.

二、引用类型

引用类型参数一般使用一个 JSON object 来描述。object 中一般使用 "class":"xxx" 强制声明 object 对应的 Java 对象类型,便于参数筛选和对象实例化。

其中特殊的一个是 java.lang.String ,可以由 JSON string 直接描述,当然也可以选择用 object 来进行描述。

public class PlayModel {
    int intParam;
    double doubleParam;
    String stringParam;
    // 省略 get/set 方法
}
# 罗列 com.ffutop.playground.api.ReferenceService 所有的方法
dubbo>ls -l com.ffutop.playground.api.ReferenceService
java.lang.String refMethod(com.ffutop.playground.model.PlayModel)
java.lang.String stringMethod(java.lang.String)

# 自定义 OBJECT 引用类型
dubbo>invoke com.ffutop.playground.api.ReferenceService.refMethod({"class":"com.ffutop.playground.model.PlayModel","intParam":1,"doubleParam":2.233,"stringParam":"HelloWorld"})
"com.ffutop.playground.service.ReferenceServiceImpl#refMethod({\"doubleParam\":2.233,\"intParam\":1,\"stringParam\":\"HelloWorld\"})"
elapsed: 14 ms.

# String 引用类型 - 第一类调用方案
dubbo>invoke com.ffutop.playground.api.ReferenceService.stringMethod("Hello World")
"com.ffutop.playground.service.ReferenceServiceImpl#stringMethod(\"Hello World\")"
elapsed: 4 ms.

# String 引用类型 - 第二类调用方案
dubbo>invoke com.ffutop.playground.api.ReferenceService.stringMethod({"class":"java.lang.String","value":"Hello World"})
"com.ffutop.playground.service.ReferenceServiceImpl#stringMethod(\"Hello World\")"
elapsed: 0 ms.

三、集合类型

常规的集合类型有 列表(List)集(Set)映射(Map),这里把 数组(Arrays) 也算在其中。

Arrays, List, Map 可以分别有 JSON 的 array, array, object 来描述。

只有 Set 比较特殊,JSON 中没有直接描述 Set 的结构,只能借用 JSON object 来描述一个 HashSet 对象。{"class":"java.util.HashSet","map":{"Hello":null,"World":null}}class 标识 object 需要实例化的 Java 对象类,mapHashSet 中的字段,将通过反射机制塞入需要最终实例化得到的 HashSet 对象。

public class HashSet<E>
    extends AbstractSet<E>
    implements Set<E>, Cloneable, java.io.Serializable
{
    static final long serialVersionUID = -5024744406713321676L;

    private transient HashMap<E,Object> map;

    // More Code Omitted
}
dubbo>ls -l com.ffutop.playground.api.CollectionService
java.lang.String setMethod(java.util.Set)
java.lang.String arrayMethod(java.lang.Object[])
java.lang.String listMethod(java.util.List)
java.lang.String mapMethod(java.util.Map)

# 数组(Arrays)类型
dubbo>invoke arrayMethod([1293,234.23,"@#(@#JNE",{"class":"com.ffutop.playground.model.PlayModel","intParam":123,"stringParam":"HelloWorld"}])
"com.ffutop.playground.service.OverloadServiceImpl#arrayMethod([1293,234.23,\"@#(@#JNE\",{\"doubleParam\":0.0,\"intParam\":123,\"stringParam\":\"HelloWorld\"}])"
elapsed: 5 ms.

# 列表(List)类型
dubbo>invoke listMethod([1293,234.23,"@#(@#JNE",{"class":"com.ffutop.playground.model.PlayModel","intParam":123,"stringParam":"HelloWorld"}])
"com.ffutop.playground.service.CollectionServiceImpl#listMethod([1293,234.23,\"@#(@#JNE\",{\"doubleParam\":0.0,\"intParam\":123,\"stringParam\":\"HelloWorld\"}])"
elapsed: 0 ms.

# 集(Set)类型
dubbo>invoke setMethod({"class":"java.util.HashSet","map":{"Hello":null,"World":null}})
"com.ffutop.playground.service.CollectionServiceImpl#setMethod([\"Hello\",\"World\"])"
elapsed: 0 ms.

# 映射(Map)类型
dubbo>invoke mapMethod({"class":"java.util.HashMap","Hello":123,"World":456})
"com.ffutop.playground.service.CollectionServiceImpl#mapMethod({\"Hello\":123,\"World\":456})"
elapsed: 0 ms.

四、多参数方法

介绍过多种典型的参数类型之后,接着需要了解多参数方法的调用方式。其实也比较简单,不过是按照 Dubbo 接口方法中参数的声明顺序,将每个单参数 JSON 串整合,并通过 , 分割即可。

dubbo>ls -l com.ffutop.playground.api.MultiParamsService
java.lang.String multiParamsMethod2(java.util.Map,java.util.List)
java.lang.String multiParamsMethod3(int,int,int,java.lang.String)
java.lang.String multiParamsMethod1(int,com.ffutop.playground.model.PlayModel)

dubbo>invoke multiParamsMethod1(1781687,{"class":"com.ffutop.playground.model.PlayModel","intParam":1781688,"doubleParam":178.1688,"stringParam":"Hello World"})
"com.ffutop.playground.service.MultiParamsServiceImpl#multiParamsMethod1(1781687,{\"doubleParam\":178.1688,\"intParam\":1781688,\"stringParam\":\"Hello World\"})"
elapsed: 11 ms.

dubbo>invoke multiParamsMethod2({"p1":"HELLO","p2":178.1688},["hello","world","1314"])
"com.ffutop.playground.service.MultiParamsServiceImpl#multiParamsMethod2({\"p1\":\"HELLO\",\"p2\":178.1688},[\"hello\",\"world\",\"1314\"])"
elapsed: 2 ms.

dubbo>invoke multiParamsMethod3(178,1687,1688,"Hello World")
"com.ffutop.playground.service.MultiParamsServiceImpl#multiParamsMethod3(178,1687,1688,Hello World)"
elapsed: 1 ms.

五、重载方法

虽然 Dubbo Telnet Command 也支持重载方法,但存在 BUG ,对于一些特定的重载方法,将发生 No Service 或者调用错误的方法等问题。

Dubbo Telnet Command - Invoke 的核心逻辑包含两个部分,其一是基于用户输入,筛选合适的 Dubbo provider 及方法;其二是对用户输入进行反序列化,构造 Java 对象并最终触发执行。

对于几种 Java 基本类型以及 String,由于 JSON 提供的描述信息及差异不足,对于此类重载,将在筛选方法时,产生错误选择,并最终导致反序列化失败。当然,另一方面这也可以被认定为 Dubbo Telnet 的 BUG。因此,当重载方法入参数量相同,且都是基本类型/String,则将无法调用成功。

dubbo>ls -l com.ffutop.playground.api.OverloadService
java.lang.String method(com.ffutop.playground.model.PlayModel)
java.lang.String method(com.ffutop.playground.model.AnotherPlayModel)
java.lang.String method(java.lang.String,com.ffutop.playground.model.PlayModel)
java.lang.String method(com.ffutop.playground.model.PlayModel,com.ffutop.playground.model.AnotherPlayModel)
java.lang.String method(java.lang.String)
java.lang.String method(java.lang.Integer)
java.lang.String method(long)

invoke method(178)
Failed to invoke method method, cause: java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
	at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java)
    # something omitted

dubbo>invoke method("1244")
"com.ffutop.playground.service.OverloadServiceImpl#method(String 1244)"
elapsed: 1 ms.

dubbo>invoke method({"class":"com.ffutop.playground.model.PlayModel","intParam":178,"doubleParam":0.1688,"stringParam":"HELLO WORLD"})
"com.ffutop.playground.service.OverloadServiceImpl#method(PlayModel {\"doubleParam\":0.1688,\"intParam\":178,\"stringParam\":\"HELLO WORLD\"})"
elapsed: 10 ms.

dubbo>invoke method({"class":"com.ffutop.playground.model.AnotherPlayModel","longParam":1781688,"floatParam":167.1688,"playParam":{"class":"com.ffutop.playground.model.PlayModel","intParam":178,"doubleParam":0.1688,"stringParam":"HELLO WORLD"}})
"com.ffutop.playground.service.OverloadServiceImpl#method(AnotherPlayModel {\"floatParam\":167.1688,\"longParam\":1781688,\"playParam\":{\"doubleParam\":0.1688,\"intParam\":178,\"stringParam\":\"HELLO WORLD\"}})"
elapsed: 3 ms.

六、字符集问题解决

前述的所有方法面对的输入都是 ASCII 字符,对于几乎所有的字符集都是兼容的。但是,日常需求中汉字形式的入参并不少见,但多数尝试都被解析为乱码。

原因一般都是用户输入汉字所使用的字符集与 Dubbo Telnet Command 默认使用的字符集不相匹配。Dubbo 默认使用了 GBK ,对于习惯性使用 UTF-8 的开发人员来说,特别残酷。

不过这个问题的解决方法也特别简单,对用户输入输出做一个包装,完成字符集转化即可。下面提供一个 Python3 版本的 GBK:UTF-8 自转换的 Telnet 工具。

#!/usr/bin/env python3
"""
Dubbo Tuned Telnet Command

Example:
    ./telent_dubbo.py localhost 20880
"""
import sys
import telnetlib
import platform
import readline

host = sys.argv[1]
port = sys.argv[2]
encoding = "GBK"

tn = telnetlib.Telnet(host, port)
cmd = None

while cmd != "exit":
    cmd = input("dubbo> ").strip()
    tn.write(cmd.encode(encoding) + b"\n")
    if cmd != "exit":
        result = tn.read_until(b"dubbo>").decode(encoding)
        result = result[:result.rfind('\n')]
        print(result)

写在最后

不得不说,除了文档方面的问题,Dubbo Telnet Command 解决了 Dubbo 服务调试的一个重大问题。