杂谈 : Java SPI

1,770 阅读5分钟

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜
文章合集 : 🎁 juejin.cn/post/694164…
Github : 👉 github.com/black-ant
CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

虽然是一个老概念 , 但是都怪自己的轻微强迫症 , 有个点没搞清楚 , 就非要把这个概念翻出来看看, 这一篇是对 Java SPI 概念的一个完善 , 为后续的 Dubbo 等框架的分析做准备

Java SPI 是 JDK提供的SPI(Service Provider Interface)机制 , SPI 机制的核心在于 ServiceLoader , 用于加载接口对应的实现类

用一句话来解释 , 就是以 Java 的方式查找接口对应的实现类 , 实现解耦 (区别于 Spring 里面的Bean工具 , SPI 的方式更加底层)

二 . 知识点

SPI 机制中有四个组成部分 :

  • Service Providers : 服务器供应商
    • Installing Service Providers : 安装服务供应商
    • Loading Service providers : 装载服务供应商
  • Service Loader : 服务承载程式

Service Provider : 服务提供者 Service Provider 是 SPI 的特定实现。服务提供者包含一个或多个实现或扩展服务类型的具体类 , 服务提供者是通过我们放在资源目录 META-INF/services 中的提供者配置文件来配置和标识的

ServiceLoader ServiceLoader 是 SPI 的核心类 , 它的作用是发现和惰性加载实现 , 它使用上下文类路径来定位提供程序实现并将它们放在内部缓存中。

常用的 SPI 类

  • CurrencyNameProvider : 为currency类提供本地化的货币符号
  • LocaleNameProvider : 为Locale类提供本地化名称
  • TimeZoneNameProvider : 为TimeZone类提供本地化的时区名称
  • DateFormatProvider : 提供指定区域的日期和时间格式
  • NumberFormatProvider : 为NumberFormat类提供货币值、整数和百分比值.
  • Driver : 从4.0版本开始,JDBC API支持SPI模式
  • PersistenceProvider : 提供JPA API的实现。
  • JsonProvider : 提供JSON处理对象
  • JsonbProvider : 提供JSON绑定对象
  • Extension : 为CDI容器提供扩展
  • ConfigSourceProvider : 提供用于检索配置属性的源

三 . SPI 的使用

SPI 的使用中我分成了三个包 :

// provider-api : API 描述包 , 包含 Provider 接口 
    |- ExchangeRateProvider : Provider 接口
    |- QuoteManager : 一个需要我们通过 SPI 构建的业务接口
    |- ProviderManager : Provider 管理器 , 加载 Provider 实现类
    
//provider-impl : 接口实现类 , 也是我们最终需要 Loader 出的类
    |- YahooFinanceExchangeRateProvider
    |- YahooQuoteManagerImpl 

//server-application : 业务包 , 业务处理 , 获得 API 类

3.1 provider-api 包

// Step 1 : Provider 接口 , 我们用它返回一个通用的业务类
public interface ExchangeRateProvider {
    QuoteManager create();
}

//  Step 2 : 这个是我们的业务类
public interface QuoteManager {
    List<String> getQuotes(String baseCurrency, LocalDate date);
}

//  Step 3 : Provider 管理工具 , 加载出 Provider
public class ProviderManager {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    public Iterator<ExchangeRateProvider> providers(boolean refresh) {
        logger.info("------> [Step 1 : 进入 Provider 处理流程] <-------");
        ServiceLoader<ExchangeRateProvider> loader = ServiceLoader.load(ExchangeRateProvider.class);

        if (refresh) {
            loader.reload();
        }
        Iterator<ExchangeRateProvider> provider = loader.iterator();


        return provider;

    }


}

3.2 provider-impl

// 实现类
public class YahooFinanceExchangeRateProvider implements ExchangeRateProvider {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public QuoteManager create() {
        logger.info("------> this is create <-------");
        return new YahooQuoteManagerImpl();
    }
}

public class YahooQuoteManagerImpl implements QuoteManager {

    @Override
    public List<String> getQuotes(String baseCurrency, LocalDate date) {
        return new ArrayList<>();
    }
}

3.3 Server applicaiton

public class StartService implements ApplicationRunner {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    @Override
    public void run(ApplicationArguments args) throws Exception {
        logger.info("------> [App 中获取]  <-------");

        ProviderManager providerManager = new ProviderManager();

        Iterator<ExchangeRateProvider> providers = providerManager.providers(true);


        while (providers.hasNext()) {
            logger.info("------> [providers 获取完成 :{}] <-------", providers.next().create());
        }

    }
}

PS : 这里有个很重要的东西 , 你需要 META-INF/services 中添加对应的文件

  • 文件名 : Provider 接口
  • 文件内容 : 涉及到的实现类

image.png

这个文件放在 impl 和 app 中都行 , 实际上引包了就会扫描

<dependency>
    <groupId>com.gang.study</groupId>
    <artifactId>provider-api</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

<dependency>
    <groupId>com.gang.study</groupId>
    <artifactId>provider-impl</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

PS : 内容已提交到 Git , 欢迎 Star !!!! 👉 Case/java/spi

四 . SPI 源码深入

如果就这么结束当然不符合我一贯的做法 , 源码还是要看一下的, 别说 , 还真有一些启发

4.1 运行的走向

// Step 1 : 发起 Providers 加载操作
Iterator<ExchangeRateProvider> providers = providerManager.providers(true);
  
// Step 2 : ServiceLoader 执行加载
ServiceLoader<ExchangeRateProvider> loader = ServiceLoader.load(ExchangeRateProvider.class);

// Step 3 : ServiceLoader 构造器
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

// Step 4 : reload 加载
public void reload() {
    providers.clear();
    lookupIterator = new LazyIterator(service, loader);
}

4.2 属性

// 默认从  META-INF/services/ 路径下加载
private static final String PREFIX = "META-INF/services/";

// 表示正在加载的服务的类或接口
private final Class<S> service;

// 类加载器用于定位、加载和实例化提供程序
private final ClassLoader loader;

// 创建ServiceLoader时获取的访问控制上下文
private final AccessControlContext acc;

// 缓存的提供商,按实例化顺序
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

// 惰性查找迭代器 , 最终获取会通过这个定制的迭代器
private LazyIterator lookupIterator;

4.3 resource 的加载

这里的核心是做了一个定制的实现类 LazyIterator

前面看了 ServiceLoader 的构建 , 这里来看一下 resource 的加载

// 这里不需要深入太多 , 主要是资源加载处理 Service 类
    
C- ServiceLoader
public Iterator<S> iterator() {

    // 核心一 : 对迭代器做了简单的实现 , 用来调用定制的迭代器 LazyIterator
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();

        public boolean hasNext() {
            if (knownProviders.hasNext()){
                return true;
            }    
            // --> 最终调用 hasNextService()
            return lookupIterator.hasNext();
        }

        public S next() {
            if (knownProviders.hasNext()){
                return knownProviders.next().getValue();
            }
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

// 判断是否存在下一个
private boolean hasNextService() {
    if (nextName != null) {
                return true;
    }
    if (configs == null) {
        try {
            // META-INF/services/java.util.spi.ResourceBundleControlProvider
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                // 通过 classLoader 加载 resource
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    // 获得实现类的名称 com.gang.spi.demo.service.YahooFinanceExchangeRateProvider
    nextName = pending.next();
    return true;
}

// 读取 Resource 中资源
private Iterator<String> parse(Class<?> service, URL u)throws ServiceConfigurationError{
        InputStream in = null;
        BufferedReader r = null;
     	// 加载 SPI impl l
        ArrayList<String> names = new ArrayList<>();
        try {
            in = u.openStream();
            r = new BufferedReader(new InputStreamReader(in, "utf-8"));
            int lc = 1;
            // Stream 逐行加载
            while ((lc = parseLine(service, u, r, lc, names)) >= 0);
        } catch (IOException x) {
            fail(service, "Error reading configuration file", x);
        } finally {
			// .... 省略 close
        }
        return names.iterator();
}

4.4 load 处理加载 Class

// 迭代器迭代
public S next() {
    if (knownProviders.hasNext())
        return knownProviders.next().getValue();
    return lookupIterator.next();
}

// 实例化 ProviderImpl
private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        // 通过 cn 获取对象的 class 类
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        // new ServiceConfigurationError(service.getName() + ": " + msg)
        fail(service,"Provider " + cn + " not found");
    }
    if (!service.isAssignableFrom(c)) {
        fail(service,"Provider " + cn  + " not a subtype");
    }
    try {
        // 实例化 Service : com.gang.spi.api.service.ExchangeRateProvider
        S p = service.cast(c.newInstance());
        // cn : com.gang.spi.demo.service.YahooFinanceExchangeRateProvider
        // p : com.gang.spi.demo.service.YahooFinanceExchangeRateProvider
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,"Provider " + cn + " could not be instantiated",x);
    }
    throw new Error();          // This cannot happen
}

PS: 核心就2个 ,一个是hasNext 中加载 resource , 再在 nextService 中实例化对应的server

这样的好处是 , 只有在正在使用的时候 , 才会真的去实例化这个对象 !!!

五 . 定制与比较

这里主要对比 Spring SPI 的加载方式 , 详见这一篇 盘点 SpringBoot : Factories 处理流程

文件的配置方面 : Spring 中使用的是 SpringFactoriesLoader , 其实与 Java 的方式是很像的 , 但是 Spring 的模式下 , 允许一个 factories 文件装载更多的类 , 使用更加简单 .


资源的加载方法 : 2 者都是通过 classLoader 加载 Resource , 并没有太大本质的区别


而资源的实例化方面 : Spring 通过一个 instantiateFactory 方法触发 ,但是同样的 , 也是 class 反射的原理


高并发情况 : 在调用 hasNext 的时候 , 加载 resource , 在迭代时才实例化 看起来好像没有什么问题 , 但是其 classLoader , provider 都是放在 ServiceLoader 对象属性中 , 多线程情况下会存在冲突

而 Spring 的模式 , 在 Server 启动时 , 就加载对象 Factories , 相对安全很多.


总得来说 , Spring Factories 的 Java SPI 的逻辑思路是一致的 , Java SPI 通过 LazyIterator 加载的方式比较骚气 ,但是相对而言获取起来就会很复杂 .

所以 , 完全可以使用 Spring Factories 来完成你自己想要的业务.

总结

定制 Iterator 完成业务是一个不错的思路

参考与感谢

www.baeldung.com/java-spi