Java热插拔技术之SPI

发布时间:2024年01月16日

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。

背景

最近,公司需要针对一个使用C#的系统以插件的形式进行二次开发。系统提供了一个类库,我们只需要实现类库中的接口,并实现相应的方法,即可完成一个插件的开发。

然后,我们将实现类打包成dll文件,这个dll文件就像是Java中的jar包一样。我们将这个dll文件上传到指定的文件夹,系统会热更新并加载这个dll文件。之后,我们需要在系统中配置该类的全限定名,这样插件就可以生效了。

这种插件式开发方式非常有趣,我在想Java能否实现这样的动态扩展。经过一番查询和研究,我发现Java确实提供了类似的功能,那就是SPI,本文将对SPI进行一个简单的介绍。

SPI是什么

SPI全称Service Provider Interface,是一种服务提供者接口,定义了一组用于实现特定服务的接口或抽象类。它是一种动态替换发现的机制,使得接口和实现可以分离。SPI被专门提供给服务提供者或扩展框架功能的开发者去使用,由调用方提供接口,第三方(提供方)来实现。

SPI和API的区别

SPI(Service Provider Interface)API(Application Programming Interface) 有以下几个区别:

  1. 定义方式:API可以是一组函数、类、协议或工具,通常易于理解和使用。API 定义了可供开发者调用的公共方法和功能。而 SPI 是一种机制,用于揭示和加载服务提供者的实现,它通常以接口的形式存在。

  2. 使用方式:API 是开发者在应用程序中直接调用的,开发者通过使用 API 提供的方法和功能来实现特定的业务逻辑。而 SPI 是通过类加载器和反射机制动态加载和实例化服务提供者的,开发者无需直接调用 SPI 的机制,而是通过使用 SPI 加载的服务实例来实现特定的功能。

  3. 扩展性:API 的设计目的是为了提供一套公共的接口和方法,以便开发者进行二次开发和扩展。而 SPI 的目的是为了实现解耦合和动态加载,允许第三方服务提供者通过 SPI 机制向应用程序中注入其实现,从而实现功能的扩展和替换。

总的来说,API是面向使用者的,它定义了如何使用软件组件;而SPI是面向提供者的,它定义了如何为软件组件提供服务 。

Java SPI

Java SPI是Java 6引入的一种服务发现机制。主要包括以下4个核心概念:

  1. 服务接口:定义一组对外提供服务的服务接口,通常以接口或抽象类的形式存在。
  2. 服务提供者接口:服务接口的具体实现类,提供给应用程序使用。
  3. 配置文件META-INF/services 目录下的配置文件,用于声明服务提供者的实现类。文件名称为服务接口的全限定名,文件内容为服务提供者接口的全限定名。
  4. ServiceLoader :Java SPI关键类,用于加载服务提供者接口的服务。 ServiceLoader 中有各种实用方法,用于获取特定的实现、迭代它们或再次重新加载服务。

当应用程序需要使用某项服务时,它会在类路径下查找 META-INF/services 目录下的配置文件,文件中列出了对应服务接口的实现类。然后,应用程序可以通过 Java 标准库提供的 ServiceLoader 类动态加载并实例化这些服务提供者,从而使用其功能。这种机制被广泛应用于 Java 中各种框架和组件的扩展开发,例如数据库驱动、日志模块等。

Java SPI的优缺点:

优点:

  1. 实现解耦:SPI使得接口的定义与具体业务实现分离,使得应用程序可以根据实际业务情况启用或替换具体组件。
  2. 可插拔性和可扩展性:SPI 允许第三方提供新的服务实现模块,并通过配置文件进行声明,在运行时动态加载,这样可以轻松地扩展和替换系统中的功能模块,实现了可插拔性和可扩展性。

缺点:

  1. 无法按需加载:虽然ServiceLoader做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
  2. 获取某个实现类的方式不够灵活:只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
  3. 不提供类似于Spring的IOC和AOP功能:扩展如果依赖其他的扩展,做不到自动注入和装配。
  4. 并发问题:多个并发多线程使用ServiceLoader 类的实例是不安全的。加载不到实现类时抛出并不是真正原因的异常,错误很难定位。

Java 提供了许多 SPI,以下是SPI及其提供的服务的一些示例:

  • java.util.spi.CurrencyNameProvider:为Currency类提供本地化的货币符号。
  • java.util.spi.LocaleNameProvider: 为 Locale 类提供本地化名称。
  • java.sql.Driver:从 4.0 版本开始,JDBC API 支持 SPI 模式。旧版本使用 Class.forName() 方法加载驱动程序。
  • jakarta.persistence.spi.PersistenceProvider:提供JPA API的实现。
  • java.text.spi.DateFormatProvider:为指定区域设置的日期和时间格式。
  • java.util.spi.TimeZoneNameProvider:为 TimeZone 类提供本地化时区名称。
  • javax.sound.sampled.spi:Java声音SPI接口,开发者可以提供自定义的音频文件读取和写入插件。
  • javax.imageio.spi:Java图像I/O的SPI接口,开发者可以提供自定义的图像读取和写入插件。

实践出真知

下面我们用一个搜索电影的例子来写一个Java SPI的示例。有时候一个人心血来潮想看一些武打电影,我们可以通过UC搜索,UC资源有限,有时就需要使用魔法通过谷歌搜索来找电影,反正不管是黑猫白猫,抓到老鼠就是好猫。所以,可以提供一个搜索电影的服务接口,具体使用哪个搜索就看个人喜好了。

  1. 定义服务接口:首先,新建一个spi-provider 模块,定义一个电影提供者接口MovieProvider,里面有一个方法searchMovie()
public interface MovieProvider {

    /**
     * 搜索电影
     * @param movieName 电影名
     */
    void searchMovie(String movieName);
}
  1. 实现服务接口:新建两个模块 uc-providergoogle-provider,分别引入spi-provider 模块,并在各自模块实现MovieProvider接口。

引入spi-provider 模块:

 <dependency>
     <groupId>BasicJava</groupId>
     <artifactId>spi-provider</artifactId>
     <version>1.0-SNAPSHOT</version>
 </dependency>
public class UCProvider implements MovieProvider{
    /**
     * 搜索电影
     *
     * @param movieName 电影名
     */
    @Override
    public void searchMovie(String movieName) {
        System.out.println("通过UC搜索:"+movieName);
    }
}
public class GoogleProvider implements MovieProvider{
    /**
     * 搜索电影
     *
     * @param movieName 电影名
     */
    @Override
    public void searchMovie(String movieName) {
        System.out.println("通过谷歌搜索:"+movieName);
    }
}
  1. 配置服务提供者:新建一个 spi-consumer 模块,引入 uc-providergoogle-provider, 并在 META-INF/services 目录下,创建一个以MovieProvider接口全限定名为命名的文本文件。
<dependencies>
    <dependency>
        <groupId>BasicJava</groupId>
        <artifactId>uc-provider</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>

    <dependency>
        <groupId>BasicJava</groupId>
        <artifactId>google-provider</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
</dependencies>

META-INF/services
site.sunlong.provider.MovieProvider

  1. 使用服务:一切准备完毕,接下来就是使用它们的时候了。怎么使用呢?为了实现动态使用插件的效果,在这里我们以配置文件的形式使用它,在配置文件中配置UCProviderGoogleProvider是否生效,然后动态的获取配置文件,话不多说,请看实操。

创建 spi-config.properties配置文件类:
spi-config.properties
spi-config.properties配置文件内容如下:

site.sunlong.provider.GoogleProvider=enable
site.sunlong.provider.UCProvider=enable

spi-consumer 模块新建一个SpiConsumer用于测试,在该类中每10s读取一次配置文件的内容,判断插件是否生效,最后只调用生效的插件。具体代码如下:

public class SpiConsumer {
    private static ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);
    public static void main(String[] args) {

        ServiceLoader<MovieProvider> providers = ServiceLoader.load(MovieProvider.class);
        String movieName = "波多";

        executor.scheduleAtFixedRate(() -> {
            // 读取配置文件
            Properties properties = new Properties();
            ClassLoader classLoader = SpiConsumer.class.getClassLoader();
            try (InputStream input = classLoader.getResourceAsStream("spi-config.properties")) {
                if (input == null) {
                    System.out.println("无法找到配置文件");
                    return;
                }
                properties.load(input);
            } catch (IOException e) {
                e.printStackTrace();
                return;
            }

            for (MovieProvider provider : providers) {
                String name = provider.getClass().getName();
                String property = properties.getProperty(name);
                if ("enable".equals(property)) {
                    provider.searchMovie(movieName);
                }
            }
            System.out.println(DateTime.now());
            System.out.println();
        }, 0, 10, TimeUnit.SECONDS); // 初始延迟为0,之后每隔10秒执行一次
    }
}

先后将site.sunlong.provider.GoogleProvidersite.sunlong.provider.UCProvider设置为disable,输出结果如下:

通过谷歌搜索:波多
通过UC搜索:波多
2023-11-21T23:08:58.241+08:00

通过谷歌搜索:波多
通过UC搜索:波多
2023-11-21T23:09:08.213+08:00

通过UC搜索:波多
2023-11-21T23:09:18.200+08:00

通过UC搜索:波多
2023-11-21T23:09:28.215+08:00

通过UC搜索:波多
2023-11-21T23:09:38.208+08:00

通过谷歌搜索:波多
2023-11-21T23:09:48.202+08:00

通过谷歌搜索:波多
2023-11-21T23:09:58.210+08:00

打印结果虽然有延迟,但从结果中还是可以看出我们是实现了可插拔插件的功能,只是结果有瑕疵,可以把配置文件放到缓存或者数据库中以便准确的判断插件是否生效。

总结

总的来说,Java SPI 的实现原理是通过类加载器动态加载配置文件,再利用反射机制实例化具体的服务提供者,并将其注入到应用程序中供服务消费者使用。

文章来源:https://blog.csdn.net/qq_39654841/article/details/134410126
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。