1. 概述

Java 6 引入了一个非常实用的机制:Service Provider Interface(SPI),用于发现并动态加载符合特定接口的实现类。

简单来说,SPI 是一种“服务发现”机制。它允许我们在不修改核心代码的前提下,通过配置方式动态替换或扩展功能模块。这在框架设计中极为常见,比如 JDBC 驱动自动加载、日志框架切换等。

本文将带你深入理解 SPI 的核心组件,并通过一个汇率查询系统的实战案例,手把手演示如何构建可插拔的模块化应用。


2. Java SPI 的核心概念

SPI 并不是一个复杂的框架,而是一套约定俗成的设计模式 + JDK 原生支持。它由四个关键部分组成:

2.1. Service(服务)

指一组对外暴露的功能接口或类集合,代表某种能力,例如“获取汇率”、“格式化日期”等。

✅ 可以理解为“我要做什么”。

2.2. Service Provider Interface(SPI,服务提供者接口)

这是服务的具体契约,通常是一个接口或抽象类,供第三方实现。

⚠️ 注意:SPI 不等于 API。API 是你调用别人的功能,SPI 是别人实现你的契约。

举个例子:

  • java.sql.Driver 是 JDBC 的 SPI,各大数据库厂商(MySQL、Oracle)都要实现它。
  • Connection.createStatement() 这种方法才是 API。

✅ 简单粗暴地说:API 是用来用的,SPI 是用来实现的

2.3. Service Provider(服务提供者)

即 SPI 的具体实现类。比如我们写一个 YahooFinanceExchangeRateProvider 来实现汇率获取逻辑。

为了让 JVM 能自动发现这个实现,必须在 META-INF/services/ 目录下创建一个配置文件:

META-INF/services/com.baeldung.rate.spi.ExchangeRateProvider

文件内容是实现类的全限定名:

com.baeldung.rate.impl.YahooFinanceExchangeRateProvider

这个文件就是“注册表”,告诉 ServiceLoader:“我有一个实现,请加载我”。

2.4. ServiceLoader(服务加载器)

这是 SPI 的核心引擎类,位于 java.util.ServiceLoader

它的职责非常明确:

  • ✅ 扫描 classpath 下所有 META-INF/services/ 中的配置
  • ✅ 加载符合条件的实现类
  • ✅ 支持懒加载和重新加载(reload()
ServiceLoader<ExchangeRateProvider> loader = ServiceLoader.load(ExchangeRateProvider.class);
loader.iterator().forEachRemaining(provider -> {
    QuoteManager manager = provider.create();
    // 使用 manager 查询汇率
});

ServiceLoader 是线程安全的,且会缓存已加载的实例,性能友好。


3. SPI 与 API 的本质区别

很多开发者容易混淆 SPI 和 API,其实它们的角色完全不同:

对比项 API SPI
使用者 应用开发者 框架/平台开发者
目的 调用已有功能 扩展新功能
示例 String.toUpperCase() 实现 Driver 接口
类比 浏览器的“前进”、“刷新”按钮 浏览器插件(如广告拦截)

常见的 Java SPI 实例

JDK 和主流框架广泛使用 SPI,以下是一些典型例子:

  • CurrencyNameProvider:提供本地化的货币符号
  • LocaleNameProvider:提供本地化的区域名称
  • DateFormatProvider:自定义日期格式
  • Driver(JDBC):数据库驱动注册(从 JDK 4 开始无需 Class.forName()
  • PersistenceProvider(JPA):Hibernate、EclipseLink 的接入点
  • JsonProvider(JSON-P):JSON 处理引擎
  • ConfigSourceProvider(MicroProfile Config):自定义配置源

这些机制让你可以在不改框架代码的情况下,自由替换底层实现。


4. 实战案例:构建可插拔的汇率系统

我们通过一个真实的场景来演示 SPI 的完整用法:一个支持多数据源的汇率查询系统

系统结构包含三个模块:

exchange-rate-api        ← 定义接口(SPI)
├── exchange-rate-impl   ← 实现 SPI(如 Yahoo Finance)
└── exchange-rate-app    ← 主程序,使用 SPI

4.1. 定义 API 与 SPI

创建 exchange-rate-api 模块,定义核心模型和接口。

汇率数据模型:

package com.baeldung.rate.api;

import java.math.BigDecimal;
import java.time.LocalDate;

public class Quote {
    private String currency;
    private BigDecimal ask;
    private BigDecimal bid;
    private LocalDate date;

    // 构造器、getter、setter 省略
}

服务接口(Service):

package com.baeldung.rate.api;

import java.time.LocalDate;
import java.util.List;

public interface QuoteManager {
    List<Quote> getQuotes(String baseCurrency, LocalDate date);
}

SPI 接口(供实现方提供):

package com.baeldung.rate.spi;

import com.baeldung.rate.api.QuoteManager;

public interface ExchangeRateProvider {
    QuoteManager create();
}

工具类(封装 ServiceLoader):

package com.baeldung.rate.spi;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;

public final class ExchangeRate {

    private static final String DEFAULT_PROVIDER = "com.baeldung.rate.impl.YahooFinanceExchangeRateProvider";

    // 获取所有已注册的 provider
    public static List<ExchangeRateProvider> providers() {
        List<ExchangeRateProvider> services = new ArrayList<>();
        ServiceLoader<ExchangeRateProvider> loader = ServiceLoader.load(ExchangeRateProvider.class);
        loader.forEach(services::add);
        return services;
    }

    // 获取默认 provider
    public static ExchangeRateProvider provider() {
        return provider(DEFAULT_PROVIDER);
    }

    // 根据类名获取指定 provider
    public static ExchangeRateProvider provider(String providerName) {
        ServiceLoader<ExchangeRateProvider> loader = ServiceLoader.load(ExchangeRateProvider.class);
        Iterator<ExchangeRateProvider> it = loader.iterator();
        while (it.hasNext()) {
            ExchangeRateProvider provider = it.next();
            if (providerName.equals(provider.getClass().getName())) {
                return provider;
            }
        }
        throw new ProviderNotFoundException("Exchange Rate provider " + providerName + " not found");
    }
}

// 自定义异常
class ProviderNotFoundException extends RuntimeException {
    public ProviderNotFoundException(String message) {
        super(message);
    }
}

⚠️ 注意:这个工具类不是必须的,客户端也可以直接使用 ServiceLoader,但封装后更易用。


4.2. 实现服务提供者

创建 exchange-rate-impl 模块,引入 exchange-rate-api 依赖:

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>exchange-rate-api</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

实现 SPI 接口:

package com.baeldung.rate.impl;

import com.baeldung.rate.api.Quote;
import com.baeldung.rate.api.QuoteManager;
import com.baeldung.rate.spi.ExchangeRateProvider;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;

public class YahooFinanceExchangeRateProvider implements ExchangeRateProvider {

    @Override
    public QuoteManager create() {
        return new YahooQuoteManagerImpl();
    }

    // 模拟实现
    private static class YahooQuoteManagerImpl implements QuoteManager {
        @Override
        public List<Quote> getQuotes(String baseCurrency, LocalDate date) {
            return Arrays.asList(
                new Quote("USD", new BigDecimal("7.21"), new BigDecimal("7.19"), date),
                new Quote("EUR", new BigDecimal("7.85"), new BigDecimal("7.83"), date)
            );
        }
    }
}

创建 SPI 配置文件:

META-INF/services/com.baeldung.rate.spi.ExchangeRateProvider

文件内容:

com.baeldung.rate.impl.YahooFinanceExchangeRateProvider

✅ 这一步是关键!没有这个文件,ServiceLoader 就找不到你的实现。


4.3. 主程序调用 SPI

创建 exchange-rate-app 模块,引入 API 依赖:

<dependency>
    <groupId>com.baeldung</groupId>
    <artifactId>exchange-rate-api</artifactId>
    <version>1.0.0-SNAPSHOT</version>
</dependency>

编写主类:

package com.baeldung.rate.app;

import com.baeldung.rate.api.Quote;
import com.baeldung.rate.api.QuoteManager;
import com.baeldung.rate.spi.ExchangeRate;

import java.time.LocalDate;
import java.util.List;

public class MainApp {
    public static void main(String[] args) {
        // 获取所有 provider
        List<com.baeldung.rate.spi.ExchangeRateProvider> providers = ExchangeRate.providers();
        
        if (providers.isEmpty()) {
            System.err.println("No exchange rate provider found!");
            return;
        }

        providers.forEach(provider -> {
            System.out.println("Using provider: " + provider.getClass().getSimpleName());
            QuoteManager manager = provider.create();
            List<Quote> quotes = manager.getQuotes("CNY", LocalDate.now());
            quotes.forEach(quote -> 
                System.out.println(quote.getCurrency() + ": " + quote.getAsk())
            );
        });
    }
}

4.4. 编译与运行

先打包所有模块:

mvn clean package

运行主程序,注意将所有 JAR 加入 classpath:

Linux/macOS:

java -cp \
./exchange-rate-api/target/exchange-rate-api-1.0.0-SNAPSHOT.jar:\
./exchange-rate-app/target/exchange-rate-app-1.0.0-SNAPSHOT.jar:\
./exchange-rate-impl/target/exchange-rate-impl-1.0.0-SNAPSHOT.jar \
com.baeldung.rate.app.MainApp

Windows:

java -cp ".\exchange-rate-api\target\exchange-rate-api-1.0.0-SNAPSHOT.jar;.\exchange-rate-app\target\exchange-rate-app-1.0.0-SNAPSHOT.jar;.\exchange-rate-impl\target\exchange-rate-impl-1.0.0-SNAPSHOT.jar" com.baeldung.rate.app.MainApp

预期输出:

Using provider: YahooFinanceExchangeRateProvider
USD: 7.21
EUR: 7.85

❌ 如果输出为空,检查:

  • META-INF/services/ 文件是否存在
  • 文件名和内容是否完全匹配
  • JAR 是否正确打包进 classpath

5. 总结

Java SPI 是一种轻量级但极其强大的解耦设计模式,适用于以下场景:

✅ 插件化架构(如 IDE 插件、浏览器扩展)
✅ 多实现切换(如日志框架 SLF4J、序列化框架 Jackson/Fastjson)
✅ 框架扩展点(如 Spring Factories、Dubbo Protocol)

关键要点回顾:

  • ✅ SPI 的核心是 ServiceLoader + META-INF/services/
  • ✅ 实现类必须通过配置文件注册
  • ✅ 支持运行时动态发现,无需硬编码
  • ✅ 适合构建高内聚、低耦合的模块化系统

踩坑提醒:

  • ❌ 文件名必须是 SPI 接口的全限定名
  • ❌ 文件内容必须是实现类的全限定名,不能有空格或注释
  • ❌ 多个实现时,加载顺序不确定,不要依赖顺序
  • ServiceLoader 默认使用上下文类加载器,注意类加载器隔离问题

掌握 SPI,你就掌握了 Java 框架设计的“任督二脉”。无论是阅读源码还是设计系统,都能游刃有余。


原始标题:Java Service Provider Interface | Baeldung

« 上一篇: AutoFactory 介绍
» 下一篇: Java每周,问题227