Java 与 Dubbo 的 SPI 机制介绍
Java SPI 机制
概念介绍
Java 原生支持 SPI 机制,具体介绍如下:
- 核心概念
- SPI(Service Provider Interface)是一种服务发现机制。
- SPI 的核心思想是定义一个接口,由多个实现类提供不同的实现方式,在系统运行时根据配置或者默认策略,动态加载并使用具体的实现类。
- SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样就可以在运行时,动态为接口替换实现类。
工作原理
- (1) 接口与实现类
- 假设有一个接口
A,它有多个实现类:A -> A1、A2、A3。
- 假设有一个接口
- (2) 配置实现类
- 可以在配置文件中指定接口
A对应使用哪个实现类。
- 可以在配置文件中指定接口
- (3) 运行时加载
- 程序启动时,会读取配置文件,根据配置信息找到对应的实现类,实例化并使用该对象。
- (4) Java 原生 SPI 机制
- Java 原生 SPI 机制的使用要求:
- 在
resources/META-INF/services/目录下,创建一个与接口全限定名相同的文件,例如:resources/META-INF/services/com.example.service.A - 文件内容的格式是一行一个实现类的全限定名(可以有多行,即支持多个不同的实现类),例如:
com.example.service.impl.A1。 - 运行时通过
ServiceLoader等工具扫描依赖的 Jar 包,在其中查找该文件,并加载指定的实现类,比如:ServiceLoader<HelloService> loader = ServiceLoader.load(HelloService.class);
- 在
- Java 原生 SPI 机制的使用要求:
- (1) 接口与实现类
应用场景
- SPI 机制常用于插件式扩展。
- 比如:如果你在开发一个框架,可以通过 SPI 让外部开发者编写插件,扩展框架的功能,而不必修改框架的源码。
典型案例
- JDBC
- Java 标准库只定义了一套 JDBC 接口,并没有真正的实现。
- 数据库厂商(如 MySQL、Oracle)会提供自己的实现,并通过 SPI 机制声明在
resources/META-INF/services/目录中。 - 运行时,Java 会根据项目引入的数据库驱动 Jar 包,自动找到对应的 JDBC 实现类。
- JDBC
Dubbo SPI 机制
概念介绍
Dubbo 借鉴了 SPI 思想,但没有直接使用 Java 原生的 SPI 机制,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader 类可以加载指定的实现类。
工作原理
- (1) 接口声明
- 在 Dubbo 中,如果某个接口需要支持 SPI 扩展,就会加上
@SPI注解,比如Protocol接口:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// 接口定义
public interface Protocol {
int getDefaultPort();
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
void destroy();
}
// 加载实现类
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); @SPI("dubbo")表示默认实现是dubbo。@Adaptive表示该方法会生成代理逻辑,运行时根据参数动态选择实现类。
- 在 Dubbo 中,如果某个接口需要支持 SPI 扩展,就会加上
- (2) 实现类配置
- Dubbo 在自己 Jar 包中的
resources/META-INF/dubbo/internal/路径下提供了一个配置文件,文件名是接口的全限定名,比如:1
resources/META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol
- 配置文件的内容是
key=实现类的全限定名,key 对应@SPI注解中的扩展名称,比如:1
2
3dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
- Dubbo 在自己 Jar 包中的
- (3) 默认实现加载
- 如果用户没有配置扩展,Dubbo 会根据
@SPI("dubbo")的默认值dubbo,从配置文件中加载对应的实现类DubboProtocol。 - 这也是 Dubbo 默认使用 Dubbo 协议作为 RPC 通信协议的原因。
- 如果用户没有配置扩展,Dubbo 会根据
- (4) 动态切换实现类
- 在
Protocol接口中,有两个方法加了@Adaptive注解。 - Dubbo 会在运行时生成代理类,在代理方法内部根据传入的 URL 参数的
protocol值决定使用哪个实现类。 - 如果 URL 参数中没指定协议,就用默认的
dubbo;如果指定了其他值(如http),则加载对应的实现类。
- 在
- (1) 接口声明
实现特点
- 微内核 + 可插拔:保留一个接口和多个实现,运行时可替换。
- 组件化:如
Protocol负责 RPC 调用,可以替换为自定义的 RPC 组件。 - 动态扩展:可通过 URL 参数或配置文件,在运行时动态切换实现类。
- 增强的 SPI:相比 Java 原生的 SPI 机制,Dubbo 的实现支持:
- 支持指定默认的实现类(
@SPI注解的默认值) - 运行时动态选择实现类(
@Adaptive) - 接口定义(扩展点)自动生成代理类
- 支持指定默认的实现类(
使用总结
- Dubbo SPI 本质上是一个运行时可扩展、可替换的组件机制。
- Dubbo 大量核心组件(如
Protocol、Cluster、Registry等)都是用这种 SPI 机制实现扩展的。 - 通过
@SPI注解 + 配置文件来确定默认的实现类,通过@Adaptive注解 + URL 参数来实现动态切换实现类。 - Dubbo SPI 的扩展文件路径(点击查看源码定义)
- 与 Java SPI 的不一样,Dubbo SPI 提供给开发者使用的扩展文件是
resources/META-INF/dubbo/接口全限定名,文件内容是key=实现类的全限定名; - Dubbo 自己内部使用的扩展文件是
resources/META-INF/dubbo/internal/接口全限定名,文件内容是key=实现类的全限定名;
- 与 Java SPI 的不一样,Dubbo SPI 提供给开发者使用的扩展文件是
加载流程
Dubbo SPI 机制加载扩展的核心步骤:
- (1) 读取并解析配置文件
- (2) 缓存所有扩展实现类
- (3) 基于用户执行的扩展名,实例化对应的扩展实现类
- (4) 执行扩展实例属性的 IOC 注入(基于
Setter注入),以及实例化扩展的包装类,实现 AOP 特性
Dubbo SPI 机制加载扩展的整个流程:

核心注解
Dubbo SPI 机制有两个核心注解,分别是 @Adaptive 和 @Activate。
@Adaptive注解(自适应)- 主要作用
- 表示该扩展类或接口方法需要自适应扩展,Dubbo 会在运行时根据 URL 或其他条件动态选择具体扩展实现。
- 常见使用方式
@Adaptive标注在接口方法上(最常见)- 表示由 Dubbo 自动生成
Xxx$Adaptive代理类 - 方法内部会根据 URL 参数在运行时选择具体扩展实现
- 使用例子:
1
2
3
4
5
6public interface Protocol {
Exporter export(Invoker invoker);
}- 当上面调用
export()方法时,Dubbo 会根据 URL 的protocol或defaultProtocol参数决定使用哪个协议实现
- 当上面调用
- 表示由 Dubbo 自动生成
@Adaptive标注在扩展类上(较少使用)- 表示该扩展类是一个手工编写的自适应扩展类
- Dubbo 将直接使用这个扩展类,而不会再自动生成代理类
- 一般用于非常复杂的场景,比如需要手写逻辑替代 Dubbo 自动生成代理类
- 目前 Dubbo 中仅有两个扩展类标注了
@Adaptive,分别是AdaptiveCompiler和AdaptiveExtensionFactory,表示这些扩展类的加载逻辑由人工编码(静态编码)完成 - 使用例子:
1
2
3
4
5
6
7
8
9
public class CustomAdaptiveCompiler implements Compiler {
public Class<?> compile(String code, ClassLoader loader) {
// 自定义代码编译逻辑
}
}
- 使用注意事项
- 接口上不能标注
@Adaptive - 接口方法上可以标注
@Adaptive,表示使用 Dubbo 自动生成的代理类,适用大多数场景 - 扩展类上可以标注
@Adaptive,表示使用手写的自适应逻辑,适用特殊复杂场景
- 接口上不能标注
- 主要作用
@Activate注解(自动激活)- 主要作用
- 表示当扩展类被自动加载时,满足
group、value、order等条件,该扩展会自动加入扩展链,无需手动在配置中指定扩展名。
- 表示当扩展类被自动加载时,满足
- 常见使用方式
@Activate标注在扩展类上(最常见)- 扩展类在满足 Activate 条件时自动激活
- 一般用于 Filter、Router、ExporterListener、Registry 等扩展点
- 使用例子:
1
2
3
4
5
6
7
8
9
10
public class MyProviderFilter implements Filter {
public Result invoke(Invoker<?> invoker, Invocation invocation) {
// 自定义过滤逻辑
return invoker.invoke(invocation);
}
}
@Activate标注在接口方法上(较少使用)- 表示该接口方法返回的扩展类实例在调用时可自动激活
- 可结合 URL 参数或
group、value、order条件进行动态控制 - 使用例子:
1
2
3
4
5
6public interface SomeFactory {
Extension create();
}
- 典型使用
@Activate自动激活的扩展点(最常见场景)- Filter 自动生效(过滤器)
- Router 自动生效(路由)
- ExporterListener 自动生效(监听服务暴露)
- Registry(如注册中心相关监听、通知)自动生效
- 可使用
@Activate激活,但通常是通过 URL 指定或者明确选择的的扩展点- Cluster(有些装饰逻辑可能自动封装,但多数是由 Dubbo 主流程显式选择)
- Protocol(支持
@Activate,但较少用于自动激活,一般是 URL 或显式选择) - ProxyFactory(可自动激活,但通常通过指定实现选择)
- 使用注意事项
- 接口上不能标注
@Activate
- 接口上不能标注
- 主要作用
总结
@Adaptive(自适应)的作用:让扩展在运行时根据 URL 或条件动态选择具体扩展实现;标注在方法上,则表示由 Dubbo 自动生成代理类;标注在类上,则表示使用手写的自适应逻辑。@Activate(自动激活)的作用:让扩展类在满足 Activate 条件时自动激活加入调用链;可以省略,但省略后不会自动激活,必须通过扩展名主动加载,例如:extensionLoader.getExtension("customizedProtocol");。
