JDK之SPI机制简明分析

缘起

最近在看公司代码, 发现一些公司自己封装的老框架中并没有使用spring. 而是使用了大量的XXXServiceLoader的东西. 后来google了一下,发现这是JDK本身提供的SPI(Service Provider Interface)机制.

分析

为什么要用SPI?

这不得不从类加载机制之全盘负责+父委托机制谈起. 关于这些问题的文章很多, 就不再赘述了. 只简明谈一下结论.

  1. 全盘负责

    当一个ClassLoder装载一个类时,除非显示的使用另外一个ClassLoder,该类所依赖及引用的类(比如其父类)也由这个ClassLoder加载字节码文件.

  2. 父委托机制

    当一个ClassLoader准备加载一份字节码文件的时候, 先委托其父类装载器寻找目标类字节码文件并试图加载,只有在找不到或者加载抛出异常的情况下才从自己的类路径中查找并装载目标类.

诚然, 父委托机制增加了字节码的安全性. 因为显然你不能自己写一个java.lang.String类然后用类加载器加载. 而且保证了字节码只会存在一份于内存中. 那么父委托机制有什么缺点吗? 缺点是显然的

如果规范制定方(比如sun)制定的接口规范(比如JNDI、JDBC、JAXP)在rt.jar. 但是提供接口实现类的第三方厂商(比如mysql、oracle、postgresql等)提供的实现类在系统加载器路径中(即 $CLASSPATH 或 -Djava.classpath 参数指定路径),则根据父委托机制,加载接口包的类加载器是无法加载实现类的字节码的.

怎么办呢? 于是我们就必须显示指定使用能够加载实现类的ClassLoader来加载第三方实现类.而这个能够加载第三方实现类的类加载器就是我们常说的线程上下文类加载器. 这个线程上下文类加载器的特点是

  1. 如果不指定, 则默认是父线程所持有的线程上下文加载器.
  2. 最初线程上下文类加载器是AppClassLoader. 这一点可以去看 sun.misc.Launcher 的源码, 参考【1】

也就是,线程上下文类加载器就是为了解决父加载器无法感知子加载器所加载的类而出现的. 可以以下面一副图看出.

于是SPI机制就利用了这一点, 实现了, 高层定义接口规范, 第三方厂商提供实现. 高层提供工具类(JDK的java.util.ServiceLoader)加载这些第三方实现. 当然, 为了工具类能找到第三方jar包中的字节码文件, 约定优于配置, 规定第三方jar包的类路径下的META-INF/services 目录下有以接口全限定名为文件名, 实现类全限定名为文件内容(多个实现类的话, 一行一个).

这样做的好处是显然的, 就能实现第三方jar包插件式的集成, 方便地接入系统.

SPI技术的关键

  1. 使用线程上下文加载器(其实就是AppClassLoader)加载第三方实现类.
  2. 使用ClassLoader 的 API 读取全部jar包中META-INF/services目录下的同名文件.

了解以上两点,我们完全可以写出自定义的SPI和ServiceLoader. 参见DEMO链接. DEMO中只是实现了一个简易版的. JDK的实现更加松耦合. 因为java.util.ServiceLoader内部实现了迭代器模式. 暴露的接口很窄.

参考

【1】https://zy19982004.iteye.com/blog/1983236

DEMO

https://github.com/yfsyfs/backend/tree/master/spi-parent