Java SPI 机制

一、什么是 SPI?

Java SPI(Service Provider Interface)是 Java 官方为 实现解耦、实现可插拔组件 而提供的一套扩展机制。其核心思想:

接口由框架定义,实现由第三方提供,运行时动态加载指定实现。

SPI 主要用于:

  • JDBC Driver 动态加载
  • 日志框架 SLF4J 动态绑定
  • Dubbo/Spring 等框架插件加载
  • 加密算法、序列化算法的动态选择

SPI 在 JDK 中由 java.util.ServiceLoader 实现。


二、SPI 的核心原理

SPI 工作流程:

  1. 接口定义(服务接口)
  2. 在 META-INF/services 目录下创建配置文件
    • 文件名:接口全限定类名
    • 内容:实现类的全限定名(可多行)
  3. 使用 ServiceLoader 加载
    • 读取 META-INF/services 文件
    • 实例化实现类
  4. 按需选择实现执行

ServiceLoader 实现了按需加载、迭代查找和缓存机制。


三、SPI 机制完整示例

下面我们写一个从零开始的完整案例。

1. 定义 SPI 接口

public interface HelloService {
    String sayHello(String name);
}

2. 创建两个实现类

public class EnglishHelloService implements HelloService {
    @Override
    public String sayHello(String name) {
        return "Hello, " + name;
    }
}
public class ChineseHelloService implements HelloService {
    @Override
    public String sayHello(String name) {
        return "你好," + name;
    }
}

3. 在 META-INF/services 添加配置

目录结构:

resources
 └── META-INF
     └── services
         └── com.example.spi.HelloService

文件内容:

com.example.spi.EnglishHelloService
com.example.spi.ChineseHelloService

4. 使用 ServiceLoader 加载 SPI

public class SpiDemo {
    public static void main(String[] args) {
        ServiceLoader<HelloService> services = ServiceLoader.load(HelloService.class);
        for (HelloService service : services) {
            System.out.println(service.sayHello("Java"));
        }
    }
}

运行输出:

Hello, Java
你好,Java

5. 按名称选择实现(扩展)

项目中通常需要根据外部配置选择实现,例如 application.properties 中:

hello.lang=cn

实现:

public class HelloServiceFactory {

    private static final Map<String, HelloService> SERVICE_MAP = new HashMap<>();

    static {
        ServiceLoader<HelloService> services = ServiceLoader.load(HelloService.class);
        for (HelloService service : services) {
            SERVICE_MAP.put(service.getClass().getSimpleName(), service);
        }
    }

    public static HelloService get(String key) {
        return SERVICE_MAP.get(key);
    }
}

使用:

HelloService service = HelloServiceFactory.get("ChineseHelloService");
System.out.println(service.sayHello("世界"));

四、Java SPI 的优缺点

优点

  • 去掉代码硬编码依赖
  • 易扩展、可插拔
  • 框架化开发常用,约定优于配置
  • 允许第三方自动接入

缺点

  • 加载机制比较原始:全部加载、无法按需筛选
  • 只依赖文件机制,不够灵活
  • 无优先级管理
  • 对类加载器、JDK 模块化不友好

为此,很多框架(如 Dubbo、Spring)扩展了自己的 SPI 体系。


五、如何把现有项目改造成 SPI 机制(最佳实践)

以下为真实项目改造步骤(可直接应用在微服务、工具包、平台 SDK 中)。

📌 目标

把“写死的实现类”改造成“可插拔的扩展点”。

例如:

OrderValidator validator = new NormalOrderValidator();  // 强耦合!!

改造成 SPI:

OrderValidator validator = ServiceLoader.load(OrderValidator.class).iterator().next();

Step 1:抽象接口(定义扩展点)

把具体逻辑改成接口:

public interface OrderValidator {
    boolean validate(Order order);
}

Step 2:拆分模块(推荐放到 API 模块)

创建两个模块:

order-api      ---> 存放接口和 SPI 配置
order-impl     ---> 一个或多个实现

其中 order-api 中包含:

  • OrderValidator 接口
  • META-INF/services 配置

Step 3:在实现模块新增 SPI 实现

例如:

public class NormalOrderValidator implements OrderValidator {
    @Override
    public boolean validate(Order order) {
        return order.getAmount() > 0;
    }
}

并在:

order-api/resources/META-INF/services/com.demo.OrderValidator

写入:

com.demo.impl.NormalOrderValidator

Step 4:使用 ServiceLoader 动态加载

封装一个工厂:

public class OrderValidatorFactory {

    private static volatile OrderValidator instance;

    public static OrderValidator get() {
        if (instance == null) {
            synchronized (OrderValidatorFactory.class) {
                if (instance == null) {
                    ServiceLoader<OrderValidator> loader = ServiceLoader.load(OrderValidator.class);
                    instance = loader.iterator().next();
                }
            }
        }
        return instance;
    }
}

业务代码:

OrderValidator validator = OrderValidatorFactory.get();
validator.validate(order);

Step 5:支持多个插件并按需选择

SPI 支持多实现,但需要我们自己区分。

最佳做法:给实现类加 @SPIName 注解

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPIName {
    String value();
}

实现类:

@SPIName("normal")
public class NormalOrderValidator implements OrderValidator {...}

工厂类升级:

public class OrderValidatorFactory {

    private static final Map<String, OrderValidator> CACHE = new HashMap<>();

    static {
        ServiceLoader<OrderValidator> loader = ServiceLoader.load(OrderValidator.class);
        for (OrderValidator validator : loader) {
            SPIName name = validator.getClass().getAnnotation(SPIName.class);
            CACHE.put(name.value(), validator);
        }
    }

    public static OrderValidator get(String type) {
        return CACHE.get(type);
    }
}

这样项目就完全模块化、插件化了。


六、改造项目为 SPI 的最佳实战建议

1. 扩展点接口一定要放在独立模块(API 模块)

避免实现模块互相依赖。

2. 不要在实现模块创建 META-INF/services

应该放在接口所在的模块(API 模块)。

3. 建议为每个实现增加唯一 ID 注解(如 Dubbo 的 @SPI)

否则 SPI 无法支持多实现选择。

4. 尽量封装自己的 ExtensionLoader 工具类

JDK 的 ServiceLoader 太原始,需要补充:

  • 缓存
  • 日志
  • 异常提示
  • 默认实现
  • 选用策略

5. 插件机制尽量支持热插拔(JAR 放进去即可生效)

这非常适合:

  • 数据质量规则扩展
  • 支付方式扩展
  • 认证方式扩展
  • 通知渠道扩展(短信/邮件/钉钉)

七、总结

Java SPI 是最轻量、最标准、最易维护的插件化方案,可以让你的项目:

  • 实现完全解耦
  • 支持多个实现动态接入
  • 运行时根据配置选择实现
  • 像 JDBC 驱动一样自动发现插件

八 、SPI 与 Spring @Conditional 条件装配对比与注意事项

一、SPI 与 @Conditional 的本质区别

  • SPI(Service Provider Interface) 是 JDK 原生提供的动态扩展机制,通过扫描 META-INF/services 来加载实现类,适合构建插件体系。
  • @Conditional(Spring 条件装配) 是 Spring 容器级别的 Bean 装配控制,用于根据环境、配置、依赖条件来决定是否注册某个 Bean。
功能对比
维度Java SPISpring @Conditional
效果发现第三方扩展、插件化机制根据条件控制 Bean 装配
场景框架扩展点、插件发现(如 JDBC、Dubbo)Spring Boot 自动配置、环境差异 Bean
加载方式扫描并实例化所有实现类满足条件的 Bean 才会生成
可插拔程度高,真正可热插拔(加 JAR 即可)中,对应用内部的组件选择

二、SPI 的注意事项(重点)

SPI 默认会实例化所有实现类,这是使用时必须注意的最大坑。

1. 全量加载问题

只要通过 ServiceLoader 遍历扩展,所有插件都会被实例化,包括:

  • 调用构造函数
  • 执行 static 静态代码块
  • 执行任何副作用逻辑(如连接服务器、加载配置)
2. 可能导致的问题
  • 启动慢
  • 不使用的插件仍然执行初始化
  • 插件初始化相互影响(如 AD、OAuth 都被初始化)
  • 插件报错导致整个系统启动失败
3. 建议的安全使用方式(增强版 SPI)
  • 不要在构造函数、static 块执行副作用逻辑
  • 扫描 SPI 时只登记插件,不实例化插件
  • 通过插件工厂按需实例化
for (AuthService s : loader) {
    registry.put(s.getAuthType(), s.getClass());
}
  • 延迟加载(Lazy Initialization)
  • 为插件定义显式的 init() 生命周期,避免构造时执行初始化

三、在插件化系统中的推荐组合

在企业级工程中:

  • SPI 做插件发现(扩展点),支持第三方丢 JAR 即可接入
  • @Conditional 配置插件的启用策略,例如:
@ConditionalOnProperty(name = "auth.type", havingValue = "ad")

这样既可以自动发现插件,也可通过配置和环境控制具体使用哪一个,实现“插件化 + Spring Boot 条件装配”的最佳结合方案。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐