目录

SPI - Service Provider Interface

服务提供者接口( SPI ) 是旨在由第三方实现或扩展的API 。它可用于启用框架扩展和可替换组件。

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。

服务是一组众所周知的接口和(通常是抽象的)类。服务提供者是服务的具体实现。提供者中的类通常实现服务本身中定义的类的接口和子类。服务提供者可以以扩展的形式安装在 Java 平台的实现中,即将 jar 文件放置在任何常用的扩展目录中。还可以通过将提供程序添加到应用程序的类路径或通过某些其他特定于平台的方式来使提供程序可用。

可以使用相应的工具将该概念扩展到其他平台。在Java 运行时环境中,SPI 用于

  • Java Database Connectivity 数据库连接
  • Java Cryptography Extension 密码学扩展
  • Java 命名和目录接口
  • 用于 XML 处理的 Java API
  • Java Business Integration(JBI)
  • Java Sound
  • Java Image I/O
  • Java File Systems

JDK SPI

JDK 中 提供了一个 SPI 的功能,核心类是 java.util.ServiceLoader。其作用就是,可以通过类名获取在 META-INF/services/ 下的多个配置实现文件。

JDK SPI机制的一个劣势,无法确认具体加载哪一个实现,也无法加载某个指定的实现,仅靠ClassPath的顺序是一个非常不严谨的方式

Java SPI 是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。

  • 设计一个接口,将接口的实现类写在配置文件中,服务通过读取配置文件来发现实现类,进行加载实例化然后使用。
  • 配置文件路径:classpath下的META-INF/services/
  • 配置文件名:接口的全限定名
  • 配置文件内容:接口的实现类的全限定名
1
2
3
4
5
6
7
8
9
public interface Hero {
  void introduce();
}
public class IronMan implements Hero {
  @Override
  public void introduce(){
    System.out.println("我是钢铁侠!");
  }
}

classpath 下的 META-INF/services/ 增加名字为 com.ynthm.demo.spi.Hero 的文件内容为实现类的全限定名按行隔开 com.ynthm.demo.spi.IronMan

1
2
3
4
5
public static void main(String[] args) {
  ServiceLoader<Hero> serviceLoader = ServiceLoader.load(Hero.class);
  System.out.println("Java SPI:");
  serviceLoader.forEach(Superman::introduce);
}

java.sql.Driver 接口

/images/compute/spi/mysql-spi.jpg
MySQL 驱动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java.sql.DriverManager#ensureDriversInitialized

  /*
     * Load the initial JDBC drivers by checking the System property
     * jdbc.drivers and then use the {@code ServiceLoader} mechanism
     */
    @SuppressWarnings("removal")
    private static void ensureDriversInitialized() {
      AccessController.doPrivileged(new PrivilegedAction<Void>() {
         // ...
                public Void run() {
                    ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                    Iterator<Driver> driversIterator = loadedDrivers.iterator();
                    try {
                        while (driversIterator.hasNext()) {
                            driversIterator.next();
                        }
                    } catch (Throwable t) {
                        // Do nothing
                    }
                    return null;
                }
            });
             // ...
    }

获取接口的实现类的方式不灵活 ServiceLoader 只能通过 Iterator 形式遍历获取,不能根据参数获取指定的某个实现类。如果不想用某些实现类,它也会被加载并实例化,造成浪费。

JCE 中的 SPI

  • 为Provider提供实现的定义和支持加密服务的框架。这个框架包括像java.security、java.crypto.spec和java.crypto.interfaces包。
  • Sun,SunRsaSign,SunJCE等实际Provider包含实际的加密实现。

每个SPI类的名称与相应引擎类的名称相同,后跟“Spi”。例如,与Signature引擎类相对应的SPI类是SignatureSpi类。

每个SPI类都是抽象的。为了提供特定类型的服务和特定算法的实现,Provider必须继承相应的SPI类并提供所有抽象方法的实现。

  • SignatureSpi
  • SignatureSpi
  • MessageDigestSpi
  • KeyPairGeneratorSpi
  • SecureRandomSpi
  • AlgorithmParameterGeneratorSpi
  • AlgorithmParametersSpi
  • KeyFactorySpi
  • CertificateFactorySpi
  • KeyStoreSpi
  • CipherSpi
  • KeyAgreementSpi
  • KeyGeneratorSpi
  • MacSpi
  • SecretKeyFactorySpi
  • ExemptionMechanismSpi

How to Implement a Provider in the JCA

Spring SPI

Spring 的 SPI 配置文件是一个固定的文件 - META-INF/spring.factories,功能上和 JDK 的类似,每个接口可以有多个扩展实现。

配置文件内容为 key-value 类型,key 为接口的全限定名, value 为实现类的全限定名,可以为多个。

1
2
3
//获取所有factories文件中配置的LoggingSystemFactory
List<PropertySourceLoader>> factories = 
    SpringFactoriesLoader.loadFactories(PropertySourceLoader.class, classLoader);

使用的第三方依赖包中,很多都使用了 Spring SPI,如 dubbo,mybatis,redisson 等等。

Dubbo SPI

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public interface Robot {
    void sayHello();
}

public class OptimusPrime implements Robot {
    
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下。

optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
public class DubboSPITest {

    @Test
    public void sayHello() throws Exception {
        ExtensionLoader<Robot> extensionLoader = 
            ExtensionLoader.getExtensionLoader(Robot.class);
        Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
        optimusPrime.sayHello();
        Robot bumblebee = extensionLoader.getExtension("bumblebee");
        bumblebee.sayHello();
    }
}

Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性

dubbo的 Filter、Protocol、Cluster、LoadBalance 等都是通过 SPI 的方式进行拓展加载的。

dubbo SPI 为每个拓展点(接口)单独设置一个文件,文件名为接口的全限定名。如org.apache.dubbo.rpc.Filter,org.apache.dubbo.rpc.Protocol,org.apache.dubbo.rpc.cluster.LoadBalance 等。

dubbo SPI 的实现在 ExtensionLoader 这个类。

  • 由 ExtensionLoader 从配置文件中加载所有的拓展类
  • 配置文件名为接口的全限定名。
  • 加载项目中及 jar 包下以下目录的配置文件
    • META-INF/dubbo/
    • META-INF/services/
  • 读取配置文件时,根据 = 为界限,确认键值对。
  • 过程中多处使用缓存提升性能。
  • 获取到 别名 – 实现类的全限定名后,即可直接通过别名去获取指定的拓展类。

Dubbo SPI 支持内部的依赖注入,通过目录来区分Dubbo 内置的 SPI 和外部的 SPI

比较不同的SPI

JDK SPI Spring SPI Dubbo SPI
文件方式 每个扩展点一个独立文件 每个扩展点一个独立文件 素有扩展在一个文件
获取摸个固定实现 不支持,只能按顺序获取所有 有别名的概念,可以通过名称获取扩展点某个固定实现,配合注解很方便 不支持,优先加载用户代码文件

三种 SPI 机制对比之下:

  • JDK 内置的机制是最弱鸡的,但是由于是 JDK 内置,所以还是有一定应用场景,毕竟不用额外的依赖;
  • Dubbo 的功能最丰富,但机制有点复杂了,而且只能配合 Dubbo 使用,不能完全算是一个独立的模块;
  • Spring 的功能和JDK的相差无几,最大的区别是所有扩展点写在一个 spring.factories 文件中,也算是一个改进,并且 IDEA 完美支持语法提示。