2330 字
约 7 分钟
21
S17-X05 · Dubbo SPI:从 ExtensionLoader 到自适应扩展

Dubbo 里"扩展"不是设计模式课上的名词,而是整个框架的装配骨架:协议、序列化、负载均衡、路由、过滤器全是扩展点,连 Protocol 自己都由 SPI 加载。读懂 ExtensionLoader,启动日志里每一行 extension ... loaded 都能对上代码。

1. JDK SPI 的三个缺陷

// META-INF/services/com.example.Serializer
// 内容:两行实现类全名
// com.example.JsonSerializer
// com.example.ProtoSerializer

ServiceLoader<Serializer> loader = ServiceLoader.load(Serializer.class);
Iterator<Serializer> it = loader.iterator();   // 只有"全部遍历",没有"按名取"
缺陷 具体表现 后果
一次加载全部 iterator() 触发所有实现类实例化 没被用到的实现也被构造,静态块与资源开销全付
无按名获取 只能遍历后自己 instanceof 过滤 想按配置名选实现,得手写工厂
无 IOC / AOP 实现类拿不到其他组件,也无法被包装 每个实现自己 new 依赖,横切逻辑(监控、日志)无处挂

第三条最致命。Dubbo 需要在每个扩展外面套一层监控、一层过滤器链,JDK SPI 给不了插入点。

2. @SPI 与目录约定

@SPI("javassist")                 // 默认扩展名
public interface Compiler {
    Class<?> compile(String code, ClassLoader loader);
}
目录 谁写 说明
META-INF/dubbo/internal/ Dubbo 自己 内置实现,升级时整体替换
META-INF/dubbo/ 用户/第三方 业务扩展放这里,推荐
META-INF/services/ 兼容 JDK SPI 文件名与内容用类全名,Dubbo 也能读

格式是 key=value,一行一个,key 就是 @SPI 里说的"扩展名":

# META-INF/dubbo/com.example.Compiler
javassist=com.example.JavassistCompiler
jdk=com.example.JdkCompiler

一个接口一个文件,文件名必须是接口全限定名。写错包名不报错,只会静默加载到零个扩展,再在 getExtension 时报找不到。

3. getExtension(name) 的加载流程

Compiler c = ExtensionLoader.getExtensionLoader(Compiler.class)
                            .getExtension("jdk");
Compiler d = ExtensionLoader.getExtensionLoader(Compiler.class)
                            .getDefaultExtension();   // 取 @SPI 的值
缓存字段 存什么 作用
EXTENSION_LOADERS Class → ExtensionLoader 全局单例,一个接口一个 loader
cachedNames Class → name 反查扩展名
classes name → Class 目录扫描结果
instance name → Object 已实例化的单例
cachedAdaptiveClass 类上标 @Adaptive 的实现 免生成,直接用
cachedWrapperClasses Wrapper 类集合 AOP 包装

流程是:扫目录 → 建 name 到 Class 的表 → newInstance → 依赖注入 → Wrapper 逐层包装 → 放进 instance 缓存。第二次取同名扩展直接命中缓存,所以扩展实现必须是无状态或线程安全的单例。

getExtension 传 "true" 是特殊值,等价于取默认扩展。名字不存在时抛 IllegalStateException,消息里会列出所有可用名字——排错时先看这行列表,往往就发现是文件里 key 拼错了。

4. @Adaptive:运行时按 URL 参数选实现

@SPI("dubbo")
public interface Protocol {
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;

    @Adaptive({"protocol"})     // 从 URL 的 protocol 参数取扩展名
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
}

自适应扩展做的事:生成一个临时类,方法里从 URL 取参数、拿扩展名、再委托给真正的实现。大致等价于:

public class Protocol$Adaptive implements Protocol {
    public <T> Exporter<T> export(Invoker<T> invoker) {
        URL url = invoker.getUrl();
        String name = url.getParameter("protocol", "dubbo");   // 缺省回落到 @SPI 值
        return ExtensionLoader.getExtensionLoader(Protocol.class)
                              .getExtension(name).export(invoker);
    }
}

三条规则:

  1. @Adaptive 标在方法上 → 生成自适应类;标在类上 → 该类就是自适应实现(AdaptiveCompiler 就是这么做的)。
  2. 方法参数里必须有 URL(或能取到 URL 的参数),否则生成的代码无法取参,运行期直接抛异常。
  3. @Adaptive({"k1","k2"}) 按顺序尝试,第一个命中的 URL 参数生效。

5. @Activate 与 Wrapper

@Activate 解决"一次装配一批扩展"的问题,典型场景是过滤器链:

@Activate(group = {CommonConstants.PROVIDER, CommonConstants.CONSUMER},
          order = -9000, value = "trace")
public class TraceFilter implements Filter { ... }
属性 含义 备注
group 只在 provider / consumer 侧装配 对应 Constants.PROVIDER / CONSUMER
order 排序,小的在前 与 Ordered 不是一套
value URL 里有该参数才激活 空串表示无条件
before/after 相对其他扩展名排序 3.x 新增,按名插队

Wrapper 是另一种装配:实现接口、且只有一个"接口类型参数"的构造器,就被判定为 Wrapper:

public class ProtocolFilterWrapper implements Protocol {
    private final Protocol protocol;

    public ProtocolFilterWrapper(Protocol protocol) {   // 单参构造 = Wrapper 标志
        this.protocol = protocol;
    }
    // export 时把 Filter 链套在 invoker 外面
}

Wrapper 不进 instance 缓存,而是在目标实现外层逐层包装,天然实现 AOP。ProtocolFilterWrapper、ProtocolListenerWrapper 都是这么挂上去的,所以你在 Protocol 实现里永远看不到过滤器代码,但过滤器确实生效了。

6. ExtensionFactory 与依赖注入

扩展实现里写 setXxx 就能拿到其他扩展:

public class NettyTransporter implements Transporter {
    private Codec2 codec;

    public void setCodec(Codec2 codec) { this.codec = codec; }   // 会被自动注入
}

注入由 ExtensionFactory 完成,三个实现分工明确:

实现 查找范围
SpiExtensionFactory 按 setter 名去 SPI 容器里找
SpringExtensionFactory 去 Spring 容器按名字找 Bean
AdaptiveExtensionFactory 组合以上,按顺序问,谁先返回用谁

所以扩展类既可以是 SPI 实现也可以是 Spring Bean。注意:Spring Bean 与 SPI 单例是两份对象,在扩展里改状态不会同步。

7. 与 JDK SPI 的对照

维度 JDK SPI Dubbo SPI
加载时机 遍历时全量实例化 按名懒加载,取到才建
按名获取 无 getExtension(name)
默认实现 无 @SPI("name")
依赖注入 无 setter 注入 + ExtensionFactory
AOP 无 Wrapper 自动包装
批量装配 无 @Activate + group/order
运行时选择 无 @Adaptive 按 URL 参数
配置文件 META-INF/services/接口全名 META-INF/dubbo/接口全名,key=value

8. 一个完整可用的自定义 Filter

package com.example.gateway;

@Activate(group = {CommonConstants.CONSUMER, CommonConstants.PROVIDER}, order = -9000)
public class TraceFilter implements Filter {

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext ctx = RpcContext.getClientAttachment();     // 3.x:区分 client/server 附件
        String traceId = ctx.getAttachment("traceId");
        boolean owner = false;
        if (StringUtils.isEmpty(traceId)) {
            traceId = UUID.randomUUID().toString().replace("-", "");
            ctx.setAttachment("traceId", traceId);
            owner = true;
        }
        long start = System.nanoTime();
        try {
            return invoker.invoke(invocation);
        } finally {
            long cost = (System.nanoTime() - start) / 1_000_000;
            if (cost > 200) {
                log.warn("slow trace={} method={} cost={}ms owner={}",
                         traceId, invocation.getMethodName(), cost, owner);
            }
        }
    }
}
# src/main/resources/META-INF/dubbo/org.apache.dubbo.rpc.Filter
trace=com.example.gateway.TraceFilter
# 显式启用(覆盖 @Activate 的自动装配)
dubbo.provider.filter=trace
dubbo.consumer.filter=trace

四个必须核对的地方:

核对项 错法 现象
资源文件名 写成 Filter 或 dubbo.Filter 过滤器完全不生效
行格式 用 : 或空格分隔 key 变成整行,找不到扩展
@Activate group 只写 PROVIDER 却想在消费端生效 消费端静默不执行
附件方向 在 provider 侧 setAttachment 想回传 调用方读不到,需用 getServerAttachment 分清方向

9. 排错:六种"扩展加载不上"

现象 原因 处理
No such extension 文件名不是接口全限定名,或 key 拼错 打开异常消息里的可用列表对照
自适应扩展报错 @Adaptive 方法没有 URL 参数 补 URL 参数,或改为类级 @Adaptive
Wrapper 没生效 构造器参数不是接口类型,或有多个构造器 只留一个单参构造器
Filter 顺序与预期不符 @Activate.order 与 Ordered 混用 统一用 @Activate.order,值越小越靠前
注入的依赖是 null setter 名与扩展名不一致 setter 名就是扩展名,如 setCodec 找 codec
两个实现互相注入卡死 循环依赖 拆出中间接口,或延迟到方法内获取

10. 边界:什么时候不该上 SPI

需求 更简单的手段 理由
只有两三个固定实现 工厂 + 枚举 SPI 的目录扫描与缓存是纯开销
需要 Spring 完整生命周期 直接 @Service SPI 单例不受容器管理
需要热插拔/灰度 SPI + 配置中心 单靠 SPI 要重启
跨语言扩展 协议层约定 SPI 是 JVM 内的机制
编译期就要校验实现 注解处理器 SPI 错误全在运行期暴露

一句话:SPI 解决的是"框架不知道谁会实现、但要在运行时按名字挑一个"。业务代码里如果已经知道要哪个实现,用工厂比用 SPI 清楚得多。

11. 自测

  1. JDK SPI 的"一次加载全部"为什么在 Dubbo 场景下不可接受?
  2. META-INF/dubbo/ 下的文件名应该是什么?
  3. getExtension("true") 返回哪个实现?
  4. @Adaptive 标在类上和标在方法上,行为差在哪?
  5. Wrapper 类的判定条件是什么,它为什么能实现 AOP?
  6. 自定义 Filter 写完却完全不执行,先核对哪三处?
S17-X05 · Dubbo SPI:从 ExtensionLoader 到自适应扩展
https://www.clxhxhhr.top/posts/5668/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。