Skip to content

Dubbo 服务导出(Export)与引用(Refer)流程

你有没有想过这个问题:

Dubbo 的 Provider 是怎么把自己「暴露」出去的?Consumer 又是怎么找到 Provider 并建立连接的?

这个过程涉及到 Dubbo 的两个核心概念:服务导出(Export)和服务引用(Refer)

今天,我们来深入理解这个过程。

服务导出:Provider 是怎么「暴露」自己的?

导出的时机

Dubbo 提供了三种服务导出的触发方式:

java
// 方式 1:延迟导出(推荐)
// Spring Bean 初始化完成后导出
@DubboService(version = "1.0.0")
public class UserServiceImpl implements UserService { }

// 方式 2:立即导出
@Service(export = "dubbo:20880")
public class UserServiceImpl implements UserService { }

// 方式 3:手动导出(Spring 事件监听)
@Service
public class UserServiceImpl implements UserService {
    @PostConstruct
    public void init() {
        // 显式调用导出
        ServiceBean.getSingleton().export();
    }
}

导出的完整流程

服务导出是一个多步骤的过程:

┌─────────────────────────────────────────────────────────────┐
│                    服务导出流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. Spring 容器初始化完成                                    │
│     └──→ Spring 触发 ContextRefreshedEvent                 │
│                                                             │
│  2. ServiceBean 收到事件                                    │
│     └──→ 调用 export() 方法                                │
│                                                             │
│  3. 检查配置                                                │
│     └──→ 合并 XML/注解/编程式配置                          │
│     └──→ 校验参数(端口、版本、分组等)                      │
│                                                             │
│  4. 收集服务 URL                                            │
│     └──→ 格式:dubbo://ip:port/interface?param=value       │
│                                                             │
│  5. 遍历协议暴露服务                                        │
│     └──→ 基于 SPI 选择 Protocol 实现                        │
│     └──→ 调用 Protocol.export()                           │
│                                                             │
│  6. 注册到注册中心                                          │
│     └──→ 将 URL 注册到 ZooKeeper/Nacos                     │
│     └──→ Consumer 即可发现此服务                           │
│                                                             │
└─────────────────────────────────────────────────────────────┘

关键代码解析

Step 1:Spring 事件触发

java
// ServiceBean 继承 ApplicationListener
public class ServiceBean<T> extends ServiceConfig<T>
    implements ApplicationListener<ContextRefreshedEvent> {

    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        if (!isExported() && !isUnexported()) {
            if (shouldExport()) {
                export();  // 开始导出
            }
        }
    }
}

Step 2:收集服务 URL

java
public class ServiceConfig<T> {
    private void doExport() {
        // 构建服务 URL
        // 协议://主机:端口/接口名?参数...
        URL serviceUrl = new URL("dubbo",
            "192.168.1.100",     // host
            20880,               // port
            UserService.class.getName(),  // path
            "version", "1.0.0",
            "group", "user-group",
            "timeout", 3000,
            "methods", "findById,findByName"
        );
    }
}

Step 3:基于 SPI 暴露服务

java
// 基于 SPI 获取 Protocol 实现(默认是 DubboProtocol)
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class)
    .getAdaptiveExtension();

// 调用 export,会在指定端口启动 Netty Server
protocol.export(serviceUrl);

Step 4:注册到注册中心

java
// RegistryProtocol 负责注册
public class RegistryProtocol implements Protocol {
    @Override
    public <T> Exporter<T> export(Invoker<T> originInvoker) throws RpcException {
        // 1. 启动 Netty Server(调用 DubboProtocol)
        Exporter<T> exporter = dubboProtocol.export(invoker);

        // 2. 注册到注册中心
        Registry registry = registryFactory.getRegistry(url);
        registry.register(serviceUrl);

        return exporter;
    }
}

服务引用:Consumer 是怎么找到 Provider 的?

引用的时机

服务引用同样有多种时机:

java
// 方式 1:启动时引用(默认)
// Spring 初始化时建立连接
@Reference(version = "1.0.0")
private UserService userService;

// 方式 2:懒加载引用
// 第一次调用时才建立连接
@Reference(version = "1.0.0", lazy = true)
private UserService userService;

// 方式 3:软引用
@Reference(version = "1.0.0", lazy = true)
private UserService userService;  // 如果引用失败,返回 null 而非抛异常

引用的完整流程

┌─────────────────────────────────────────────────────────────┐
│                    服务引用流程                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. Spring 容器初始化                                        │
│     └──→ ReferenceBean 初始化                               │
│                                                             │
│  2. 创建 Invoker(代理对象)                                 │
│     └──→ 基于动态代理生成远程调用的代理                       │
│                                                             │
│  3. 注册到注册中心                                          │
│     └──→ 向 Registry 订阅服务                               │
│                                                             │
│  4. 获取 Provider 列表                                     │
│     └──→ Registry 返回可用 Provider 的地址                  │
│                                                             │
│  5. 建立连接                                                │
│     └──→ Netty Client 连接 Provider                        │
│                                                             │
│  6. 订阅服务变更                                            │
│     └──→ Registry 监听 Provider 变化                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘

关键代码解析

Step 1:创建 Invoker

java
// ReferenceBean 实现了 FactoryBean
public class ReferenceBean<T> extends ReferenceConfig<T>
    implements FactoryBean {

    @Override
    public Object getObject() {
        return get();  // 返回代理对象
    }

    @Override
    public Object getObjectType() {
        return refInterface;
    }

    @Override
    public boolean isSingleton() {
        return true;
    }
}

Step 2:生成代理对象

java
public class ReferenceConfig<T> {
    private T createProxy() {
        // 1. 如果指定了 URL,直接连接
        if (url != null) {
            urls.add(url);
        }

        // 2. 如果配置了注册中心,从注册中心获取地址
        if (registryURLs != null) {
            for (URL registryURL : registryURLs) {
                // 订阅服务,获取 Provider 列表
                List<URL> providerUrls = subscribe(registryURL);
            }
        }

        // 3. 基于动态代理创建代理对象
        return proxyFactory.getProxy(invoker);
    }
}

Step 3:订阅服务变更

java
// RegistryProtocol 处理订阅逻辑
public class RegistryDirectory<T> implements NotifyListener {

    // 收到 Provider 列表变更通知
    @Override
    public void notify(List<URL> urls) {
        // 1. 更新本地 Provider 列表
        this.providerUrls = urls;

        // 2. 刷新 Invoker
        refreshInvoker();
    }
}

基于注解的配置示例

Dubbo 2.7+ 支持注解配置,使用更加简洁:

Provider 端

java
// Application.java
@SpringBootApplication
@EnableDubbo  // 启用 Dubbo
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
java
// UserServiceImpl.java
@DubboService(          // 替换原来的 @Service
    version = "1.0.0",
    group = "user",
    timeout = 3000,
    retries = 2,
    loadbalance = "roundrobin"
)
public class UserServiceImpl implements UserService {
    @Override
    public User findById(Long id) {
        return userDao.findById(id);
    }
}

Consumer 端

java
// Application.java
@SpringBootApplication
@EnableDubbo          // 启用 Dubbo
@EnableDubboClients   // 扫描 @Reference(Dubbo 2.7+)
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
java
// UserController.java
@RestController
public class UserController {

    @Reference(        // 替换原来的 @Reference
        version = "1.0.0",
        group = "user",
        timeout = 5000,
        check = false   // 启动时不检查 Provider 是否存在
    )
    private UserService userService;

    @GetMapping("/user/{id}")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
}

导出与引用的时序图

┌─────────┐      ┌─────────┐      ┌─────────┐      ┌─────────┐
│ Spring  │      │Service  │      │Protocol│      │Registry │
│ Container│     │ Config  │      │        │      │         │
└────┬────┘      └────┬────┘      └────┬────┘      └────┬────┘
     │               │               │               │
     │ 启动完成      │               │               │
     │──────────────→│               │               │
     │               │               │               │
     │               │  export()     │               │
     │               │──────────────→               │
     │               │               │               │
     │               │               │ 启动 Netty   │
     │               │               │←──────────────│
     │               │               │               │
     │               │               │ register()   │
     │               │               │──────────────→│
     │               │               │               │
     │               │               │    完成      │
     │               │←──────────────│               │
     │   完成        │               │               │
     │←──────────────│               │               │
     │               │               │               │

面试追问方向

  • Provider 启动时为什么要监听 ContextRefreshedEvent?如果 Bean 有依赖关系怎么办?
  • 服务导出时 URL 包含哪些信息?版本号、分组是怎么传递的?
  • Consumer 怎么知道 Provider 下线了?心跳检测机制是怎么工作的?
  • 如果注册中心挂了,Consumer 还能调用 Provider 吗?Cached Registry 是什么?

总结

Dubbo 的服务导出和引用是两个镜像的过程:

阶段服务导出(Provider)服务引用(Consumer)
触发Spring 容器刷新完成Spring 容器刷新完成/第一次调用
核心对象ExporterInvoker(代理对象)
关键动作启动 Netty Server,注册到 Registry订阅服务,获取 Provider 列表
结果开放端口,监听请求建立连接,准备调用

理解了这个过程,你才能理解 Dubbo 的服务治理是怎么实现的——注册中心、负载均衡、路由策略,都建立在这个基础之上。

基于 VitePress 构建