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);
}
}
三条规则:
@Adaptive标在方法上 → 生成自适应类;标在类上 → 该类就是自适应实现(AdaptiveCompiler就是这么做的)。- 方法参数里必须有
URL(或能取到 URL 的参数),否则生成的代码无法取参,运行期直接抛异常。 @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. 自测
- JDK SPI 的"一次加载全部"为什么在 Dubbo 场景下不可接受?
META-INF/dubbo/下的文件名应该是什么?getExtension("true")返回哪个实现?@Adaptive标在类上和标在方法上,行为差在哪?- Wrapper 类的判定条件是什么,它为什么能实现 AOP?
- 自定义 Filter 写完却完全不执行,先核对哪三处?