问题

什么是SPI,和API有啥区别?

答案

核心概念

API(Application Programming Interface):应用程序编程接口,由服务提供方定义接口并提供实现,调用方直接使用。

SPI(Service Provider Interface):服务提供接口,由服务提供方定义接口规范,第三方提供具体实现,通过服务发现机制动态加载。

本质区别

维度 API SPI
调用方向 调用方 → 实现方 实现方 → 框架
实现者 接口定义者提供实现 第三方提供实现
耦合度 强耦合(直接引用) 解耦(配置驱动)
典型场景 工具类库、SDK 插件机制、驱动加载

形象比喻

  • API:餐厅提供菜单(接口)和厨师(实现),顾客直接点菜
  • SPI:餐厅提供菜单(接口)和厨房(框架),不同厨师(实现)可以应聘入职

SPI实现原理

1. Java标准SPI机制

核心类java.util.ServiceLoader

实现步骤

// 1. 定义服务接口
package com.example.spi;

public interface Database {
    void connect(String url);
}
// 2. 提供实现(MySQL实现)
package com.example.mysql;

public class MySQLDatabase implements Database {
    @Override
    public void connect(String url) {
        System.out.println("连接MySQL: " + url);
    }
}
// 3. 配置SPI文件
// 文件路径:META-INF/services/com.example.spi.Database
// 文件内容:
com.example.mysql.MySQLDatabase
com.example.postgresql.PostgreSQLDatabase
// 4. 使用ServiceLoader加载
ServiceLoader<Database> loader = ServiceLoader.load(Database.class);
for (Database db : loader) {
    db.connect("jdbc:...");
}

2. SPI加载流程

// ServiceLoader源码简化版
public final class ServiceLoader<S> implements Iterable<S> {
    private static final String PREFIX = "META-INF/services/";

    public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return new ServiceLoader<>(service, cl);
    }

    private Iterator<S> iterator() {
        // 1. 读取 META-INF/services/{接口全限定名} 文件
        String fullName = PREFIX + service.getName();
        Enumeration<URL> configs = loader.getResources(fullName);

        // 2. 解析文件内容,获取实现类名
        List<String> names = parse(configs);

        // 3. 通过反射实例化实现类
        for (String name : names) {
            Class<?> c = Class.forName(name, false, loader);
            S p = service.cast(c.newInstance());
            providers.add(p);
        }
    }
}

典型应用场景

1. JDBC驱动加载

// 无需显式加载驱动类
// DriverManager通过SPI自动发现所有数据库驱动
Connection conn = DriverManager.getConnection(
    "jdbc:mysql://localhost:3306/db", "user", "password"
);

// MySQL驱动的SPI配置
// META-INF/services/java.sql.Driver
// com.mysql.cj.jdbc.Driver

2. Servlet容器

// Servlet 3.0+ 使用SPI自动注册ServletContainerInitializer
// META-INF/services/javax.servlet.ServletContainerInitializer
// org.springframework.web.SpringServletContainerInitializer

3. SLF4J日志门面

// SLF4J通过SPI绑定具体日志实现(Logback、Log4j2等)
Logger logger = LoggerFactory.getLogger(MyClass.class);
logger.info("日志输出"); // 自动路由到具体实现

4. Dubbo扩展机制

Dubbo改进了Java SPI,支持按需加载:

// Dubbo SPI配置
// META-INF/dubbo/com.alibaba.dubbo.rpc.Protocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
http=com.alibaba.dubbo.rpc.protocol.http.HttpProtocol

// 使用时按需加载
Protocol protocol = ExtensionLoader
    .getExtensionLoader(Protocol.class)
    .getExtension("dubbo");

SPI的优缺点

优点

  1. 解耦:接口与实现分离,支持插件化开发
  2. 扩展性强:新增实现无需修改原有代码
  3. 标准化:遵循统一的服务发现规范

缺点

  1. 性能开销:使用反射和IO操作,性能较低
  2. 加载全部实现:Java SPI会实例化所有实现类(Dubbo SPI优化了这点)
  3. 线程安全:ServiceLoader不是线程安全的
  4. 缺少依赖注入:无法注入Spring等容器管理的Bean

实战示例

支付网关插件化设计

// 1. 定义支付接口
public interface PaymentService {
    void pay(BigDecimal amount);
}

// 2. 支付宝实现
public class AlipayService implements PaymentService {
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("支付宝支付: " + amount);
    }
}

// 3. 微信实现
public class WechatPayService implements PaymentService {
    @Override
    public void pay(BigDecimal amount) {
        System.out.println("微信支付: " + amount);
    }
}

// 4. 配置文件
// META-INF/services/com.example.PaymentService
// com.example.AlipayService
// com.example.WechatPayService

// 5. 动态选择支付方式
public class PaymentFactory {
    private static final Map<String, PaymentService> services = new HashMap<>();

    static {
        ServiceLoader.load(PaymentService.class).forEach(service -> {
            services.put(service.getClass().getSimpleName(), service);
        });
    }

    public static PaymentService getService(String name) {
        return services.get(name);
    }
}

// 使用
PaymentService payment = PaymentFactory.getService("AlipayService");
payment.pay(new BigDecimal("100.00"));

答题总结

API 是服务方定义接口并提供实现,调用方直接使用;SPI 是服务方定义接口规范,第三方提供实现,通过 ServiceLoader 动态加载。SPI通过 META-INF/services/ 配置文件实现服务发现,典型应用包括 JDBC驱动加载、SLF4J日志绑定、Dubbo扩展机制 等。SPI的核心优势是解耦和扩展性,适合插件化架构设计。