调试时,怎样直接调用 Dubbo 服务并查看返回值
排查 Dubbo 时,经常会遇到这种情况:Provider 看起来已经启动,注册中心里也有地址,但我不想先经过一整条业务链路,只想用一个确定的参数调用服务,看看它到底返回什么。
Dubbo 的 20880 端口不是 HTTP 接口,不能把它当成普通 URL 用浏览器或 curl 访问。要拿到返回值,仍然需要一个能说 Dubbo 协议的调用方。本文以 Dubbo 2.6.12 为例,记录三种做法:给已有 Consumer 加一个临时调试入口、用泛化调用绕过 API Jar,以及直接在 Provider 端用 Telnet 调用。
所有示例都应先在本地、测试或预发环境使用。一次调试调用也会执行真实业务方法;查询接口通常没有问题,创建、扣款、删除这类操作要用测试数据,不能因为“只是看返回值”就当成无副作用。
概要
先根据手里的条件选方法:
| 当前条件 | 适合的方式 | 能确认什么 |
|---|---|---|
| 有接口 Jar,想模拟正常 Consumer | 临时 HTTP 调试入口 | Consumer 能否拿到远程结果 |
| 有接口全限定名和 Provider 地址,但没有接口 Jar | 泛化调用 | 指定方法能否执行、返回值是什么 |
| 只想确认某台 Provider 自己能不能执行方法 | Telnet invoke | Provider 端口和业务实现是否正常 |
如果目的是验证注册中心订阅、路由和负载均衡,就让调试入口使用原有的 <dubbo:reference> 或 @Reference 配置。下面的 url 写法会直接连接指定的 Provider,只适合把问题缩小到一台机器时使用。
学习内容
1. 有接口 Jar 时,给 Consumer 加一个固定的调试入口
假设共享 API 中有一个很简单的接口:
package com.example.api;
public interface GreetingService { String sayHello(String name);}在已有的 Consumer Web 应用里引用它。Dubbo 2.6.x 使用的是旧注解包:
import com.alibaba.dubbo.config.annotation.Reference;import com.example.api.GreetingService;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RequestParam;import org.springframework.web.bind.annotation.RestController;
@RestControllerpublic class DubboDebugController {
@Reference( interfaceClass = GreetingService.class, url = "dubbo://127.0.0.1:20880", timeout = 3000, retries = 0 ) private GreetingService greetingService;
@RequestMapping(value = "/internal/debug/dubbo/greeting", method = RequestMethod.GET) public String greeting(@RequestParam("name") String name) { return greetingService.sayHello(name); }}调用这个 HTTP 入口:
curl 'http://127.0.0.1:8080/internal/debug/dubbo/greeting?name=minjer'如果 Provider 的实现返回 "Hello, " + name,终端会直接看到:
Hello, minjer这里 HTTP 只是方便手工触发,真正的远程调用发生在 greetingService.sayHello(name)。@Reference 创建了接口代理;代理把参数编码成 Dubbo 请求,连接 127.0.0.1:20880,再把 Provider 的返回值交给 Controller。
url 指定了 Provider 地址,因此不会从注册中心挑选地址。想验证正常服务发现时,删掉 url,保留项目原本的注册中心和 @Reference 配置即可。无论直接连接还是走注册中心,interfaceClass、group、version 都必须与 Provider 一致。
如果项目使用 XML,同一个引用可以写成:
<dubbo:reference id="greetingService" interface="com.example.api.GreetingService" url="dubbo://127.0.0.1:20880" timeout="3000" retries="0"/>然后把 greetingService 注入 Controller。注解和 XML 只选一种,不要为同一个接口重复创建两份 Reference。
这个 Controller 应当是临时调试工具,而不是通用的“Dubbo 转发器”。接口、方法和参数要写死,限制在内部网络或测试环境,并在排查结束后删除。把服务名、方法名和任意参数都开放给 HTTP 请求,会把远程执行能力暴露出去,风险很高。
2. 没有接口 Jar 时,使用泛化调用
有时只知道服务接口名、方法名和 Provider 地址,拿不到共享 API Jar。Dubbo 的 GenericService 可以在这种情况下调用服务。它的 $invoke 需要三个参数:方法名、参数类型数组和参数值数组。
下面的配置仍然直接连接一台 Provider:
<dubbo:reference id="genericGreetingService" interface="com.example.api.GreetingService" generic="true" url="dubbo://127.0.0.1:20880" timeout="3000" retries="0"/>在一个仅限测试环境的 Controller 中调用:
import com.alibaba.dubbo.rpc.service.GenericService;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.RequestMapping;import org.springframework.web.bind.annotation.RequestMethod;import org.springframework.web.bind.annotation.RestController;
@RestControllerpublic class GenericDubboDebugController {
@Autowired private GenericService genericGreetingService;
@RequestMapping(value = "/internal/debug/dubbo/generic-greeting", method = RequestMethod.GET) public Object invokeGreeting() { return genericGreetingService.$invoke( "sayHello", new String[]{"java.lang.String"}, new Object[]{"minjer"} ); }}访问 /internal/debug/dubbo/generic-greeting,输出同样是:
Hello, minjer"java.lang.String" 不是展示用的文字,而是 Provider 方法参数的完整 Java 类型名。遇到重载方法时,它决定调用哪一个重载。group 和 version 也要按 Provider 的配置补到 <dubbo:reference> 上。
复杂参数用 Map 表示;当 Provider 参数是自定义 DTO 时,class 要写 DTO 的全限定名:
Map<String, Object> request = new HashMap<String, Object>();request.put("class", "com.example.api.UserQuery");request.put("userId", 42L);
Object result = genericGreetingService.$invoke( "queryUser", new String[]{"com.example.api.UserQuery"}, new Object[]{request});System.out.println(result);泛化调用省掉了 Consumer 端的 DTO 类,却没有省掉接口契约。接口全名、方法名、参数类型、字段名写错一个,Provider 就无法找到正确的方法或无法把 Map 还原成参数对象。它适合定位和临时验证,不适合替代正常的 API 依赖。
3. 只检查 Provider 时,直接用 Telnet 调用
Telnet 是一种基于纯文本的远程交互方式。终端中的 Telnet 客户端会建立一个 TCP 连接,再把键盘输入的命令发送给对端;它不是 HTTP,也不理解 Dubbo 协议本身。
Dubbo 2.6.12 在 Dubbo 协议端口注册了 Telnet 命令处理器。因此 Provider 的 20880 端口可达时,可以用 Telnet 连上端口,再执行 Dubbo 提供的 invoke 命令:
Telnet 能调用服务,不是因为它把文本自动转换成了一次普通的 Consumer RPC。Provider 的网络层会先检查输入是否以 Dubbo 协议的魔数开头;普通文本不符合 Dubbo 请求帧,就交给 Telnet 编解码器处理。后面的流程是:
Telnet 输入 invoke 服务名.方法名(参数) → TelnetCodec 解析出一行字符串 → TelnetHandlerAdapter 按命令名找到 invoke 处理器 → InvokeTelnetHandler 查找本机已暴露的 Exporter → 解析参数、定位 Java 方法 → Invoker.invoke(...) 执行业务方法 → 将返回值转成 JSON 文本写回连接也就是说,invoke 是 Dubbo Provider 专门保留的一条调试路径。它不经过注册中心,也没有 Consumer 的 Directory、Router 和 LoadBalance;它是在 Provider 进程里找到已暴露的 Invoker 后直接发起调用。
$ telnet 127.0.0.1 20880
invoke com.example.api.GreetingService.sayHello("minjer")"Hello, minjer"elapsed: 4 ms.invoke 后面是 服务接口全名.方法名(参数)。参数使用 JSON 风格的写法,字符串要带引号。Provider 存在多个同名重载方法时,Dubbo 会要求先选择具体方法;自定义对象也需要按 JSON 对象传入。
Telnet 的内容不会加密,Dubbo 2.6.x 的这组命令也不应暴露到公网。它只适合在受控网络中临时排查;生产环境至少要限制来源地址,排查完成后不要保留不必要的访问入口。
这条命令在 Provider 进程内直接找到已暴露的服务并执行。它能说明 Provider 已监听端口、对应服务已经暴露、业务方法可以返回结果;但它不验证 Consumer 的注册中心订阅、路由规则或负载均衡。需要检查完整调用链时,仍应使用前两种 Consumer 侧方式。
遇到的问题
curl http://host:20880/... 为什么没有结果?
因为默认 Dubbo 协议不是 HTTP。它接收的是 Dubbo 的二进制请求帧,普通 HTTP 请求不能被当作接口调用。要么通过 Consumer 的代理调用,要么使用 Dubbo 自带的 Telnet 命令。
直接写了 url,为什么还要检查 group 和 version?
地址只解决“连到哪台机器”。一台 Provider 可以暴露同一个接口的多个分组或版本,Consumer 仍然需要用接口名、group、version 找到正确的服务键。直接调用失败时,先对照 Provider 的暴露配置,再检查调用方的这三项。
能连上端口,为什么调用仍然报错?
端口可达只说明 TCP 连接建立了。调用还可能因为方法名、参数类型、DTO 字段、业务校验或服务端异常失败。此时把 Consumer 的异常、Provider 的异常日志和实际传入的参数放在一起看,比反复修改超时时间更有效。
思考疑问
“直接调用”并不等于绕过 Dubbo。临时 HTTP 入口和泛化调用仍是一个 Consumer,只是把触发方式变得更容易观察;Telnet 则只验证 Provider 一侧。
调试前先决定要证明哪件事:是 Provider 能返回、某个地址能连通,还是正常 Consumer 能从注册中心找到服务。目标不同,入口也不同。这样拿到的返回值才有解释力。
