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感知