Java-SPI 机制学习

简介

SPI 是一种基于接口的服务发现机制。它定义了一组接口规范,而具体的实现则由不同的服务提供者提供。在运行时,通过特定的服务加载器(ServiceLoader)来加载这些服务提供者,并将它们整合到系统中。这种机制使得系统的功能可以根据需求灵活地扩展和替换,而不需要对核心代码进行大规模的修改。

SPI 流程解析

图1 SPI 工作流图

图 1 所示的 SPI 工作流图展示了SPI机制在项目中整体的运作流程。首先,项目 2 对项目 1 存在依赖关系,项目 1 定义了 SPI 接口。在项目 2 中,开发人员需针对该 SPI 接口实现相应方法,并在其中融入特定业务逻辑。同时,要在项目 2 里创建 META - INF/services 目录,将项目 1 的 SPI 接口的相对路径以文件形式呈现,且在该文件内填写项目 2 中具体实现 SPI 接口的 Java 类的相对路径,以此完成服务提供者的注册工作。

当项目启动后,若服务需调用带有 SPI 接口的方法时,此方法内部首先要借助 ServiceLoader.load () 方法来定位所有的 SPI 接口实现类。随后,运用 serviceLoader.iterator () 方法遍历并调用每个实现类的特定方法,进而达成功能的实现。最终,将执行结果返回给项目 2,以便其开展后续处理流程。

SPI 实现示例

SPI 实现示例将参照图 1所示的SPI工作流图讲述。项目1 我们定义为SPI Provider 模块,项目2 我们定义为SPI Client 模块。SPI Client 模块依赖于SPI Provider 模块。

其中,SPIProvider 中定义了一个SPI 接口 LanguageSPIInterface,

public interface LanguageSPIInterface {

    void getLanguage();
}

以及它的默认实现类DefaultLanguageSPIImpl。其中DefaultLanguageSPIImpl的方法包含利用ServiceLoader.load()找到具体实现的方法,以及包含serviceLoader.iterator() 方法,遍历调用所有具体实现的具体方法。

public class DefaultLanguageSPIImpl implements LanguageSPIInterface{
    @Override
    public void getLanguage() {
        ServiceLoader<LanguageSPIInterface> serviceLoader = ServiceLoader.load(LanguageSPIInterface.class);
        Iterator<LanguageSPIInterface> iterator = serviceLoader.iterator();
        LanguageSPIInterface service;
        while (iterator.hasNext()) {
            service = iterator.next();
            service.getLanguage();
        }
    }
}

SPIClient 定义了SPI接口的两个实现 CNLanguageImpl 、ENLanguageImpl。

@Service
public class CNLanguageImpl implements LanguageSPIInterface {
    @Override
    public void getLanguage() {
        System.out.println("ZH_CNLanguageImpl");
    }
}

@Service
public class ENLanguageImpl implements LanguageSPIInterface {
    @Override
    public void getLanguage() {
        System.out.println("US_ENLanguageImpl");
    }
}

SPIClient 模块还需要创建 META-INF/services目录,并创建SPI 接口定义文件 person.wend.spiprovider.spiinterface.LanguageSPIInterface。

person.wend.spiprovider.spiinterface.LanguageSPIInterface 文件包含有两个实现类的相对路径。

person.wend.spiclient.spiimpl.CNLanguageImpl
person.wend.spiclient.spiimpl.ENLanguageImpl

配置完以上步骤,项目2启动后,我们就可以直接调用languageSPIInterface.getLanguage()方法,来获取所有实现SPI 接口的结果了。

LanguageSPIInterface languageSPIInterface = new DefaultLanguageSPIImpl();
languageSPIInterface.getLanguage();

ServiceLoader

ServiceLoader 是 Java 中用于加载服务提供者接口(SPI)实现的核心类,它实现了 Iterable<S> 接口,意味着可以通过迭代的方式获取服务提供者的实例。其主要功能是在运行时按照一定规则查找并加载服务接口对应的实现类,让应用程序可以灵活地选择和使用不同的服务实现,同时解耦了接口与具体实现。

以下是关于 java 9版本的 ServiceLoader实现的具体解析。

重要的成员变量

  • service:代表正在被加载的服务对应的 Class 对象(接口或抽象类),指明了服务的类型。

  • serviceName:服务类型的全限定名,用于在查找资源等操作中标识服务。

  • layer:用于定位服务提供者的模块层,当通过类加载器定位提供者时为 null

  • loader:用于定位、加载和实例化服务提供者的类加载器,当通过模块层定位提供者时为 null

  • acc:创建 ServiceLoader 时获取的访问控制上下文,在涉及安全相关操作时会用到,若没有安全管理器则为 null

  • lookupIterator1lookupIterator2:分别用于 iterator()stream() 方法的懒加载迭代器,在需要时才会创建并开始查找服务提供者。

  • instantiatedProvidersloadedProviders:用于缓存已经实例化和加载的服务提供者相关信息,前者存储实例化后的服务实例,后者存储服务提供者的相关对象(实现了 Provider 接口的对象),便于后续复用以及遵循懒加载机制。

  • loadedAllProviders:标记是否已经加载了所有的服务提供者,用于 stream() 方法等逻辑中判断是否可以直接返回已缓存的提供者流。

  • reloadCount:每次调用 reload() 方法时递增,用于判断迭代器等操作过程中是否发生了缓存被清除的情况,若不一致则可能抛出并发修改异常。

迭代器内部类

  • LayerLookupIterator<T>:用于实现基于模块层(及父层)的懒加载服务提供者查找逻辑。在迭代过程中通过深度优先搜索(DFS)的顺序遍历模块层,查找服务提供者并尝试加载,内部维护了模块层的栈、已访问模块层的集合等数据结构,通过 hasNext()next() 方法按照规则进行查找和返回下一个服务提供者(以 Provider<T> 形式),若过程中出现配置错误等问题会抛出 ServiceConfigurationError 异常。

  • ModuleServicesLookupIterator<T>:针对通过类加载器定义的模块或者类加载器所在模块层中模块的服务提供者进行懒加载查找。在 hasNext()next() 方法中,通过不断切换类加载器以及遍历模块层相关信息来查找服务提供者并加载,同样在出现问题时抛出相应异常。

  • LazyClassPathLookupIterator<T>:用于处理通过服务配置文件配置的服务提供者的懒加载查找情况,会忽略命名模块中的服务提供者。它会解析配置文件(遵循一定的语法规则,格式错误会抛出异常),查找并加载符合要求(是服务类型的子类型且有合适构造函数等)的服务提供者类,在安全上下文等不同情况下通过合适的方式执行查找逻辑。

核心方法 iterator()stream()

  • iterator():返回一个迭代器用于懒加载和实例化服务提供者。首次调用时若 lookupIterator1 未创建则会创建合适的查找迭代器(根据是基于模块层还是类加载器等情况)。迭代器在迭代过程中先返回缓存中的已实例化提供者,然后通过查找迭代器继续懒加载并实例化剩余的提供者,同时添加到缓存中。若缓存被 reload() 方法清除后再使用迭代器会抛出 ConcurrentModificationException,并且迭代器不支持 remove 操作,调用会抛出 UnsupportedOperationException

  • stream():返回一个流用于懒加载服务提供者(流中的元素是 Provider<S> 类型,需调用 get() 方法获取实例)。若已经加载完所有提供者则直接返回缓存的提供者流,否则创建查找迭代器并基于其和缓存的提供者构建流。在处理流的过程中同样遵循懒加载机制以及考虑缓存清除等情况,流的分割器(Spliterator)是快速失败的,若缓存被清除会抛出 ConcurrentModificationException

静态加载方法

  • load(Class<S> service, ClassLoader loader, Module callerModule):创建一个新的 ServiceLoader 实例,传入服务类型、类加载器以及调用者模块信息。

  • load(Class<S> service, ClassLoader loader):通过反射获取调用者类,再结合服务类型和类加载器创建 ServiceLoader 实例,调用者需要对服务类型有访问权限且所在模块声明使用该服务类型,否则抛出异常。

  • load(Class<S> service):使用当前线程的上下文类加载器和服务类型创建 ServiceLoader 实例,同时提示不建议在 VM 范围内缓存该实例,因为不同应用的线程上下文类加载器可能不同,容易导致问题。

  • loadInstalled(Class<S> service):使用平台类加载器和服务类型创建 ServiceLoader 实例,主要用于只加载已安装到当前 Java 虚拟机中的服务提供者,忽略应用模块路径或类路径上的提供者。

  • load(ModuleLayer layer, Class<S> service):从给定的模块层及其祖先层中加载服务提供者创建 ServiceLoader 实例,不会查找未命名模块中的提供者,按照特定顺序遍历模块层查找提供者,并且同样有对服务类型权限等相关要求。

其他方法

  • findFirst():方便地获取第一个可用的服务提供者,等价于调用 iterator() 方法获取第一个元素,若没有找到则返回空的 Optional,若加载过程中出现错误会按照 ServiceConfigurationError 相关规则抛出异常。

  • reload():清除 ServiceLoader 的提供者缓存,使得后续调用 iterator()stream() 方法时会重新从头开始懒加载提供者,常用于运行时安装了新服务提供者的场景。

  • toString():返回一个描述该 ServiceLoader 的字符串,包含服务类型的名称信息。

SPI 机制的优缺点

优点

  • 代码解耦:核心代码只依赖于接口,而不依赖于具体的实现类。这样,当需要更换实现或者添加新的实现时,不需要修改核心代码,降低了代码的耦合度,提高了系统的可维护性和可扩展性。
  • 动态扩展:可以在运行时动态地加载和替换服务提供者,使得系统能够根据实际需求灵活地调整功能。例如,在分布式系统中,可以根据不同的环境配置加载不同的服务实现。
  • 模块化开发:不同的服务提供者可以独立开发和部署,只要遵循相同的接口规范,就可以方便地集成到系统中。这有助于团队分工协作,提高开发效率。

缺点

  • 加载性能:由于服务加载器在加载服务时需要扫描类路径下的所有相关文件,当服务提供者数量较多或者类路径较为复杂时,可能会影响系统的启动性能。
  • 无法实现懒加载:服务加载器在初始化时会一次性加载所有的服务提供者,即使有些服务可能在整个应用生命周期中都不会被使用,这也会占用一定的系统资源。
  • 不支持动态卸载:一旦服务提供者被加载,就无法在运行时动态地卸载,这在一些需要动态调整服务配置的场景下可能会带来不便。

参考文献

字节大模型-豆包

深入理解 Java 中的 SPI 机制_jdbc spi机制-CSDN博客

相关推荐
moxiaoran57534 分钟前
IDEA 未启用lombok插件的Bug
java·bug·intellij-idea
knowwait17 分钟前
Docker常用命令
java·docker·容器
普罗米修斯Aaron_Swartz19 分钟前
C#中移位运算
开发语言·c#
胡八一33 分钟前
解决 AWS SDK for Java 连接 S3 文件系统Unable to load an HTTP implementation 问题
java·http·aws
无名之逆34 分钟前
Rust HTTP请求库
服务器·开发语言·后端·网络协议·http·rust·请求
码农飞飞36 分钟前
详解Rust宏编程
开发语言·算法·rust·自定义·使用场景·宏编程
夏旭泽42 分钟前
设计模式-模板模式
java·开发语言·设计模式
WalkerShen1 小时前
使用maven的方式创建Springboot项目
java·spring boot·maven
Geek极安网络安全1 小时前
2024年山西省第十八届职业院校技能大赛 (高职组)“信息安全管理与评估”赛项规程
运维·开发语言·网络·安全·web安全·php
小只笨笨狗~1 小时前
token失效重新存储发起请求
开发语言·javascript·ecmascript