Skip to content

Dubbo SPI 与扩展点

Dubbo 的很多能力都不是写死的,而是通过 SPI 扩展点加载。负载均衡、协议、序列化、Filter、路由、集群容错等都可以扩展。理解 Dubbo SPI,才能真正理解 Dubbo 为什么能做成一个可插拔的 RPC 框架。

学习目标

目标说明
理解 SPI知道 SPI 是一种按接口发现实现类的机制
区分 Java SPI知道 Dubbo SPI 和 JDK SPI 的区别
掌握扩展点知道 Filter、LoadBalance、Protocol、Serialization 等如何扩展
会写 Demo能写一个自定义 Filter 或 LoadBalance
会面试能讲清 Dubbo 自适应扩展和扩展点加载思想

SPI 是什么

SPI 全称 Service Provider Interface,可以理解为:

框架定义接口,第三方或业务方提供实现,框架运行时按名称加载实现。

普通写法是代码里直接 new:

java
LoadBalance loadBalance = new RandomLoadBalance();

这样框架和实现强耦合。SPI 写法是通过配置选择:

text
random=org.apache.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
roundrobin=org.apache.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance

调用时根据名称加载:

java
LoadBalance loadBalance = ExtensionLoader
        .getExtensionLoader(LoadBalance.class)
        .getExtension("random");

Dubbo SPI 和 JDK SPI 区别

对比JDK SPIDubbo SPI
配置目录META-INF/services/META-INF/dubbo/
加载方式一次加载所有实现按名称加载指定实现
IOC 能力支持依赖注入
AOP 包装不支持支持 Wrapper 包装
自适应扩展不支持支持 Adaptive
性能实现多时可能浪费按需加载更适合框架

Dubbo 自己实现 SPI,是因为 RPC 框架扩展点很多,JDK SPI 无法很好满足按名加载、自适应选择、包装增强和依赖注入。

Dubbo SPI 加载流程

mermaid
flowchart TD
    A["调用 ExtensionLoader"] --> B["读取 SPI 配置文件"]
    B --> C["解析 name 到 class 映射"]
    C --> D["按名称创建扩展对象"]
    D --> E["注入依赖"]
    E --> F["Wrapper 包装增强"]
    F --> G["缓存并返回扩展实例"]

这条链路解释了为什么 Dubbo 扩展点可以做到:

  1. 配置里写 loadbalance=random 就能切换实现。
  2. 扩展类可以依赖其他扩展。
  3. Filter、Protocol 等可以被包装增强。
  4. 不同 URL 参数可以选择不同扩展实现。

ExtensionLoader 内部为什么不是简单反射

如果 SPI 只是 Class.forName(name).newInstance(),它只能创建对象,无法支撑 Dubbo 的协议、路由、负载均衡、Filter、Cluster 等复杂组合。Dubbo SPI 的关键是把“按名加载、单例缓存、依赖注入、Wrapper 包装、自适应选择、自动激活”拆成多个阶段。

可以把一次 getExtension("random") 理解成:

mermaid
flowchart TD
    A["调用getExtension(name)"] --> B["检查扩展实例缓存"]
    B --> C{"缓存是否命中"}
    C -- "命中" --> D["返回已包装实例"]
    C -- "未命中" --> E["读取并解析SPI资源"]
    E --> F["找到name对应实现Class"]
    F --> G["反射创建原始实例"]
    G --> H["按setter注入依赖扩展"]
    H --> I["按Wrapper类逐层包装"]
    I --> J["缓存最终实例"]
    J --> D

这里要特别注意三类缓存:

缓存对象缓存什么为什么需要
ExtensionLoader缓存每个扩展接口一个Loader避免重复解析同一扩展接口
扩展类映射缓存name -> Class避免每次调用重新读取配置文件
扩展实例缓存name -> instance扩展通常按名称复用,减少对象创建和状态分裂

这也是为什么扩展实现要尽量无状态或线程安全。一个自定义 LoadBalance 如果把“上一次请求的用户ID”存在成员变量里,多线程调用时就会互相污染。

SPI 文件路径和命名为什么容易错

Dubbo SPI 不是 Java 原生 ServiceLoader,配置位置和格式不同。常见资源目录包括:

text
META-INF/dubbo/
META-INF/dubbo/internal/
META-INF/services/

业务自定义扩展通常放在:

text
META-INF/dubbo/接口全限定名

例如自定义 Filter:

text
META-INF/dubbo/org.apache.dubbo.rpc.Filter

文件内容是 扩展名=实现类全限定名

text
tenant=com.example.dubbo.filter.TenantProviderFilter

常见错误:

错误现象
文件名写成实现类名ExtensionLoader 找不到扩展
放到 META-INF/services 但按 Dubbo SPI 加载配置不生效
扩展名和配置使用名不一致No such extension
依赖包没有打进最终 jar本地能跑,部署后找不到
实现类没有无参构造或构造失败扩展创建失败

常见扩展点

扩展点作用商业用途
Filter调用前后增强日志、鉴权、traceId、限流、指标
LoadBalance负载均衡按机房、权重、延迟、租户选择实例
Router路由过滤灰度、标签、地域隔离
ProtocolRPC 协议自定义协议或适配其他通信方式
Serialization序列化替换为 Protobuf、JSON、公司内部格式
Cluster集群容错自定义失败处理策略
Configurator动态配置运行时调整超时、权重、路由
ProxyFactory代理生成JDK、Javassist 等代理方式

Demo:自定义 Filter 传递租户 ID

商业系统经常需要把租户 ID、traceId、用户 ID 从 Consumer 传到 Provider。Dubbo 可以通过 RpcContext 附件传递。

Consumer 侧 Filter:

java
package com.example.dubbo.filter;

import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.RpcException;

@Activate(group = "consumer")
public class TenantConsumerFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String tenantId = TenantContext.getTenantId();
        if (tenantId != null) {
            RpcContext.getClientAttachment().setAttachment("tenantId", tenantId);
        }
        return invoker.invoke(invocation);
    }
}

Provider 侧 Filter:

java
package com.example.dubbo.filter;

import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.Result;
import org.apache.dubbo.rpc.RpcContext;
import org.apache.dubbo.rpc.RpcException;

@Activate(group = "provider")
public class TenantProviderFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        String tenantId = RpcContext.getServerAttachment().getAttachment("tenantId");
        try {
            TenantContext.setTenantId(tenantId);
            return invoker.invoke(invocation);
        } finally {
            TenantContext.clear();
        }
    }
}

线程上下文工具:

java
public final class TenantContext {
    private static final ThreadLocal<String> LOCAL = new ThreadLocal<>();

    private TenantContext() {
    }

    public static void setTenantId(String tenantId) {
        LOCAL.set(tenantId);
    }

    public static String getTenantId() {
        return LOCAL.get();
    }

    public static void clear() {
        LOCAL.remove();
    }
}

SPI 配置文件:

text
META-INF/dubbo/org.apache.dubbo.rpc.Filter

内容:

text
tenantConsumer=com.example.dubbo.filter.TenantConsumerFilter
tenantProvider=com.example.dubbo.filter.TenantProviderFilter

为什么要在 finally 里清理 ThreadLocal?因为 Provider 线程池会复用线程,如果不清理,租户信息可能串到下一次请求,造成严重数据隔离事故。

Demo:自定义负载均衡

下面是教学版示例:优先选择 URL 参数里 zone 与本机 zone 一致的 Provider,否则退化成第一个。

java
package com.example.dubbo.loadbalance;

import org.apache.dubbo.common.URL;
import org.apache.dubbo.rpc.Invocation;
import org.apache.dubbo.rpc.Invoker;
import org.apache.dubbo.rpc.cluster.LoadBalance;

import java.util.List;

public class SameZoneLoadBalance implements LoadBalance {

    @Override
    public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) {
        String localZone = System.getProperty("zone", "default");
        for (Invoker<T> invoker : invokers) {
            String providerZone = invoker.getUrl().getParameter("zone", "default");
            if (localZone.equals(providerZone)) {
                return invoker;
            }
        }
        return invokers.get(0);
    }
}

SPI 文件:

text
META-INF/dubbo/org.apache.dubbo.rpc.cluster.LoadBalance

内容:

text
sameZone=com.example.dubbo.loadbalance.SameZoneLoadBalance

使用:

java
@DubboReference(loadbalance = "sameZone")
private StockService stockService;

真实项目里不能简单返回第一个,要处理权重、可用性、慢实例和空列表。这段代码主要用于理解扩展机制。

自适应扩展是什么

Dubbo 的 Adaptive 扩展可以根据 URL 参数动态选择具体实现。

例如同一个 LoadBalance 接口:

text
loadbalance=random
loadbalance=roundrobin
loadbalance=leastactive

框架运行时根据 URL 参数选择不同实现,而不是业务代码手写 if else。

简化流程:

mermaid
flowchart TD
    A["拿到 URL 参数"] --> B{"loadbalance 是什么"}
    B -- "random" --> C["加载 RandomLoadBalance"]
    B -- "roundrobin" --> D["加载 RoundRobinLoadBalance"]
    B -- "leastactive" --> E["加载 LeastActiveLoadBalance"]
    C --> F["执行 select"]
    D --> F
    E --> F

这就是 Dubbo 高度可配置的基础。

自适应扩展更像“运行时生成的路由器”

自适应扩展不是把所有实现都执行一遍,而是生成一个“根据 URL 参数选择扩展名”的代理类。以 LoadBalance 思路简化后,大概相当于下面的 JDK 8 代码:

java
import java.util.HashMap;
import java.util.Map;

public class AdaptiveExtensionDemo {

    interface LoadBalance {
        String select(String serviceUrl);
    }

    static class RandomLoadBalance implements LoadBalance {
        public String select(String serviceUrl) {
            return "random provider for " + serviceUrl;
        }
    }

    static class RoundRobinLoadBalance implements LoadBalance {
        public String select(String serviceUrl) {
            return "roundrobin provider for " + serviceUrl;
        }
    }

    static class MiniExtensionLoader {
        private final Map<String, LoadBalance> extensions = new HashMap<String, LoadBalance>();

        MiniExtensionLoader() {
            extensions.put("random", new RandomLoadBalance());
            extensions.put("roundrobin", new RoundRobinLoadBalance());
        }

        LoadBalance getExtension(String name) {
            LoadBalance extension = extensions.get(name);
            if (extension == null) {
                throw new IllegalArgumentException("找不到扩展: " + name);
            }
            return extension;
        }
    }

    static class AdaptiveLoadBalance implements LoadBalance {
        private final MiniExtensionLoader loader;

        AdaptiveLoadBalance(MiniExtensionLoader loader) {
            this.loader = loader;
        }

        public String select(String serviceUrl) {
            String name = getParameter(serviceUrl, "loadbalance", "random");
            return loader.getExtension(name).select(serviceUrl);
        }

        private String getParameter(String url, String key, String defaultValue) {
            String mark = key + "=";
            int index = url.indexOf(mark);
            if (index < 0) {
                return defaultValue;
            }
            int start = index + mark.length();
            int end = url.indexOf('&', start);
            return end < 0 ? url.substring(start) : url.substring(start, end);
        }
    }

    public static void main(String[] args) {
        LoadBalance adaptive = new AdaptiveLoadBalance(new MiniExtensionLoader());
        System.out.println(adaptive.select("dubbo://10.0.0.1:20880?loadbalance=random"));
        System.out.println(adaptive.select("dubbo://10.0.0.2:20880?loadbalance=roundrobin"));
    }
}

真实 Dubbo 会根据扩展接口、@Adaptive、URL 参数名和默认扩展名生成或使用自适应类。这个 Demo 只说明核心思想:调用方依赖扩展接口,运行时从 URL 或 Invocation 上下文取参数,再按名称选择具体实现

如果没有自适应扩展,框架就会在很多地方写死:

text
if protocol == dubbo then ...
if protocol == triple then ...
if loadbalance == random then ...

这样每增加一个协议、序列化或负载均衡算法,都要修改框架核心代码,扩展性和稳定性都会变差。

Wrapper 包装扩展

Wrapper 可以理解成 Dubbo SPI 层面的装饰器。

mermaid
flowchart TD
    A["原始 Protocol"] --> B["ProtocolFilterWrapper"]
    B --> C["ProtocolListenerWrapper"]
    C --> D["最终 Protocol"]

它允许框架在不改原始实现的情况下增加 Filter、Listener 等能力。这和设计模式里的装饰器模式类似。

Wrapper 顺序为什么影响调用链

Wrapper 本质是构造函数接收同类型扩展对象,再返回一个增强后的对象。例如:

java
public class MetricsProtocolWrapper implements Protocol {
    private final Protocol protocol;

    public MetricsProtocolWrapper(Protocol protocol) {
        this.protocol = protocol;
    }
}

如果一个扩展同时被多个 Wrapper 包装,调用顺序会影响日志、Filter、Listener、异常映射和资源释放。抽象模型如下:

mermaid
flowchart TD
    A["原始扩展"] --> B["Wrapper-1"]
    B --> C["Wrapper-2"]
    C --> D["最终暴露给调用方的扩展"]

因此排查时不能只问“加载的是哪个 Protocol”,还要看它外面包了哪些 Wrapper。很多能力不是原始协议类直接完成,而是在 Wrapper 中织入。

@Activate 自动激活、分组和顺序

Filter 这类扩展不能每次都手工指定,否则框架默认的监控、上下文、鉴权、限流等能力很难统一装配。Dubbo 通过 @Activate 支持按条件自动激活。

常见维度:

维度含义
group在 Consumer 侧、Provider 侧或两侧激活
valueURL 中存在某些参数时激活
order多个扩展的相对顺序
before/after与其他扩展的前后关系,具体能力看版本

Filter 顺序会影响业务正确性:

mermaid
flowchart TD
    A["收到RPC调用"] --> B["Trace Filter生成上下文"]
    B --> C["Auth Filter校验身份"]
    C --> D["Tenant Filter设置租户"]
    D --> E["Limit Filter并发或QPS限制"]
    E --> F["真实Invoker执行"]
    F --> G["finally清理ThreadLocal"]

如果顺序错了会怎样:

错误顺序后果
租户上下文晚于业务执行SQL 或缓存 Key 拿不到租户,可能串数据
异常映射早于指标统计指标看到的是加工后的假成功
限流在昂贵鉴权之后大量非法请求仍消耗认证和数据库资源
没有 finally 清理Provider 线程复用导致身份串号

所以自定义 Filter 不只要“能跑”,还要说明它在 Consumer 还是 Provider 侧执行、放在谁前后、失败时返回什么、是否影响重试和幂等。

扩展点使用原则

原则原因
优先使用官方扩展点兼容性和维护成本更好
不在 Filter 写慢逻辑Filter 每次调用都执行,慢逻辑会放大性能问题
ThreadLocal 必须清理Dubbo 线程池复用线程,容易串数据
扩展要有开关出问题时能快速关闭
扩展要可观测加日志、指标和异常告警
不把业务逻辑塞进框架扩展会造成隐式依赖,难测试难排查

生产排查:自定义扩展不生效或导致故障

扩展不生效

mermaid
flowchart TD
    A["扩展不生效"] --> B["确认SPI资源是否进入最终jar"]
    B --> C["确认文件名是否为扩展接口全限定名"]
    C --> D["确认扩展名和配置使用名一致"]
    D --> E["确认@Activate group和值是否匹配"]
    E --> F["确认URL最终参数是否包含期望key"]
    F --> G["打开Dubbo启动日志或ExtensionLoader诊断"]

常见证据:

证据看什么
jar 内容META-INF/dubbo/... 是否存在
启动日志扩展加载失败、重复扩展名、类创建异常
URL 参数loadbalancefilterprotocol 等最终值
调用链日志Filter 是否进入、顺序是否符合预期
版本依赖Dubbo 2.x/3.x 包名、接口签名、激活语义是否变化

扩展导致线上故障

故障可能原因止血
全链路变慢Filter 做远程调用、写日志过多、同步落库配置关闭扩展,改异步或采样
租户串数据ThreadLocal 未清理或附件信任外部传入立即下线扩展,补鉴权和清理
灰度串流Router/LoadBalance 无匹配时静默回退改成严格失败或受控回退
重试放大Filter 把业务异常包装成可重试异常区分业务错误和瞬时传输错误
Provider 线程打满Provider Filter 在业务线程中做慢操作前置限流、异步化或移出RPC链路

扩展点是框架能力入口,不是业务逻辑垃圾桶。越靠近调用链底层,影响面越大,越需要开关、灰度、监控、回滚和压测。

常见面试追问

问题标准回答深入原理
Dubbo SPI 为什么不用 JDK SPIJDK SPI 一次加载全部实现、缺少按名加载、自适应、依赖注入和 Wrapper,不适合大量扩展点的 RPC 框架。ExtensionLoader内部流程
Filter 能做什么可做日志、鉴权、trace、限流、指标、参数校验,但不能写慢业务。Filter Demo@Activate顺序
自适应扩展是什么根据 URL 参数运行时选择具体扩展实现,本质像一个运行时生成的路由器。自适应扩展 Demo
Wrapper 有什么用在扩展对象外层包一层增强逻辑,类似装饰器;很多默认能力通过 Wrapper 织入。Wrapper顺序
自定义扩展怎么排查不生效检查 SPI 文件路径、接口名、扩展名、最终 jar、URL 参数、@Activate group 和版本兼容。扩展排查Runbook

本章小结

Dubbo SPI 是 Dubbo 可插拔架构的基础。Dubbo 通过 ExtensionLoader 按名称加载扩展,通过 URL 参数实现自适应选择,通过 Wrapper 做装饰增强,通过 Filter 等扩展点把日志、鉴权、限流、监控等能力插入调用链。掌握 SPI 后,Dubbo 就不再是黑盒,而是一套可以理解和扩展的框架。