Java SPI

什么是SPI

SPI 全称:Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。

面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。

为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。这有点类似IOC的思想,将装配的控制权移到了程序之外。

SPI的作用就是为被扩展的API寻找服务实现。

SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQLPostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻zhao服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。

SPI整体机制图如下:

Java SPI

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader。

SPI 的不足

1.不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。

2.获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。(Spring 的BeanFactory,ApplicationContext 就要高级一些了。)

3.多个并发多线程使用 ServiceLoader 类的实例是不安全的。

API 与 SPI

SPI与API区别:

API是调用并用于实现目标的类、接口、方法等的描述;

SPI是扩展和实现以实现目标的类、接口、方法等的描述;

换句话说,API 为操作提供特定的类、方法,SPI 通过操作来符合特定的类、方法。

SPI和API的使用场景解析:

  • API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
  • SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。

项目案例

定义接口

首先我们需要定义接口,通过接口来定制规范给外部进行拓展。

/**
* 自定义的spi接口
*/
public interface Provider {

    /**
    * 校验级别
    * @param level 级别
    * @return 是否属于当前级别
    */
    boolean verification(Integer level);

}

定义工厂

定义获取实现spi模块的服务工厂,通过读取classpath下的META-INF/services目录下配置的实现类,获取到全部服务。

/**
* 服务工厂
*/
public class ProviderServiceFactory {

    /** 全部服务的集合,保存在ServiceLoader */
    static ServiceLoader<Provider> loader = ServiceLoader.load(Provider.class);

    /**
    * 返回服务集合数据
    */
    public static Iterator<Provider> providers() {
        return loader.iterator();
    }

}

配置META-INF/services目录

在资源目录下面创建META-INF/services,之后创建文件xxx.xxx.Provider,里面写实现类的具体实现类名

使用

Integer level = 1;
Iterator<Provider> providers = ProviderServiceFactory.providers();
while (providers.hasNext()) {
    Provider next = providers.next();
    if (next.verification(level)) {
        // do.......
    }
}

后续问题

问:spi调用的是无参构造函数,所以内部的bean无法被注入

答:SPI 实现类的构造函数中使用了 @Autowired 注解注入依赖的 bean,那么由于 SPI 实现类是由 Java 自动创建的,它没有被 Spring 托管,所以 Spring 容器无法注入 bean,这会导致 bean 为空。

是的,Java SPI 实例化的类无法被 Spring 管理,因为这些类是由 Java SPI API 直接实例化的。如果您的实现类需要被 Spring 管理,那么您可以将其声明为 Spring bean,并使用 Spring 的依赖注入功能来注入所需的依赖项。

不过,正如您所说,Java SPI 更适合用于处理静态数据或对外部服务的访问,而不是需要动态创建或需要 Spring 管理的对象。例如,您可以使用 Java SPI 来加载配置文件、国际化资源、驱动程序或插件等静态数据,或者使用 Java SPI 来访问第三方服务,如数据库、缓存或消息队列等。这些场景下,Java SPI 非常适合用于解耦和扩展您的代码。

使用建议

当您编写一个开发框架或工具库时,您可能需要暴露一些可插拔的扩展点,以允许用户根据需要自定义框架或库的行为。例如,您可以暴露一个接口,该接口允许用户提供自己的实现,以实现一些特定的功能。然而,由于您无法预测用户将会实现什么样的接口,因此您不能将用户实现的类硬编码为您的库的一部分。在这种情况下,Java SPI(Service Provider Interface)提供了一种方便的方法来实现可插拔的扩展点。

Java SPI 是一种标准的 Java 扩展机制,它允许您将接口的实现类通过 JAR 文件扩展到您的应用程序中。您可以将接口的实现类打包为 JAR 文件,并在 META-INF/services 目录下创建一个名为接口全限定名的文件。该文件中包含了接口的所有实现类的全限定名,每行一个。Java SPI API 会自动扫描这些文件,并将实现类实例化为接口的对象。

使用 Java SPI 有以下几个优点:

  1. 高度可扩展性:Java SPI 允许用户在运行时动态地添加、替换或删除接口的实现类,从而为用户提供了高度可扩展性。
  2. 松散耦合:Java SPI 可以帮助您实现松散耦合的设计,因为您的代码不需要直接引用用户实现的类,而是通过接口进行交互。这使得您的代码更加模块化、可维护和可测试。
  3. 易于使用:Java SPI 是 Java 标准库的一部分,因此您不需要引入任何额外的库或框架来使用它。Java SPI 的 API 也非常简单,您只需要使用 ServiceLoader 类即可。
  4. 高度兼容:Java SPI 是 Java 标准的扩展机制,因此它可以在任何支持 Java 的平台上运行,包括服务器端、客户端和嵌入式设备等。

需要注意的是,Java SPI 也有一些缺点,例如实现类需要提供默认构造函数、缺乏版本控制和依赖注入不方便等。因此,在使用 Java SPI 时,您需要权衡其优缺点,选择最适合您的场景的方案。

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。

给TA打赏
共{{data.count}}人
人已打赏
技术文章

JDK1.7版本下JavaFx自定义日期选择器

2023-2-28 20:59:50

技术文章

新 QQ NT 桌面版如何实现内存优化探索?

2023-8-8 16:37:58

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索