Java SPI 到底怎么实现动态扩展的?从 ServiceLoader 到 Dubbo SPI 全链路拆解

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 自动配置等框架源码至关重要。

相关推荐
新时代农民工~1 小时前
【双机高可用部署方案-前后端部署】
java·nginx·springboot
企业数字化笔记1 小时前
固定资产还有借用记录能报废吗?Java前置检查、停止折旧与SQL验收
java·开发语言·sql
飞虹地星海1 小时前
从零到一跑通苍穹外卖:一个大二学生的暑假项目复盘
java
她的男孩1 小时前
账号锁定配了"错 4 次锁 30 分钟",我连错 100 次一次没锁上:扒完 4091 行认证源码,找到 5 个坑
java·后端·架构
程序猿乐锅1 小时前
【黑马点评 | 第二篇】Redis 缓存更新策略与商铺缓存实现
java·网络·redis·spring·mybatis
livemetee1 小时前
MySQL联合唯一索引(code,deleted)与逻辑删除问题
java·数据库
MacroZheng1 小时前
Redis官方发布高颜值可视化工具,功能更是强的离谱!
java·redis·后端
wuminyu1 小时前
纯轻量级锁体系下C2编译器锁粗化和消除机制剖析
java·linux·c语言·jvm·c++
ss2731 小时前
AI全栈实战 | 1.4-02 Spring AOP:@Transactional 失效的 N 种场景,根因其实只有一个
java·spring·ai编程