12 Dubbo 2.7 服务发布全流程源码解析

Dubbo 2.7 服务发布全流程源码解析

学习目标

完成本章后,你能够:

  • 完整绘制ServiceBean从Spring事件触发到Netty Server启动的全流程图
  • 说明为什么Dubbo需要"双重暴露"(本地+远程)
  • 阐述RegistryProtocol如何将"注册"与"暴露"串联
  • 解释DubboProtocol.export()中创建Netty Server的详细步骤
  • 理解服务暴露各阶段的防御性编程设计

1. ServiceBean部署全流程

1.1 从Spring事件到服务导出的完整链路

scss 复制代码
Spring容器启动生命周期:

┌──────────────────────────────────────────┐
│  ApplicationContext.refresh()            │
│    ↓                                     │
│  finishRefresh()                         │
│    ↓                                     │
│  广播 ContextRefreshedEvent              │
│    ↓                                     │
│  ServiceBean.onApplicationEvent(event)   │  ← 入口
│    ↓                                     │
│  export()                                │
│    ↓                                     │
│  doExport()                              │
│    ↓                                     │
│  doExportUrls()                          │
│    ↓                                     │
│  loadRegistries() → 获取注册中心URL列表    │
│    ↓                                     │
│  遍历每个注册中心URL,逐协议export          │
│    ↓                                     │
│  doExportUrlsFor1Protocol()              │
│    ↓                                     │
│  Protocol.export(invoker)                │  ← 协议层暴露
│    ↓                                     │
│  RegistryProtocol.export(invoker)        │  ← 注册中心包装
│    ↓────┬────────────────────.           │
│          │                               │
│  ① registry.register(url) → ZK写入       │
│  ② protocol.export(invoker) → 底层暴露   │
└──────────────────────────────────────────┘

1.2 doExport源码流程

java 复制代码
/**
 * ServiceConfig.doExport() ------ 服务导出的顶层入口
 * 
 * 关键流程:
 * 1. 导出标志检查(防重入)
 * 2. 延迟导出处理(delay参数)
 * 3. 配置裁剪与校验
 * 4. 调用 doExportUrls()
 */
public class ServiceConfigDoExport {
    
    /**
     * 简化的doExport方法
     */
    protected synchronized void doExport() {
        
        // ===== 1. 导出状态检查 =====
        if (unexported) {
            throw new IllegalStateException(
                "该服务已经被取消导出: " + interfaceClass.getName());
        }
        
        if (exported) {
            return;  // 已导出,防止重复导出
        }
        
        exported = true;  // 设置标志位
        
        // ===== 2. 设置默认路径 =====
        // 如果没有显式指定path,默认使用接口名
        if (StringUtils.isEmpty(path)) {
            path = interfaceName;
        }
        
        // ===== 3. 触发导出 =====
        doExportUrls();
    }
    
    /**
     * doExportUrls ------ 将服务导出到多个协议下的多个注册中心
     * 
     * 每个注册中心 + 每个协议 = 生成一个独立的URL
     */
    private void doExportUrls() {
        
        // ===== 1. 获取所有注册中心URL =====
        // 例如配置了两个注册中心
        // registry://10.0.1.10:2181/...?registry=zookeeper
        // registry://10.0.2.10:2181/...?registry=zookeeper
        List<URL> registryURLs = loadRegistries(true);
        
        // ===== 2. 为每个协议创建导出URL =====
        for (ProtocolConfig protocolConfig : protocols) {
            
            // 拼接pathKey(区分不同注册中心的同名协议)
            String pathKey = URL.buildKey(
                getContextPath(protocolConfig)
                    .map(p -> p + "/" + path)
                    .orElse(path),
                group,
                version
            );
            
            providerModel.setServicePath(pathKey);
            
            // ===== 3. 将服务导出到每个注册中心 =====
            doExportUrlsFor1Protocol(protocolConfig, registryURLs);
        }
    }
}

1.3 doExportUrlsFor1Protocol

java 复制代码
/**
 * doExportUrlsFor1Protocol ------ 核心:将一个服务按单一协议导出
 * 
 * 这是整个导出流程最关键的节点
 * 完成了:URL构建 → Invoker创建 → Protocol暴露
 */
public class ExportForOneProtocol {
    
    /**
     * 简化的导出流程
     */
    private void doExportUrlsFor1Protocol(ProtocolConfig protocolConfig, 
                                           List<URL> registryURLs) {
        
        // ===== 1. 构建服务URL =====
        // URL样例(最终要注册到ZK的格式):
        // dubbo://192.168.1.10:20880/com.example.GreetingService
        //   ?anyhost=true
        //   &application=greeting-provider
        //   &default.timeout=3000
        //   &dubbo=2.0.2
        //   &interface=com.example.GreetingService
        //   &methods=sayHello,getVersion
        //   &pid=12684
        //   &side=provider
        //   &version=1.0.0
        
        URL url = buildServiceUrl(protocolConfig);
        
        // ===== 2. 对每个注册中心执行导出 =====
        for (URL registryURL : registryURLs) {
            
            // ===== 3. 在服务URL中加入注册中心信息 =====
            // registryURL: registry://zk-host:2181/...
            // 合并后的url: dubbo://.../...?registry=zookeeper&...
            URL exportedUrl = url.addParameterAndEncoded(
                Constants.REGISTRY_KEY, 
                registryURL.toFullString()
            );
            
            // ===== 4. 检测是否为injvm协议 =====
            // ref.get() 获取 ServiceBean 持有的 ref 对象
            // 如果 ref 是本地代理(GenericService),添加generic参数
            if (isGenericService(ref)) {
                exportedUrl = exportedUrl.addParameter(
                    Constants.GENERIC_KEY, "true");
            }
            
            // ===== 5. 创建Invoker =====
            // ProxyFactory.getInvoker() → 使用Javassist动态代理包装ref
            Invoker<?> invoker = proxyFactory.getInvoker(
                ref, interfaceClass, exportedUrl);
            
            // ===== 6. 暴露Invoker =====
            // DelegateProviderMetaDataInvoker 包装原来的Invoker
            // 增加元数据(ServiceConfig引用)
            Exporter<?> exporter = protocol.export(
                new DelegateProviderMetaDataInvoker(invoker, this));
            
            exporters.add(exporter);  // 保存以便后续unexport
        }
    }
}

2. 双重暴露机制

2.1 为什么需要双暴露

java 复制代码
/**
 * 双重暴露的设计原因
 * 
 * 一个Dubbo Provider至少需要暴露在两个协议上:
 * 1. Injvm协议(本地暴露):同一JVM内的调用无需走网络
 * 2. Dubbo协议(远程暴露):来自其他JVM的调用走网络
 * 
 * 场景解释:
 * 
 * 假设Provider应用中有一个Spring MVC Controller,
 * 它调用了一个本地Dubbo服务:
 * 
 * @RestController
 * public class UserController {
 *     @Reference
 *     private UserService userService;  // ← 本地调用
 * }
 * 
 * 如果只有远程暴露(dubbo://...:20880),这个本地调用需要:
 * Controller → Dubbo序列化 → Netty TCP → Netty Server → 反序列化 → Provider
 * 走了一圈不必要的网络调用。
 * 
 * 有了本地暴露(injvm://),同样可达但:
 * Controller → 直接引用 → Provider
 * 零网络开销!
 */
public class DualExportMechanism {
    
    /**
     * 双暴露的触发条件:
     * 
     * if (isInjvmReferral()) {
     *     // 1. 优先暴露本地
     *     InjvmProtocol.export(invoker);
     *     
     *     // 2. 再暴露远程
     *     DubboProtocol.export(invoker);
     * }
     * 
     * Injury判定条件:
     * - scope = "local" 或 scope = null 且 url中没有 remote 参数
     */
}

2.2 本地暴露InjvmProtocol

java 复制代码
/**
 * InjvmProtocol ------ JVM本地调用协议
 * 
 * 极端简单:不涉及任何网络操作
 * export:将Invoker保存在内存Map中
 * refer:从内存Map中寻找对应的Invoker
 */
public class InjvmProtocolSimulation extends AbstractProtocol {
    
    /** 存储所有本地暴露的Exporter */
    private static final Map<String, Exporter<?>> EXPORTER_MAP = 
        new ConcurrentHashMap<>();
    
    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        // ===== 构建服务Key =====
        // key = interface:port/group/version
        // 如:com.example.GreetingService:anyhost/group1/1.0.0
        String key = serviceKey(invoker.getUrl());
        
        // ===== 创建InjvmExporter =====
        // 只是一个简单的POJO,里面包含invoker引用
        InjvmExporter<T> exporter = new InjvmExporter<>(invoker, key);
        
        // ===== 存入内存Map =====
        EXPORTER_MAP.put(key, exporter);
        
        return exporter;
    }
    
    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
        // 从内存Map查找对应的Exporter
        String key = serviceKey(url);
        Exporter<?> exporter = EXPORTER_MAP.get(key);
        
        if (exporter == null) {
            throw new RpcException("No local service found: " + key);
        }
        
        // 直接返回Exporter中的Invoker(无需网络包装!)
        return (Invoker<T>) exporter.getInvoker();
    }
}

2.3 远程暴露DubboProtocol

java 复制代码
/**
 * DubboProtocol ------ 基于Netty的TCP远程调用
 * 
 * export的职责:
 * 1. 为当前Provider开启Netty Server(如果同一端口没有已开启的Server)
 * 2. 将Invoker绑定到服务路径上
 */
public class DubboProtocolExportSimulation extends AbstractProtocol {
    
    /** 端口 → Server 的缓存 */
    private final Map<String, ExchangeServer> serverMap = new ConcurrentHashMap<>();
    
    /** 已导出的 Exporter(针对指定服务的Key) */
    private final Map<String, Exporter<?>> exporterMap = new ConcurrentHashMap<>();
    
    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
        
        URL url = invoker.getUrl();
        
        // ===== 1. 生成服务Key =====
        // key = interface:port/group/version
        // 这个key在后面接收请求时用于寻址对应的Invoker
        String key = serviceKey(url);
        
        // ===== 2. 创建DubboExporter =====
        DubboExporter<T> exporter = new DubboExporter<>(invoker, key, exporterMap);
        exporterMap.put(key, exporter);
        
        // ===== 3. 开启Netty Server(此端口下复用同一个Server) =====
        openServer(url);
        
        return exporter;
    }
    
    /**
     * 开启Netty Server ------ 如果该端口已有Server,则复用
     */
    private void openServer(URL url) {
        // key = host:port
        String key = url.getAddress();
        
        // 客户端仍然可以从Server请求中被通知
        boolean isServer = url.getParameter(Constants.IS_SERVER_KEY, true);
        if (isServer) {
            ExchangeServer server = serverMap.get(key);
            if (server == null) {
                synchronized (serverMap) {
                    server = serverMap.get(key);
                    if (server == null) {
                        // ===== 4. 创建Netty Server =====
                        // 通过 Exchangers.bind(url, handler) 创建服务器
                        // handler = requestHandler → 处理所有RPC请求
                        server = Exchangers.bind(url, requestHandler);
                        serverMap.put(key, server);
                    }
                }
            }
        }
    }
    
    /**
     * Netty Server的请求处理器
     * 
     * 当收到一个RPC请求时:
     * 1. 从请求中提取服务Key
     * 2. 从exporterMap中找到对应的Exporter
     * 3. 调用Exporter中的Invoker执行业务逻辑
     */
    private final ExchangeHandler requestHandler = new ExchangeHandlerAdapter() {
        
        @Override
        public CompletableFuture<Object> reply(ExchangeChannel channel, 
                                                 Object message) throws RemotingException {
            
            Invocation inv = (Invocation) message;
            
            // 1. 根据服务Key查找Invoker
            // inv.getAttachments().get("path") → com.example.GreetingService
            String serviceKey = serviceKey(
                inv.getAttachments().get(Constants.PATH_KEY));
            
            // 2. 从exporterMap获取对应的DubboExporter
            DubboExporter<?> exporter = (DubboExporter<?>) 
                exporterMap.get(serviceKey);
            
            if (exporter == null) {
                throw new RemotingException("No service found: " + serviceKey);
            }
            
            // 3. 获取Invoker并执行
            Invoker<?> invoker = exporter.getInvoker();
            
            // 4. 执行Filter链 + 实际业务逻辑
            Result result = invoker.invoke(inv);
            
            // 5. 异步回调返回结果
            return CompletableFuture.completedFuture(result);
        }
    };
}

3. RegistryProtocol------注册与暴露的桥梁

3.1 职责划分

RegistryProtocol 包装了底层的DubboProtocol,负责在export之前向注册中心注册Service地址,以及在refer之前从注册中心订阅Provider列表。

3.2 export源码分析

java 复制代码
/**
 * RegistryProtocol.export() 完整流程
 */
public class RegistryProtocolExport {
    
    public <T> Exporter<T> export(final Invoker<T> originInvoker) 
            throws RpcException {
        
        URL registryUrl = getRegistryUrl(originInvoker);
        
        // ===== 步骤1:获取底层的真实Protocol =====
        // 通过ProtocolListenerWrapper → ProtocolFilterWrapper → DubboProtocol链
        // getProtocol() 内部调用 ExtensionLoader.getAdaptiveExtension()
        Protocol protocol = getProtocol();
        
        // ===== 步骤2:委托底层Protocol进行真实的export =====
        // protocol = DubboProtocol(被Filter和Listener层层Wrapper)
        final ExporterChangeableWrapper<T> exporter = 
            new ExporterChangeableWrapper<>(
                protocol.export(originInvoker),  // ← 底层DubboProtocol暴露
                originInvoker
            );
        
        // ===== 步骤3:获取Registry实例 =====
        Registry registry = getRegistry(originInvoker);
        
        // ===== 步骤4:构建注册URL =====
        // 从exported URL中提取注册用的信息
        final URL registeredProviderUrl = getRegisteredProviderUrl(
            originInvoker.getUrl()
        );
        
        // ===== 步骤5:向注册中心注册 =====
        // registryUrl: zookeeper://host:port
        // registeredProviderUrl: dubbo://host:port/com.example.Xxx?...
        // 
        // ZookeeperRegistry.register() 内部:
        // → 创建临时节点:/dubbo/com.example.Xxx/providers/dubbo%3A%2F%2F...
        registry.register(registeredProviderUrl);
        
        // ===== 步骤6:创建监听URL(Provider超时/重连时重新注册) =====
        // 返回Exporter(注册行为 + 底层Netty公开曝光)
        return exporter;
    }
}

3.3 注册到ZK的节点结构

perl 复制代码
ZooKeeper中的Dubbo节点结构(export后的结果):

/dubbo
└── /com.example.GreetingService
    └── /providers
        ├── dubbo%3A%2F%2F192.168.1.10%3A20880%2F...          ← 临时节点
        │   (编码过的: dubbo://192.168.1.10:20880/...?version=1.0.0&...)
        │   Provider下线时该临时节点会自动消失
        │   
        └── dubbo%3A%2F%2F192.168.1.11%3A20880%2F...          ← 另一台Provider
            (同一接口的多个Provider节点)

4. 服务暴露完整时序图

scss 复制代码
ServiceBean           ServiceConfig         RegistryProtocol       DubboProtocol        ZooKeeper
    │                      │                      │                     │                  │
    │①onApplicationEvent   │                      │                     │                  │
    │─────────────────────►│                      │                     │                  │
    │                      │                      │                     │                  │
    │                      │②export()             │                     │                  │
    │                      │──►                   │                     │                  │
    │                      │                      │                     │                  │
    │                      │③doExportUrls()       │                     │                  │
    │                      │  ├──loadRegistries() │                     │                  │
    │                      │  └──buildServiceUrl()│                    │                  │
    │                      │                      │                     │                  │
    │                      │④导出每个协议          │                     │                  │
    │                      │doExportUrlsFor1Protocol()                 │                │
    │                      │                      │                     │                  │
    │                      │⑤创建Invoker           │                     │                  │
    │                      │proxyFactory.getInvoker(ref, url)           │                │
    │                      │                      │                     │                  │
    │                      │⑥registryProtocol.export(invoker)       │                  │
    │                      │─────────────────────►│                     │                  │
    │                      │                      │                     │                  │
    │                      │                      │⑦dubboProtocol.export│                  │
    │                      │                      │────────────────────►│                  │
    │                      │                      │                     │                  │
    │                      │                      │                     │⑧openServer()    │
    │                      │                      │                     │  └──Netty Bind  │80
    │                      │                      │                     │                  │
    │                      │                      │⑨registry.register() │                  │
    │                      │                      │──────────────────────────────────────►│
    │                      │                      │                     │                  │
    │                      │                      │               ZK创建临时节点           │
    │                      │                      │                     │                  │
    │                      │                      │⑩返回DubboExporter  │                  │
    │                      │                      │◄────────────────────│                  │
    │                      │                      │                     │                  │
    │                      │⑪返回Exporter         │                     │                  │
    │                      │◄─────────────────────│                     │                  │
    │                      │                      │                     │                  │
    │⑫export完成            │                      │                     │                  │
    │◄──────────────────────│                      │                     │                  │
    │                      │                      │                     │                  │
    服务正式提供对外RPC访问

本章总结

本章追踪了Dubbo服务从Spring容器事件触发到Netty端口监听再到ZooKeeper注册的完整链路:

阶段 关键类 核心动作
Spring事件 ServiceBean 监听ContextRefreshedEvent
配置处理 ServiceConfig doExport() → doExportUrls()
双重暴露 InjvmProtocol/DubboProtocol 本地内存Map + Netty Server
注册中心 RegistryProtocol register() + protocol.export()
网络监听 DubboProtocol.openServer Netty Server绑定端口

核心要点:

  • 服务导出需要经历:Spring事件 → 配置裁剪 → Invoker创建 → 本地暴露 → 远程暴露 → ZK注册
  • 双暴露保证同一JVM内的调用不走网络
  • ZK中的临时节点确保Provider下线后自动被Consumer感知
相关推荐
Elastic 中国社区官方博客20 小时前
我们如何通过 70 多个拉取请求,将 OpenTelemetry Java agent 迁移到 invokedynamic
java·大数据·开发语言·elasticsearch·搜索引擎·全文检索·es
tyn18820 小时前
记录本地安装nacos服务的方法及异常
java·开发语言
步行cgn21 小时前
Spring MVC 视图机制的核心接口与类
java·开发语言·后端·spring·mvc
y1su21 小时前
Leetcode 字符串模板
java·数据结构·算法·leetcode·职场和发展·动态规划
码事漫谈21 小时前
AI 读你的提示词,其实很没耐心
后端
用户8356290780511 天前
如何使用 Python 格式化 Excel 单元格中的长文本
后端·python
我命由我123451 天前
Python 开发 - Python 的 pathlib
java·开发语言·后端·python·学习·java-ee·学习方法
IT_陈寒1 天前
Java性能优化完全指南
前端·人工智能·后端
电子IT笔记1 天前
对接群晖NAS,上传和删除踩坑记录
前端·后端
Mk598781 天前
P4:把 Agent 的记忆搬进 Redis —— AgentScope-Java 工程化实战
后端