.NET 也能直接调用 Dubbo:开源客户端 DubboNet,几行 C# 接入 Java 服务

后端业务服务使用 Java + Dubbo,管理后台、任务程序或测试工具使用 .NET,这样的技术组合并不少见。两边需要协作时,我们希望 C# 应用能够直接调用已有 Dubbo 接口,而不是为每个接口再维护一层 HTTP 适配代码。

DubboNet 就是为这个场景提供的 .NET Dubbo 客户端库。 它可以发现服务、读取方法签名、选择 Provider,再通过 Dubbo TCP/Hessian2 或已支持的 Triple HTTP/JSON 方式完成调用,.NET 调用方不需要引用 Java 接口 JAR。

先看怎么用,再展开介绍它能做什么。

本文示例基于 DubboNet 3.6.2。当前包目标框架为 net6.0,可被 .NET 6 及以上应用引用。

一、最简单的使用方式

1. 安装 NuGet 包

在已有 .NET 项目目录中执行:

bash 复制代码
dotnet add package DubboNet --version 3.6.2

2. 几行 C# 调用一个 Dubbo 方法

假设 Java 端已经发布接口 com.example.api.HelloService,其中有一个方法:

java 复制代码
String sayHello(String name);

服务注册在 ZooKeeper,默认根目录为 /dubbo。C# 调用代码如下:

csharp 复制代码
using DubboNet.Clients;

using var client = new DubboClient("127.0.0.1:2181");

var result = await client.QueryAsync(
    "com.example.api.HelloService#sayHello",
    "DubboNet");

if (!result.QuerySuccess)
{
    Console.WriteLine(result.ErrorMeaasge);
    return;
}

Console.WriteLine(result.Result);

这里的入口由"完整 Java 接口名 + # + 方法名"组成,后面直接传业务参数。DubboNet 会尝试从方法元数据中选择对应签名,不需要每次调用都手写参数类型。

带参数调用的自动类型推断还需要可用的方法元数据(这个dubbo服务默认都是带的);如果没有元数据,后面介绍的"显式 Java 类型"方式同样可以调用。

文中的接口、方法、地址和 DTO 都是示例,需要替换成你自己的服务。

3. 无参数、多参数也使用同一套 API

csharp 复制代码
// 无参数方法。
var health = await client.QueryAsync(
    "com.example.api.HealthService#check");

// 多参数方法,参数按 Java 方法的声明顺序传入。
var customer = await client.QueryAsync(
    "com.example.api.CustomerService#getCustomer",
    "customer-001",
    1234L);

公共调用入口统一为 QueryAsync。单个参数本身是数组或集合时,也会作为一个参数处理,不会误拆成多个 Java 方法参数。

实际应用中请复用长生命周期的 DubboClient,不要每个请求都创建和释放一次。 上面的 using 用来展示完整生命周期;在 Web 应用或后台服务中,可以由 DI 容器或宿主统一管理,关闭应用时再释放。

二、DubboNet 的能力和优势

能力 对 .NET 调用方的价值
不依赖 Java 接口 JAR 用接口名、方法名和参数完成调用,适合跨语言接入与通用工具
ZooKeeper / Nacos 服务发现 不必在业务代码中维护每个 Provider 的 IP,并能订阅节点变化
原生 Dubbo TCP + Hessian2 通过 $invoke 泛化调用已有 Dubbo 服务,不依赖 Telnet 执行业务请求
Triple Java Interface Unary HTTP/JSON 通过统一客户端 API 调用已支持的 Dubbo 3 Triple 接口
元数据读取与类型推断 获取方法签名、识别重载,减少手工填写类型的工作
显式签名与强类型响应 兼顾调用确定性与 C# 业务代码的易用性
六种负载均衡模式 根据流量特点选择随机、轮询、最少活跃、最短响应等策略
连接复用与缓存 复用注册中心资源、Provider 连接和方法元数据,减少重复初始化
可选消费者注册 让注册中心看到应用引用的接口,并支持运行中开关
MIT 开源 可以查看实现、提交问题,也可以按许可证约定用于自己的项目

原生执行器支持在单个 TCP 连接上使用 Request ID 复用并发请求,不需要为每个请求新建一条连接。LeastActive、ShortestResponse 等策略使用消费者本地统计,也不要求开放 Provider 的 Telnet 诊断端口(Telnet的操作方式在1.3.0以后的版本逐步被弃用)。

尽可能的资源复用使得DubboNet有非常强的性能表现,即便在性能测试过程中同时启动上千个异步请求也只需要一个client。

三、让调用更明确:Java 类型、强类型响应与复杂参数

1. 有重载或 null 参数时,显式指定 Java 类型

自动推断适合简单、签名明确的接口。但目标Dubbo服务可能没有开启元数据中心,或因歧义及重载导致参数推断失败。

遇到这些情况,可以直接传入准确的 Java 签名:

csharp 复制代码
var result = await client.QueryAsync(
    "com.example.api.CustomerService#getCustomer",
    new[] { "java.lang.String", "java.lang.Long" },
    new object[] { "customer-001", 1234L });

类型数组和参数数组一一对应。常见类型可以写成 int、long、java.lang.Integer、java.lang.String、java.util.List、java.util.Map,以及 Java DTO 的全限定名。

显式签名不依赖元数据获取成功。对于关键接口、复杂重载或元数据不完整的环境,这是更确定的接入方式。

2. 直接读取 C# 响应模型

不想在业务代码中手动解析 JSON,可以指定响应类型:

csharp 复制代码
var result = await client.QueryAsync<CustomerDto>(
    "com.example.api.CustomerService#getCustomer",
    new[] { "java.lang.Long" },
    new object[] { 1234L });

if (result.QuerySuccess && result.ResultModle is CustomerDto customer)
{
    Console.WriteLine($"{customer.Id}: {customer.Name}");
}
else
{
    Console.WriteLine(result.ErrorMeaasge);
}

对应模型可以定义为:

csharp 复制代码
using System.Text.Json.Serialization;

public sealed class CustomerDto
{
    [JsonPropertyName("id")]
    public long Id { get; set; }

    [JsonPropertyName("name")]
    public string? Name { get; set; }
}

ResultModle 通过 System.Text.Json 反序列化,原始 JSON 仍保留在 Result 中。属性名称或大小写不一致时,可以像上面一样使用 JsonPropertyName 映射。

ResultModle 和 ErrorMeaasge 是当前版本的实际 API 拼写。模型反序列化错误记录在 ErrorMeaasge 中;QuerySuccess 本身也不代表业务响应里的 code 或 success 一定成功。

3. POJO、版本、分组和 attachments

需要传复杂对象,或者指定服务版本、分组与附加信息时,可以使用 DubboInvocation:

csharp 复制代码
using DubboNet.DubboService.DataModle;

var invocation = new DubboInvocation(
    "com.example.api.CustomerService",
    "save",
    new[] { "com.example.api.dto.CustomerRequest" },
    new object[]
    {
        new
        {
            name = "张三",
            age = 28
        }
    })
{
    Version = "1.0.0",
    Group = "prod",
    Attachments = new Dictionary<string, string>
    {
        ["traceId"] = Guid.NewGuid().ToString("N")
    }
};

var result = await client.QueryAsync(invocation);

原生 Dubbo 泛化调用会根据 POJO 参数类型补充需要的 class 信息。对象字段应与 Java DTO 对应,复杂嵌套结构应先在真实 Provider 上验证。

Version、Group 表示服务版本与服务分组,不是 Dubbo 框架版本,也不是 Nacos 的 Naming 分组。traceId 这类 attachment 是否参与链路追踪,需要服务端配合读取和处理。

四、从 ZooKeeper 切换到 Nacos

使用 Nacos 时,以 nacos:// 开头指定注册中心即可:

csharp 复制代码
using var nacosClient = new DubboClient(
    "nacos://127.0.0.1:8848",
    new DubboClient.DubboClientConf
    {
        // Nacos Naming 分组,与 Provider 的注册配置保持一致。
        DubboRootPath = "DEFAULT_GROUP",

        // Nacos Config 中存放 Dubbo 元数据的分组,独立配置。
        MetadataRootPath = "dubbo",

        DubboRequestTimeout = 5_000
    });

var result = await nacosClient.QueryAsync(
    "com.example.api.HelloService#sayHello",
    "hello-nacos");

Nacos 还可以通过连接字符串查询参数指定 namespace ID、认证等信息。实际项目中请从配置或密钥管理服务读取凭据,不要硬编码进源码。

两种注册中心使用相同的 QueryAsync 调用方式;DubboClient 会根据 Provider URL 选择执行器:

  • dubbo://:经典 Dubbo2 TCP 帧 + Hessian2 泛化调用。
  • tri://:当前支持的 Triple Java Interface 单次请求/响应 HTTP/JSON 调用。

需要注意,支持 Dubbo 3 Provider 不等于支持 Dubbo 3 的全部能力。目前服务发现仍面向接口级注册,Triple 也有明确边界,文末会单独说明。

五、按业务选择负载均衡

创建客户端时可以指定策略:

csharp 复制代码
using var balancedClient = new DubboClient(
    "127.0.0.1:2181",
    new DubboClient.DubboClientConf
    {
        NowLoadBalanceMode = DubboClient.LoadBalanceMode.LeastActive,
        DubboRequestTimeout = 5_000
    });

当前支持以下六种模式:

模式 主要行为
Random 默认策略,按 Provider 权重随机选择
RoundRobin 平滑加权轮询
LeastActive 优先选择本客户端观察到的在途请求数最少的节点,同分时加权随机
P2CLoadBalance 按权重抽取两个不同节点,再比较本地在途请求数
ShortestResponse 综合最近 30 秒的成功响应平均耗时、在途请求数及失败惩罚选址
ConsistentHash 相同参数 key 尽量命中同一节点,适合请求亲和场景

这里的活跃数和耗时是当前消费者本地的观测值,不是 Provider 的全局负载。最短响应模式也不是简单地选择"最后一次请求最快"的节点。

一致性 Hash 尤其容易在压测时被误解:当前 key 是完整参数数组序列化后的 JSON。所有请求都传 [0,0] 时,落到同一个节点是预期行为;如果要测试集群总体吞吐,应选择能分散重复请求的策略,或者准备具有合理分布的业务 key。

当前一致性 Hash 使用 UTF-8、稳定 MurmurHash2 和每节点 160 个虚拟节点,不保证与 Java Dubbo 的 Hash 实现得到相同映射,也不按正权重比例分配虚拟节点。

六、不只是调用:也可以读取接口和类型元数据

如果你正在开发 Dubbo 调试工具、接口浏览器或动态参数编辑器,往往还需要知道"接口有哪些方法、参数是什么类型"。

DubboNet 提供了对应 API:

csharp 复制代码
// 查看一个方法的全部重载。
var overloads = await client.GetMethodMetadataAsync(
    "com.example.api.CustomerService",
    "getCustomer");

foreach (var method in overloads)
{
    Console.WriteLine(
        $"{method.ReturnType} {method.Name}" +
        $"({string.Join(", ", method.ParameterTypes)})");
}

// 查看服务的全部方法签名。
var methods = await client.GetServiceMethodsMetadataAsync(
    "com.example.api.CustomerService");

// 从元数据中心读取 POJO、集合、枚举等类型定义。
var types = await client.GetServiceTypesMetadataAsync(
    "com.example.api.CustomerService");

方法签名优先按 Provider 版本选择元数据来源,必要时使用一次性 Telnet ls -l 回退;元数据成功读取后会缓存,并监听相关变化。

GetServiceTypesMetadataAsync 则依赖元数据中心的完整类型定义,不会退回 Telnet,因为 ls -l 没有完整 POJO 结构。它提供的是 Provider 已上报的信息,不会凭空补全缺失的业务定义。

这使 DubboNet 不仅可以嵌入业务应用,也适合作为 .NET Dubbo 测试和管理工具的调用基础。

七、按需开启消费者注册,并支持热切换

如果希望注册中心及兼容管理平台能够看到"哪个应用引用了哪些接口",可以开启消费者注册。默认情况下它是关闭的。

csharp 复制代码
using DubboNet.Clients.DataModle;

DubboClient.ConfigureConsumerRegistration(new ConsumerRegistrationOptions
{
    Enabled = true,
    ApplicationName = "customer-admin-dotnet"
});

// 后续 QueryAsync 使用到的接口会在后台登记消费者记录。

// 运行期间关闭,不需要重建 DubboClient。
DubboClient.ConfigureConsumerRegistration(new ConsumerRegistrationOptions
{
    Enabled = false
});

// 再次开启,继承之前的应用身份,并补登记仍存活客户端已引用的接口。
DubboClient.ConfigureConsumerRegistration(new ConsumerRegistrationOptions
{
    Enabled = true
});

上面演示的是三次独立配置操作,实际使用时在对应业务时机执行,不需要启动后立即连续切换。

这个配置是进程级的,不是某个 client 独享的开关。配置更新后,后台按照最新期望状态执行注册或注销,不会等待网络操作完成才返回,也不会因为注册失败而阻断 RPC。

ZooKeeper 使用临时节点和现有会话机制;Nacos 使用临时实例及 SDK 的连接维护机制。同时提供失败日志、退避重试和会话恢复处理。应用仍应正常释放 client,不能把心跳当作"自动识别业务是否还在使用"的替代品。

八、调试时也可以绕过注册中心直连

已知 Provider 的 IP、端口及 Java 方法签名时,可以直接使用原生执行器:

csharp 复制代码
using DubboNet.DubboService;
using DubboNet.DubboService.DataModle;

using var suite = new NativeDubboActuatorSuite(
    "127.0.0.1",
    20880,
    new DubboActuatorSuiteConf
    {
        DubboRequestTimeout = 5_000
    });

var invocation = new DubboInvocation(
    "com.example.api.HelloService",
    "sayHello",
    new[] { "java.lang.String" },
    new object[] { "hello-direct" });

var result = await suite.SendQuery(invocation);
Console.WriteLine(result.Result);

直连适合联调和定点排障,但不会提供服务发现、负载均衡或方法签名推断。已有 Telnet 开放条件的环境,还可以使用相关诊断 API 读取 ls、status 等信息。

九、可能需要注意的点

  • 已验证的组合 :原生 Dubbo 路径包括 Dubbo 2.7.3,以及 Dubbo 3.2.20 / 3.3.6 的经典 dubbo:// 兼容模式;Triple 路径已验证 Dubbo 3.3.6 Java Interface Unary HTTP/JSON。其他版本需结合项目兼容说明实测。
  • 运行平台:当前包目标为 .NET 6,未提供 .NET Standard 或 .NET Framework 目标。
  • 服务发现:支持 ZooKeeper / Nacos 接口级注册,尚未实现 Dubbo 3 应用级服务发现。
  • 原生序列化:目前使用 Hessian2,不会根据 Provider 配置自动切换到其他序列化器。
  • Triple 范围:尚未实现 Protobuf/IDL、流式调用和完整 gRPC framing,不能将当前实现视作通用 gRPC 客户端。
  • Nacos 多环境工具场景:受当前底层 SDK 生命周期限制,同一进程最多保留 32 种不同连接配置,详情见github README。

接入后也要分别检查"调用层成功"和"业务成功":先看 QuerySuccess,再看响应中的业务状态。不要在非幂等写操作外层直接套无限重试。

十、适合哪些项目?

如果你有以下需求,可以考虑试用 DubboNet:

  • 用 .NET 管理后台调用已有 Java Dubbo 业务服务。
  • 在 C# 后台任务、集成程序中复用现有 Dubbo 接口。
  • 开发 Dubbo 接口调试、元数据浏览或压测工具。
  • 希望直接接入注册中心,而不是在调用方维护 Provider 地址列表。

它把服务发现、签名解析、连接复用、负载均衡和调用结果处理封装进一个 .NET 客户端,让跨语言调用不必从底层协议开始写起。

先从一个简单的查询接口开始,确认注册中心、协议和类型签名,再逐步接入真实业务。 欢迎试用、反馈兼容场景,也欢迎通过 Issue 和 PR 一起完善。

<>()