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感知
相关推荐
TYKJ02315 小时前
服务器带宽的"独享"和"共享"到底差在哪?从原理到实测讲清楚
运维·服务器·后端
用户7138742290015 小时前
.NET 10 Claim 身份体系深度解析
后端
XovH15 小时前
Django 从 0 到 1 打造完整电商平台:商品搜索
后端
XovH15 小时前
Django 从 0 到 1 打造完整电商平台:商品排序与浏览量统计
后端
kunge201315 小时前
1. Tmux 使用指南(入门篇)
后端·架构·操作系统
XovH15 小时前
Django 从 0 到 1 打造完整电商平台:商品详情页与图片展示
后端
Larcher15 小时前
新手入门:从前端三件套到动态数据渲染
前端·后端·代码规范
胡萝卜术15 小时前
从“用户管理”全栈项目深挖模块化、RESTful 与语义化之道
前端·后端
用户2986985301415 小时前
告别手动复制:Java 拆分 Word 文档的两种实用方案
java·后端