Java SPI 到底怎么实现动态扩展的?从 ServiceLoader 到 Dubbo SPI 全链路拆解
面试常考:META-INF/services、ServiceLoader 原理、Dubbo SPI 增强、SPI 与 IOC/AOP
一、从一个解耦问题出发
你在写一个框架,需要支持多种数据库驱动(MySQL、Oracle、PostgreSQL)。不能把所有驱动的依赖硬编码进框架,怎么办?
java
// 硬编码方式 (糟糕)
public class ConnectionFactory {
public Connection create(String type) {
if ("mysql".equals(type)) return new MySQLConnection();
else if ("oracle".equals(type)) return new OracleConnection();
// 每加一种数据库就要改代码...
}
}
// SPI 方式 (优雅)
ServiceLoader<Driver> loaders = ServiceLoader.load(Driver.class);
for (Driver driver : loaders) {
driver.connect(url);
}
// 新增数据库只需加 jar 包 + 配置文件,零代码修改
JDBC 的 DriverManager 就是这么做的。这就是 Java SPI(Service Provider Interface)机制。
二、Java SPI 机制
核心要素
bash
┌──────────────────────────────────────────────────────┐
│ Java SPI 三要素 │
│ │
│ 1. 服务接口 (Service Interface) │
│ 定义在 API 模块中 │
│ 例: java.sql.Driver │
│ │
│ 2. 服务实现 (Service Implementation) │
│ 各提供方实现接口 │
│ 例: com.mysql.cj.jdbc.Driver │
│ │
│ 3. 配置文件 │
│ META-INF/services/接口全限定名 │
│ 内容: 实现类的全限定名 │
│ 例: META-INF/services/java.sql.Driver │
│ 内容: com.mysql.cj.jdbc.Driver │
│ │
└──────────────────────────────────────────────────────┘
SPI 工作流程
css
调用方: ServiceLoader.load(Driver.class)
│
▼
┌─────────────────────────────────┐
│ 扫描所有 jar 的 │
│ META-INF/services/ │
│ java.sql.Driver 文件 │
└──────────────┬──────────────────┘
│
┌──────────▼──────────┐
│ 读取每行实现类名 │
│ com.mysql.cj... │
│ org.postgresql... │
│ oracle.jdbc... │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ Class.forName() │
│ 加载并实例化每个类 │
└──────────┬──────────┘
│
┌──────────▼──────────┐
│ 返回实现实例迭代器 │
│ 遍历使用 │
└─────────────────────┘
SPI 实战示例
定义接口:
java
package com.example.spi;
public interface PaymentService {
String getName();
void pay(String orderId, double amount);
}
实现1 - 支付宝:
java
package com.example.alipay;
public class AlipayService implements PaymentService {
@Override
public String getName() { return "alipay"; }
@Override
public void pay(String orderId, double amount) {
System.out.println("支付宝支付: " + orderId + ", " + amount);
}
}
文件 META-INF/services/com.example.spi.PaymentService:
com.example.alipay.AlipayService
实现2 - 微信支付:
java
package com.example.wechat;
public class WechatPayService implements PaymentService {
@Override
public String getName() { return "wechat"; }
@Override
public void pay(String orderId, double amount) {
System.out.println("微信支付: " + orderId + ", " + amount);
}
}
文件 META-INF/services/com.example.spi.PaymentService:
com.example.wechat.WechatPayService
使用:
java
public class SPIDemo {
public static void main(String[] args) {
ServiceLoader<PaymentService> loader =
ServiceLoader.load(PaymentService.class);
// 遍历所有实现
for (PaymentService service : loader) {
System.out.println(service.getName());
// alipay
// wechat
}
// 按名称获取特定实现
PaymentService alipay = StreamSupport
.stream(loader.spliterator(), false)
.filter(s -> "alipay".equals(s.getName()))
.findFirst()
.orElseThrow();
alipay.pay("ORD001", 99.9);
}
}
三、ServiceLoader 源码拆解
java
public final class ServiceLoader<S> implements Iterable<S> {
// 配置文件目录前缀
private static final String PREFIX = "META-INF/services/";
private final Class<S> service; // 接口类型
private final ClassLoader loader; // 类加载器
private LinkedHashMap<String, S> providers = new LinkedHashMap<>(); // 缓存
public static <S> ServiceLoader<S> load(Class<S> service) {
return ServiceLoader.load(service, Thread.currentThread().getContextClassLoader());
}
// 懒加载迭代器
private class LazyClassPathLookupIterator<T> implements Iterator<Provider<T>> {
// 核心: 按 reload() 时的配置查找下一个实现
@Override
public boolean hasNext() {
// 1. 读取配置文件 (每一行是一个实现类全限定名)
// 2. Class.forName(name, false, loader) 加载
// 3. newInstance() 实例化
// 4. 缓存到 providers
}
@Override
public Provider<T> next() {
return nextProvider();
}
}
public Iterator<S> iterator() {
return new LazyIterator();
}
}
ServiceLoader 的特点
vbnet
ServiceLoader 特性:
┌──────────────────────────────────────────────────────┐
│ │
│ 1. 懒加载: 只有迭代到某个实现时才加载和实例化 │
│ 遍历 for 循环才触发 Class.forName │
│ │
│ 2. 顺序保证: 按配置文件中的顺序加载 │
│ (按 jar 在 classpath 中的顺序) │
│ │
│ 3. 单实例: 每个实现类只实例化一次 │
│ 缓存在 providers Map 中 │
│ │
│ 4. 无参构造: 实现类必须有 public 无参构造器 │
│ 不能通过构造器注入依赖 │
│ │
│ 5. 非线程安全: 多线程并发使用 ServiceLoader │
│ 需要外部同步 │
│ │
│ 6. 无法按需获取: 只能遍历所有,不能按 key 获取 │
│ │
└──────────────────────────────────────────────────────┘
ServiceLoader 的局限
| 局限 | 说明 | 影响 |
|---|---|---|
| 全部加载 | 遍历时加载所有实现 | 性能浪费 |
| 无依赖注入 | 只能无参构造 | 无法注入依赖 |
| 无 AOP | 直接 new | 无法包装代理 |
| 无生命周期 | 创建后无法管理 | 无启动/销毁 |
| 非 IOC | 无按名获取 | 需遍历过滤 |
| 线程不安全 | 无内置同步 | 多线程有风险 |
四、Dubbo SPI:Java SPI 的增强版
Dubbo 为了解决 Java SPI 的局限,设计了自有的 SPI 机制,1000倍性能提升 + IOC + AOP。
核心差异对比
| 特性 | Java SPI | Dubbo SPI |
|---|---|---|
| 配置文件位置 | META-INF/services/ | META-INF/dubbo/ |
| 配置格式 | 每行一个类名 | key=value |
| 加载方式 | 全部加载 | 按需加载 (IoC) |
| 依赖注入 | 不支持 | 支持 |
| AOP 包装 | 不支持 | 支持 (Wrapper) |
| 自适应扩展 | 不支持 | @Adaptive |
| 激活扩展 | 不支持 | @Activate |
| 性能 | 遍历全部 → 慢 | 按 key 查 → 快 |
Dubbo SPI 配置格式
文件 META-INF/dubbo/org.apache.dubbo.rpc.Protocol:
ini
dubbo=com.example.DubboProtocol
http=com.example.HttpProtocol
registry=com.example.RegistryProtocol
key-value 格式,按 key 获取实现:
java
ExtensionLoader<Protocol> loader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol protocol = loader.getExtension("dubbo"); // 按需获取,O(1)
Dubbo SPI 三大扩展类型
less
┌──────────────────────────────────────────────────────────┐
│ Dubbo SPI 扩展类型 │
│ │
│ 1. 普通扩展 (普通 SPI) │
│ getExtension("dubbo") → 获取指定实现 │
│ 按配置文件 key 获取 │
│ │
│ 2. 自适应扩展 (@Adaptive) │
│ getAdaptiveExtension() → 动态选择实现 │
│ 根据运行时参数决定用哪个实现 │
│ 编译期生成代理类 │
│ │
│ 3. 激活扩展 (@Activate) │
│ getActivateExtension(url, key) → 批量获取 │
│ 根据条件激活多个实现 (类似责任链) │
│ 用于 Filter、Listener 等扩展点 │
│ │
└──────────────────────────────────────────────────────────┘
ExtensionLoader 核心原理
java
public class ExtensionLoader<T> {
private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
private static final String DUBBO_INTERNAL_DIRECTORY = "META-INF/dubbo/internal/";
private final Class<?> type; // 扩展接口类型
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
// 扩展名 → 扩展实例 (懒加载、缓存)
// 按名获取扩展
public T getExtension(String name) {
// 1. 从缓存获取
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 2. 创建扩展 (含 IOC + AOP)
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
private T createExtension(String name) {
// 1. 从配置文件加载 Class
Class<?> clazz = getExtensionClasses().get(name);
// 2. 反射实例化
T instance = (T) clazz.getDeclaredConstructor().newInstance();
// 3. IOC 注入 (注入其他扩展依赖)
instance = injectExtension(instance);
// 4. AOP 包装 (Wrapper 包装)
for (Class<?> wrapperClass : cachedWrapperClasses) {
instance = (T) wrapperClass
.getConstructor(type).newInstance(instance);
}
return instance;
}
}
Dubbo IOC 依赖注入
java
// Dubbo 的 setter 注入
private T injectExtension(T instance) {
for (Method method : instance.getClass().getMethods()) {
if (method.getName().startsWith("set")
&& method.getParameterCount() == 1
&& Modifier.isPublic(method.getModifiers())) {
Class<?> pt = method.getParameterTypes()[0];
// 判断参数类型是否是扩展点接口
if (pt.isInterface()
&& ExtensionLoader.getExtensionLoader(pt) != null) {
// 获取自适应扩展并注入
Object ext = ExtensionLoader
.getExtensionLoader(pt)
.getAdaptiveExtension();
method.invoke(instance, ext);
}
}
}
return instance;
}
Dubbo AOP Wrapper
java
// ProtocolFilterWrapper 包装 Protocol
public class ProtocolFilterWrapper implements Protocol {
private final Protocol protocol; // 被包装的真实实例
public ProtocolFilterWrapper(Protocol protocol) {
this.protocol = protocol; // 构造器注入
}
@Override
public <T> Exporter<T> export(Invoker<T> invoker) {
// 前置: 构建 Filter 责任链
invoker = buildInvokerChain(invoker);
// 委托给真实 Protocol
return protocol.export(invoker);
}
}
// 配置文件 META-INF/dubbo/org.apache.dubbo.rpc.Protocol
// filter=com.example.ProtocolFilterWrapper ← 被识别为 Wrapper
// (因为构造器参数是 Protocol 类型)
@Adaptive 自适应扩展
java
@Adaptive // 标注在方法上
public interface Protocol {
@Adaptive
<T> Exporter<T> export(Invoker<T> invoker);
}
// Dubbo 编译期生成自适应类:
public class Protocol$Adaptive implements Protocol {
public <T> Exporter<T> export(Invoker<T> invoker) {
// 1. 从 URL 中获取 protocol 参数
URL url = invoker.getUrl();
String extName = url.getProtocol(); // "dubbo" / "http" / ...
// 2. 按参数动态获取扩展
Protocol extension = ExtensionLoader
.getExtensionLoader(Protocol.class)
.getExtension(extName);
// 3. 委托执行
return extension.export(invoker);
}
}
// 运行时根据 URL 参数动态选择实现
@Activate 激活扩展
java
// Filter 链: 根据条件激活多个 Filter
@Activate(group = PROVIDER, order = 100)
public class ValidationFilter implements Filter { ... }
@Activate(group = CONSUMER, value = "cache")
public class CacheFilter implements Filter { ... }
// 获取激活的 Filter 链
List<Filter> filters = ExtensionLoader
.getExtensionLoader(Filter.class)
.getActivateExtension(url, "service.filter", CONSUMER);
五、SPI 与双亲委派
Java SPI 的 ServiceLoader 默认使用当前线程上下文类加载器:
java
public static <S> ServiceLoader<S> load(Class<S> service) {
// 用 Thread.currentThread().getContextClassLoader()
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
这打破双亲委派:核心类(BootstrapClassLoader 加载的 DriverManager)需要加载第三方实现(AppClassLoader 的 MySQL Driver),但核心类不能向下看到子类加载器的类。通过线程上下文类加载器解决了这个问题。
css
双亲委派与 SPI 的矛盾:
┌──────────────────────────────────────────────────────┐
│ │
│ BootstrapClassLoader (rt.jar) │
│ └── DriverManager.class (核心类) │
│ 需要加载: com.mysql.cj.jdbc.Driver │
│ 但 BootstrapClassLoader 看不到 AppClassLoader │
│ │
│ 解决: Thread.currentThread().getContextClassLoader()│
│ → AppClassLoader → 加载 MySQL Driver │
│ │
│ 这就是 "线程上下文类加载器" 的核心用途之一 │
│ │
└──────────────────────────────────────────────────────┘
六、SPI 的典型应用场景
| 场景 | 接口 | 实现 |
|---|---|---|
| JDBC | java.sql.Driver | MySQL/Oracle/PG 驱动 |
| 日志门面 | SLF4J LoggerFactory | Logback/Log4j2 |
| Spring Boot | EnableAutoConfiguration | spring.factories |
| Dubbo | Protocol/Filter/Serialization | 各 SPI 扩展 |
| Java NIO | CharsetProvider | UTF-8/GBK 等 |
Spring Boot 的 SPI:spring.factories
java
// Spring Boot 的自动配置
// META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.WebAutoConfiguration,\
com.example.DataSourceAutoConfiguration
// SpringFactoriesLoader (类似 ServiceLoader)
List<String> configs = SpringFactoriesLoader.loadFactoryNames(
EnableAutoConfiguration.class, classLoader);
七、面试高频问题速答
Q1: ServiceLoader 是饿汉还是懒汉加载?
懒汉。ServiceLoader.load() 只创建对象不加载实现类。在遍历迭代器时才逐个 Class.forName + newInstance。每个实现类首次被迭代到时才加载实例化。
Q2: Java SPI 和 Dubbo SPI 的核心区别?
Java SPI 遍历所有实现(无法按需),Dubbo SPI 用 key-value 配置按名获取。Dubbo SPI 还支持 IOC(setter 注入)、AOP(Wrapper 包装)、@Adaptive(运行时动态选择)和 @Activate(条件批量激活)。
Q3: Dubbo SPI 的 IOC 怎么实现的?
在 createExtension 中通过反射扫描 setter 方法,如果参数类型是扩展点接口,就获取自适应扩展注入。本质是 setter 注入,类似 Spring 的依赖注入但更轻量。
Q4: @Adaptive 自适应扩展解决什么问题?
在编码时不知道用哪个实现,需要根据运行时参数(URL 中的 key)动态选择。Dubbo 编译期生成代理类,从 URL 提取 key 再调用 getExtension(key) 获取真正实现。
Q5: SPI 的配置文件在哪?
Java SPI:META-INF/services/接口全限定名(内容是每行一个实现类全限定名)。Dubbo SPI:META-INF/dubbo/接口全限定名(内容是 key=value 格式)。Spring Boot:META-INF/spring.factories。
Q6: 为什么 SPI 用线程上下文类加载器?
核心类(如 DriverManager)由 BootstrapClassLoader 加载,需要加载第三方实现(如 MySQL Driver)在 AppClassLoader 中。父加载器看不到子加载器的类,用线程上下文类加载器(默认 AppClassLoader)解决,这是双亲委派的"后门"。
八、总结
java
SPI 核心知识:
┌──────────────────────────────────────────────────────┐
│ │
│ Java SPI (ServiceLoader): │
│ 配置: META-INF/services/接口名 │
│ 加载: 懒加载,遍历时实例化 │
│ 缺点: 全加载、无IOC/AOP、无按名获取 │
│ │
│ Dubbo SPI (ExtensionLoader): │
│ 配置: META-INF/dubbo/接口名 (key=value) │
│ 加载: 按需 O(1) 获取 │
│ IOC: setter 注入扩展依赖 │
│ AOP: Wrapper 构造器包装 │
│ @Adaptive: URL 参数动态选择 │
│ @Activate: 条件批量激活 │
│ │
│ 设计价值: │
│ 接口与实现分离 → 可插拔扩展 │
│ 新增实现零代码修改 → 只需加jar+配置 │
│ 符合开闭原则 │
│ │
│ 类加载: │
│ ServiceLoader 用线程上下文类加载器 │
│ 解决核心类加载子类加载器类的问题 │
│ │
└──────────────────────────────────────────────────────┘
SPI 是 Java 解耦和可扩展设计的经典模式。Java SPI 简单但功能有限,Dubbo SPI 在其基础上增加了 IOC/AOP/自适应/激活等增强,是生产级 SPI 的优秀实践。理解 SPI 对于阅读 Dubbo、Spring Boot 自动配置等框架源码至关重要。