SPI机制原理分析

1. 什么是SPI

Service Provider Interface,服务提供接口,其实看这个字面意思很难理解,我给出一个自己的解释。

就是服务的调用者提供接口,由第三方或扩展框架实现的接口,它提供了这样一个机制,为某个接口寻找服务实现的机制,让api提供者提供接口,第三方实现,实际上是“基于接口编程+策略模式+配置文件”组合实现的动态加载或则说是动态替换发现机制,实现了服务调用方和实现者的解耦,类似spring的IOC容器,将管理装配权反转到程序之外。

2. SPI和API的区别

2.1 API接口和实现都是放在实现方,实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API。如图:

spring有SPI与springboot spi spring spi机制_java


2.2 SPI是接口存在于调用方,由接口调用方确定接口规则,然后由不同的服务厂商根据这个规则对这个接口进行实现,从而提供服务。如图:

spring有SPI与springboot spi spring spi机制_加载_02

3. SPI的作用

3.1 其实我们平时用的框架很多都是使用了SPI机制,我们都知道开源框架的扩展性很高,为什么框架的扩展性都很高呢,其中一个重要原因就是利用了SPI机制,SPI让框架的扩展成为了可能,我们平时在进行模块化设计时,只有深入理解了SPI机制,才能更好的设计出符合可插拔原则的模块。

3.2 SPI机制在JDBC DriverManager中的应用

在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName(“com.mysql.jdbc.Driver”)这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。

3.3 SPI机制在SLF4J 中的实现

Spring框架提供的日志服务 SLF4J 其实只是一个日志门面(接口),但是 SLF4J 的具体实现可以有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。

3.4 SPI机制在Springboot 自动装配中的实现

Springboot的核心自动装配就是利用SPI将需要自动装配的类自动加载实例化,同时通过conditional选择性装配。

4. 通过代码展示SPI机制的实现原理:

4.1 先创建一个支付接口Pay

public interface Pay {
    void invokeStrategy();
}

4.2 AliPay实现Pay

public class WechatPay  implements  Pay{
    @Override
    public void invokeStrategy() {
        System.out.println("WechatPay.......................");
    }
}

4.3 WechatPay实现Pay

public class AliPay implements Pay {
    @Override
    public void invokeStrategy() {
        System.out.println("AliPay...........................");
    }
}

4.4 在项目resources目录下新建META-INF/services/pay全路径目录,同时将AliPay和WechatPay的全路径写入

spring有SPI与springboot spi spring spi机制_java_03


4.5 测试代码

public class SpiTest {
    public static void main(String[] args) {
        ServiceLoader<Pay> load = ServiceLoader.load(Pay.class);
        Iterator<Pay> iterator = load.iterator();
        while(iterator.hasNext()){
            Pay next = iterator.next();
            next.invokeStrategy();
        }
    }
}
  1. 6 打印结果, 可以看到AliPay和WechatPay的invokeStrategy方法都执行了
AliPay...........................
WechatPay.......................
  1. 通过源码分析SPI机制的实现原理
    5.1 ServiceLoader.load();,先获取线程上下文加载器, 然后调用静态的load()方法。了解java类加载机制双亲委派模型的都知道,BootstrapClassLoader是顶级加载器,默认加载的是**%JAVA_HOME%中lib下的jar包和class类文件**,子类加载器可以见到父类加载器加载的类,而父类加载器看不见子类加载器加载的类,而ServiceLoader是在rt.jar包中的类,正好在是lib下的jar包,由BootstrapClassLoader类加载器加载,因此当前的类加载器已不能再加载spi中的接口和实现类了,这里就是要拿到AppClassLoader加载器加载。有些人可能会说了这不是破坏了双亲委派模型了吗,对的,SPI机制就是破坏双亲委派模型的一种方式,这可能是个面试题哈。
  2. spring有SPI与springboot spi spring spi机制_实例化_04

  3. 5.2 getContextClassLoader就是获取AppClassLoader类加载器,值得说明的是,java引入了线程上下文加载器ThreadContextClassLoader,可以在线程中设置,如果创建线程时还未设置,它将会从父线程中继承一个;如果在应用程序的全局范围内都没有设置过,那么这个类加载器默认就是应用程序类加载器AppClassLoader加载器,由于ServiceLoader并没有设置,所以获取的是AppClassLoader类加载器
  4. spring有SPI与springboot spi spring spi机制_java_05

  5. 5.3 load()方法调用了构造器
  6. spring有SPI与springboot spi spring spi机制_java_06

  7. 5.3 构造器的核心是reload()方法
  8. spring有SPI与springboot spi spring spi机制_加载器_07

  9. 5.4 一个是清除providers,其实这个providers的作用是缓存,存放已经实力化的实现类,这中设计方式在很多框架中都有应用,实例的多次加载都走缓存,而不是重新实例化。lookupIterator是一个内部类,这里用到了迭代器模式,由lookupIterator具体实现SPI机制。
  10. spring有SPI与springboot spi spring spi机制_加载器_08


  11. spring有SPI与springboot spi spring spi机制_java_09

  12. 5.5 ServiceLoader通过迭代器加载实现,可以看到核心是通过lookupIterator,因为此时的providers还没有数据,只有等实现类都实例化了providers中才有数据走缓存。
  13. spring有SPI与springboot spi spring spi机制_java_10

  14. 5.6 hasNext()实则调用lookupIterator的hasNext()
  15. spring有SPI与springboot spi spring spi机制_加载_11

  16. 5.7 走核心方法hasNextService()
  17. spring有SPI与springboot spi spring spi机制_实例化_12

  18. 5.8 由于是第一执行程序,nextName为null, 所以开始加载指定目录META-INF/services/下的资源
  19. spring有SPI与springboot spi spring spi机制_加载器_13


  20. spring有SPI与springboot spi spring spi机制_实例化_14

  21. 5.9 通过URLClassPath加载资源,因为我们可以知道它可以加载所有在MEAT-INF/services下的资源,最终转化成Enumeration枚举,可能很多人没用过这个类,其实在java没有迭代器模式出现之前,都用的是Enumeration,我们现在只能在源码中看到它的身影了。
  22. spring有SPI与springboot spi spring spi机制_加载器_15


  23. spring有SPI与springboot spi spring spi机制_java_16

  24. 5.10 由于configs已经加载到资源,而且不为null, 所以会将资源转成pending
  25. spring有SPI与springboot spi spring spi机制_实例化_17

  26. 5.11 pending是通过流的方式读取资源,将资源放入List,再转成Iterator迭代器。同时赋值nextName为实现类的全类名。
  27. spring有SPI与springboot spi spring spi机制_加载_18

  28. 5.12 next()方法的核心还是调用lookupIterator的next()方法
  29. spring有SPI与springboot spi spring spi机制_加载_19

  30. 5.13 核心走nextService()方法
  31. spring有SPI与springboot spi spring spi机制_加载器_20

  32. 5.14 通过反射实例化实现类,同时放入缓存,返回对应的实例。这里的cast是将子类强转成父接口,这就是SPI的一个重要机制,服务的调用者看到的永远是接口,而不关注实现。
  33. spring有SPI与springboot spi spring spi机制_实例化_21

  34. 5.15 源码中有个小细节,就是获取实例的时候,把nextName设置成null, 这是为什么呢,其实看过java迭代器源码的人都知道,迭代器里一个重要参数是游标,游标和size进行比较,得出迭代器是否结束,这里设置成null,就是为了每次在一次迭代结束之后,都进行判断。支持源码分析结束。
6. 分析SPI机制在JDBC DriverManager中的应用

6.1 大家都知道,如果项目中连接的数据库是mysql, 我们需要导入mysql-connector-java依赖。

6.2 在JDBC4.0之前,我们需要通过这段代码获取数据库连接,可以看到,需要通过Class.forName(“com.mysql.jdbc.Driver”)这句先加载数据库相关的驱动。

String url = "jdbc:mysql:///consult?serverTimezone=UTC";
String user = "root";
String password = "root";

Class.forName("com.mysql.jdbc.Driver");

Connection connection = DriverManager.getConnection(url, user, password);

6.3 而JDBC4.0之后不需要用Class.forName(“com.mysql.jdbc.Driver”)来加载驱动,直接获取连接就可以,正是利用了SPI机制。

spring有SPI与springboot spi spring spi机制_加载_22


6.4 下面通过源码分析,JDBC是如何利用SPI机制的 6.4.1 DriverManager也是通过BootstrapClassLoader类加载器加载的,根据类加载的特性,我们直接看下面这个代码块

spring有SPI与springboot spi spring spi机制_实例化_23


6.4.2 我们并没有设置jdbc.drivers系统属性,因为会走SPI加载,这个时候走使用AppClassLoader加载器加载驱动类,这里破坏了双亲委派模型。

spring有SPI与springboot spi spring spi机制_加载_24


6.4.3 这里进行类迭代器迭代,却没有执行具体的逻辑,这是为什么呢,通过之前对SPI源码的分析,我们知道在进行迭代的时候其实会通过反射进行实现类的实例化,也就是说会进行类加载。

spring有SPI与springboot spi spring spi机制_加载_25


6.4.4 因为我们看具体的实现类,基于类加载的特性,这里通过静态代码块自动将驱动类注册到DriverManager中,这样完成了自动注册,这种实现方式是不是很巧妙,看过Springboot源码的,可以在源码中很多地方看到这种实现方式。

spring有SPI与springboot spi spring spi机制_加载器_26

7. SPI机制的缺点

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

7.2 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。

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

7.4 值得说明的是,Springboot对SPI机制的使用就更高级一些,它通过@Conditional注解按需装配,建议大家研究下Springboot这部分源码

8. SPI的应用场景

8.1 其实SPI的应用场景非常多,比如我们在进行模块化拆分时,有技术负责人主导模块拆分和接口定义,由小组成员具体实现服务。

8.2 在实际业务开发中,比如芯选商城用户支付,项目里面有什么支付模块我就使用什么样的支付模块,比如说有支付宝支付模块就选择支付宝、有微信支付模块我就选择微信支付、同时有多个的时候,我默认选择第一