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 参数loadbalance、filter、protocol 等最终值
调用链日志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 就不再是黑盒,而是一套可以理解和扩展的框架。