SPI原理

发布时间 2023-07-30 23:46:38作者: easy16

什么是SPI?

SPI全称为Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services目录中查找文件,自动加载文件中指定的实现类,并将它们实例化、初始化,然后返回给调用方使用。

其设计思想是:面向接口 + 配置化 + 反射。

SPI的优点

  1. 松耦合:SPI机制使得服务提供者和服务使用者之间松耦合,服务提供者可以独立地进行扩展和升级,而不会影响到服务使用者。

  2. 可扩展性:SPI机制可以很方便地扩展新的服务提供者实现类,只需要将实现类打包成jar包,并在META-INF/services目录下创建一个以服务接口全限定名为命名的文件,然后在文件中写入实现类的全限定名即可。

  3. 配置化:SPI机制可以通过配置文件来指定具体使用哪个服务提供者实现类,从而达到动态切换服务提供者的目的。

SPI的缺点

  1. 无法保证唯一性:SPI机制没有强制要求服务提供者实现类的唯一性,如果存在多个同名的服务提供者实现类,那么加载的时候就会出现问题。

  2. 无法进行参数传递:SPI机制只能用于无参数的构造函数创建实例,无法进行参数传递。

  3. 无法进行依赖注入:SPI机制只能通过反射来创建实例,无法进行依赖注入。

SPI实现具体步骤

  1. 定义接口:首先,需要定义一个接口,用于描述要实现的服务功能。

  2. 编写服务提供者:不同的模块可以实现这个接口,并提供自己的具体实现。

  3. 编写配置文件:在META-INF/services目录下,创建一个以接口的全限定名为名称的文本文件,其中包含所有实现了该接口的服务提供者的类名。这个配置文件的格式是每行一个服务提供者的类名。

  4. 加载服务提供者:在应用程序运行时,Java SPI机制会自动加载这个配置文件,并根据其中的类名实例化相应的服务提供者。

SPI的示例

项目关系图

 

spi-api项目代码

 

spi-plugin1项目代码

 资源文件中org.example.HelloService内容为:

org.plugin.a.SpiPluginA1
org.plugin.a.SpiPluginA2

spi-plugin2项目代码

 

 资源文件中org.example.HelloService内容为:

org.plugin.b.SpiPluginB1
org.plugin.b.SpiPluginB2

spi-app项目代码

spi-app项目不用关心Service的具体实现,它只需要和接口交互即可。

 在spi-app项目的pom中,引用spi-plugin1的jar包依赖

    <dependencies>
        <dependency>
            <groupId>org.example</groupId>
            <artifactId>spi-plugin1</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

  运行spi-app项目,调用spi-plugin1中的实现,结果如下

 同样在spi-app项目的pom中,引用spi-plugin2的jar包依赖,其他不改动,则调用spi-plugin2中的实现,结果如下

 如果在spi-app项目的pom中,同时引用spi-plugin1、spi-plugin2的jar包依赖,则两个Service provider都会执行。

从以上示例可知:SPI定义好Interface之后,在项目中mave中只要引入对应不同实现的jar包,就可以调用不同的服务提供者实现类,从而达到动态切换的效果。

SPI的源码分析

Java SPI的实现主要依赖于ServiceLoader类。这个类是Java标准库中提供的,用于加载和实例化配置文件中指定的服务提供者。

ServiceLoader类的load方法接受一个接口类型作为参数,并返回一个ServiceLoader对象。通过这个对象,我们可以迭代获取接口的所有实现类的实例。

public static <S> ServiceLoader<S> load(Class<S> service) {
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
        return ServiceLoader.load(service, cl);
    }

 

ServiceLoader类的实现中,它会根据配置文件中的类名,使用反射机制来实例化服务提供者的对象。这样,我们就可以通过接口来引用具体的实现类,而无需在代码中显式地指定类名,实现了松耦合和可插拔的设计。

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
                c = Class.forName(cn, false, loader);
            } catch (ClassNotFoundException x) {
                fail(service,
                     "Provider " + cn + " not found");
            }
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            try {
                S p = service.cast(c.newInstance());
                providers.put(cn, p);
                return p;
            } catch (Throwable x) {
                fail(service,
                     "Provider " + cn + " could not be instantiated",
                     x);
            }
            throw new Error();          // This cannot happen
        }

 

SPI的应用场景

1. JDBC驱动程序加载:JDBC驱动程序加载就是一个典型的SPI应用场景。JDBC规范定义了一组接口,不同的数据库厂商需要实现这些接口,并将实现类打包成jar包,在META-INF/services目录下创建一个以接口全限定名为命名的文件,然后在文件中写入实现类的全限定名即可。当应用程序需要连接数据库时,就可以通过JDBC驱动程序管理器自动加载并初始化相应的驱动程序实现类。

2. 日志框架:许多日志框架(如Log4j、Logback等)都使用了SPI机制。日志框架定义了一组接口,并提供了默认的实现类。用户可以通过配置文件来指定使用哪个实现类。

3. RPC框架:RPC框架(如Dubbo、gRPC等)也使用了SPI机制。RPC框架定义了一组接口,不同的序列化、负载均衡、注册中心等组件需要实现这些接口,并将实现类打包成jar包,在META-INF/services目录下创建一个以接口全限定名为命名的文件,然后在文件中写入实现类的全限定名即可。当应用程序需要调用远程服务时,就可以通过RPC框架自动加载并初始化相应的组件实现类。

SPI和SpringBoot对比

 
SPI SpringBoot自动装配
使用配置文件:META-INF/serivce 使用文件META-INF/spring.factories
提供jar的一方,也一起提供配置文件 提供自动配置的jar包,也提供配置META-INF/spring.factories
使用getResource读取classpath中的配置文件 和spi读取配置文件的方法一样