文章

Dubbo源码(1):统一配置总线——URL

Dubbo源码(1):统一配置总线——URL

概述

RPC+服务治理是一个重要且热门的课题,在这个领域下的新事物层出不穷。但是在国内的Java生态,Dubbo无疑是最广泛选型的产品。

不管是一线大厂还是名不见经传的小厂,都不约而同的选用Dubbo。这中间当然有【历史惯性】的因素,但是更重要的一定还是因为Dubbo框架丰富的功能和极优秀的代码架构。

如果你去深度的追寻Dubbo的起源,你一定会感受到的一点:Dubbo不是一项KPI工程。我们熟知的Dubbo2.x是由一群热爱代码的专家为了解决1.x的问题来自行孵化、设计的,不是交作业、不是蹭热点。

img

Dubbo作者亲述:那些辉煌、沉寂与重生的故事-阿里云开发者社区

Dubbo的故事-江湖-极客时间

梁飞 是一个热爱代码、热爱思考、热爱分享的技术人,这一点从他早年的博客可以看出来:[梁飞iteye]

基于Dubbo1.x收集到的痛点,结合他对代码、架构的思考,再从0到1系统性的设计、主导的Dubbo2.x完全值得我们细致的去分析、研究源码和背后的出发点。

今天我们将从org.apache.dubbo.common.URL切入,去看看这个遍布Dubbo代码各个层次的类的作用和其中的思考。

URL的引入

软件分层

可维护的代码架构一定是层次清晰、条理分明的。

这个层次不是业务逻辑的步骤拆分,而是代码聚类,分层编排。或者就是我们耳熟能详的【高内聚、低耦合】。

【有向无环图(DAG)】这个概念每个技术人都不停的在各个场景看到听到,无论是宏观的系统架构如SOA、DDD系统架构、还是具体到各类依赖管理工具比如Maven/npm/go mod。

继续的深入到代码架构中,我们日常接触到的任务易读的、易拓展的、易复用的代码,其代码实体(类/接口)的拆分、编排往往也是匹配【DAG】。

image-20260111165631310

上图是一个简单的模块分层聚类的示意,也是一个典型的【有向无环图】,其中灰线是代码边界:

  1. 实灰线: 实灰线是不可穿越的代码边界,一般发生在同层次的兄弟模块间。比如biz1biz2之间不可以发生任何代码耦合。
  2. 虚灰线: 虚灰线是【单通】的代码边界,一般发生在上下层次的父子模块间。比如biz1可以耦合、访问 domain1中的代码实体(类、方法、枚举),但是反之不行。

这样做的好处是明显的:

  1. 同层兄弟模块各自聚焦各自领域,保证独立设计和演进,避免横向的代码连接导致领域失焦和代码复杂度爆炸,让代码【易读易维护】。
  2. 下层代码不对上层业务做任何预设和依赖,只聚焦自身业务领域,为上层提供稳定的、原子的代码能力(interface/method)。不预设 因此 不挑剔,可以被不同场景的上层业务来编排,让代码【可复用】。
  3. 下层提供能力,上层负责编排。

代码冗余

当我们严格按上述DAG架构和代码可见性来落地我们的代码,会产生大量的冗余代码,这些冗余的代码一般产生在【模块边界】上。

考虑如下示例:

image-20260111165812755

foo 编排 bar + baz,需要构建bar / baz 的输入输出。

img

img

img

简单的做法就是直接在foo模块中透出BarIn/BarOutBazIn/BazOut,或者将这些提取到公共的infra中。

这会导致bar/baz模块的改动,会对foo的API产生影响,进而影响到application,这个印象不可控,破坏了foo的独立性。

为了避免这个问题,保证自身API的稳定性,foo模块需要冗余声明下层组件相关的输入输出,使用时在内部进行转换、桥接,将自己的API界面和底层组件解耦。

img

Dubbo URL

Dubbo的代码架构是边界分明的,可以贴一下其经典的代码架构图:

img

如果不做特别的设计,其分层界面上会产生大量的冗余的用于上下层级之间传递输入输出的代码。为了解决这个问题,Dubbo设计了一个宽松的、用于存放和传递元数据的【容器类】——URL。

为什么说它宽松呢?因为它本质上就是一个Map。

img

这个URL类,作为Dubbo infra层的核心类,就像Java标准库中的String类一样,遍布整个Dubbo代码仓库,主要的使用场景就是跨各个模块、层次的API界面,传递【配置和元数据】。

URL的特性

宽松+语义化

Dubbo是一个RPC框架,其复杂的代码层次决定了它必须要使用一个宽松的【容器】用来承载各层次代码的需求。

但同时它又是一个网络框架,其整个系统的主轴就是【网络通讯】,而URL这个语言无关的概念本身就是为了网络通讯而产生的。

因此我们可以看到,URL类中除了一个宽松的Map,还有大量语义化的字段,和URL的基础概念匹配。

img

URL所有的API都是在操作上述几个部分。

Copy On Write

URL被设计为【不可变】对象。类似java.math.BigDecimal

1
2
3
4
5
6
7
8
9
10
11
12
13
import java.math.BigDecimal;

public class Main {
    public static void main(String[] args) {
        BigDecimal balance = new BigDecimal("1000.00");
        
        // 尝试增加 100
        balance.add(new BigDecimal("100.00"));

        // 猜猜看,这里会输出什么?
        System.out.println("余额是: " + balance); 
    }
}

这个特性对【并发】更友好。

1
2
3
4
5
6
7
public static void main(String[] args) throws InterruptedException {
    URL url = URL.valueOf("dubbo://127.0.0.1:20880/dubbo-provider1.xml");
    new Thread(() -> {
        URL url2 = url.addParameter("kye", "value");
    });
    URL url1 = url.setPath("demo");
}

image-20260111170013963

一共有3个URL对象,彼此之间不会互相干扰,不会因为并发修改导致各类奇怪的问题。

序列化/反序列化

Dubbo各个环节都需要使用URL,URL的来源和去向也不尽相同。

image-20260111170055398

其序列化按围绕标准的URL格式来实现。

img

序列化

按URL格式构造字符串:

img

反序列化

按URL格式解析、提取字符串:

img

URL的缺点

【宽松】是URL最大的缺点。

在复杂的代码流程中,使用Map<String,String> 来传递参数,常常不知道一个参数不知道是哪里塞进来的,或者不知道哪里有用到。比如,这种直接使用魔法字符串作为key操作这个Map。

img

Dubbo定义了一堆字符串常量,希望解决这个问题,但是这种方式太看自律了。

img

gRPC也处理这个问题的方式值得借鉴,gRPC使用【带类型的常量】来作为Map的key。

img

img

总结

  1. URL的核心价值 :作为 Dubbo 的 统一配置总线 ,URL 通过 宽松的 Map 结构 承载跨模块的配置与元数据,有效解决了分层架构中模块边界的代码冗余问题,成为连接各层次(如业务层、领域层、基础设施层)的核心媒介 。其设计既满足了 RPC 框架对 网络通讯语义 的需求(如协议、地址、端口等标准化字段),又通过 不可变特性(Copy On Write) 提升了并发场景下的安全性 。
  2. 设计权衡与局限
    1. 优势 :URL 的 序列化/反序列化机制 基于标准 URL 格式实现,确保了跨环节(如服务注册、调用、配置传递)的兼容性 。
    2. 不足宽松的 Map 结构 导致参数管理依赖字符串常量,易出现“魔法字符串”问题,维护成本较高。相比之下,gRPC 采用 带类型的常量作为 Map key 的方案更优 。
  3. 架构启示 :Dubbo URL 的设计体现了对 “高内聚、低耦合” 原则的实践,通过统一的数据载体减少模块间直接依赖,为复杂分层架构提供了灵活的元数据传递方案 。这一思路对中间件开发中 跨模块通信配置管理 具有重要参考价值。
本文由作者按照 CC BY 4.0 进行授权