Java SPI 使用及原理分析
背景介绍
对于业务线上的一名研发来说,Java
的 SPI
机制使用还是不多的,反而倒是在很多的开源框架中,Java
的 SPI
机制被大量的应用,比如 Apache Duboo
框架,其内部很多的模块的扩展机制,比如注册中心、配置中心、负载均衡策略,都是通过 Java
的 SPI
机制来实现的,还比如笔者一直参与的开源框架 Apache ShardingSphere
的,在最新的 5.X
版本中实现的微内核、可插拔的机制,同样是通过 Java
的 SPI
机制来完成的。
那 Java
SPI
机制到底是什么东西呢?其实 Java
SPI
(Service Provider Interface
)是 JDK
内置的一种动态加载扩展点的实现。在 ClassPath
的 META-INF/services
目录下放置一个与接口同名的文本文件,文件的内容为接口的实现类,多个实现类用换行符分隔。JDK
中使用 java.util.ServiceLoader
来加载具体的实现。
Java SPI 实战
- 定义一个接口
IRegistry
用于实现数据储存
1 | package com.dongzl.spi; |
- 提供
IRegistry
的实现,IRegistry
有两个实现:ZookeeperRegistry
和EtcdRegistry
1 | package com.dongzl.spi; |
1 | package com.dongzl.spi; |
- 添加配置文件,在
META-INF/services
目录添加一个文件,文件名和接口全名称相同,所以文件完整路径是META-INF/services/com.dongzl.spi.IRegistry
,文件内容为:
1 | com.dongzl.spi.ZookeeperRegistry |
- 通过
ServiceLoader
加载IRepository
实现
1 | package com.dongzl.spi; |
在上面的例子中,我们定义了一个扩展点和它的两个实现。在 ClassPath
中添加了扩展的配置文件,最后使用 ServiceLoader
来加载所有的扩展点。 最终的输出结果为:
1 | class: com.dongzl.spi.ZookeeperRegistry |
Java SPI 实现原理
1 | private static final String PREFIX = "META-INF/services/"; |
1 | /** |
我们看到,在 ServiceLoader.java
的 load(Class<S> service)
方法中,使用当前线程的 ClassLoader
作为参数,创建了一个 ServiceLoader
对象,通过注释我们也可以了解到,默认不指定类加载参数的的情况下:
1 | ServiceLoader.load(service); |
与
1 | ServiceLoader.load(service, Thread.currentThread().getContextClassLoader()); |
是等价的。
在 ServiceLoader
构造方法有两个参数,分别是 Class
对象和指定的类加载器。在构造方法中完成了两件工作:一是变量赋值,二是调用 reload()
方法。而 reload()
方法的作用是根据接口的 Class
对象和类加载器来初始化 LazyIterator
对象。
1 | private ServiceLoader(Class<S> svc, ClassLoader cl) { |
在调用 ServiceLoader
的 iterator()
方法时,在内部创建了 java.util.Iterator
接口的匿名实现:
1 | public Iterator<S> iterator() { |
在 Iterator
接口 hasNext()
和 next()
方法匿名实现中,首先会从全局变量 providers
判断是否已经缓存了扩展服务类,如果已缓存,直接返回结果;如果还未缓存,则会继续调用 LazyIterator
类中对应的 hasNext()
和 next()
方法。
在 LazyIterator
类内部实现中,hasNext()
方法逻辑主要在 hasNextService()
方法中完成,next()
方法逻辑主要在 nextService()
方法中完成。
hasNextService()
方法主要逻辑
1 | // 获取完整路径名称(包名 + 接口名) |
nextService()
方法主要逻辑
1 | // 通过 Class.forName java 反射机制加载类 |
通过这一段分析,我们也就能理解到,在 hasNextService()
方法内部只是完成了服务类配置的解析和读取,并没有真正完成具体实现类的初始化和加载,真正的类实例化和加载是在调用 nextService()
方法中完成的,这就是为什么 LazyIterator
类名中会有 Lazy
,Lazy
主要的意思是在 ServiceLoader
初始化中并不会完成服务类的加载,甚至在调用 Iterator
对象的 hasNext()
方法(对应 LazyIterator
的 hasNextService()
方法)依旧没有进行类的加载,而真正的加载需要延迟到调用 Iterator
对象的 next()
方法(对应 LazyIterator
的 nextService()
方法)中来完成,这就是延迟加载的真正含义。
loadInstalled() 方法与 load() 方法区别
在 ServiceLoader
类中,除了 load()
方法,还有 loadInstalled()
方法,这个方法逻辑并不复杂:
1 | /** |
load()
方法和 loadInstalled()
方法最大的区别是使用类的加载器不同,load()
方法使用 Thread.currentThread().getContextClassLoader()
作为类加载器;而 loadInstalled()
方法在 while
循环内部,通过逐级向上查找最顶级的父 ClassLoader
来作为 ServiceLoader
的类加载器,最终使用类加载器是按照如下顺序来完成的:
ExtClassLoader –> SysClassLoader –> Bootstrap ClassLoader
那么这个方法的操作存在的意义是什么呢?在注释中也有一段描述:
将仅查找并加载已安装到当前的 Java 虚拟机中的 provider 产生的服务;应用程序类路径的 provider 将被忽略。
如果将上面实战案例换成如下测试代码:
1 | ServiceLoader<IRegistry> serviceLoader = ServiceLoader.loadInstalled(IRegistry.class); |
执行测试程序是不会有任何输出的,也就是我们在应用程序内部定义的 SPI
扩展并没有被加载;如果我们将测试程序打成 jar
包,放入 JDK
安装目录 jre/lib/ext
目录下面,再执行我们的测试程序,会正常产生结果,说明我们打包的 SPI
扩展已经被正常加载。
Java SPI 存在不足
Java
SPI
使用虽然简单,也做到了基本的加载扩展点的功能。但还是存在以下的不足:
ServiceLoader
虽然使用了延迟加载的思想,但是还是会通过遍历一次性加载所有的扩展实现,也就是对服务的实现类需要全部加载并实例化一遍。如果我们并不想使用某些实现类,也同样会被加载并实例化了,这就造成了资源浪费;或者某些服务实例化比较耗时,也会拖慢整个系统性能;
- 获取某个实现类的方式不够灵活,只能通过
Iterator
遍历形式获取,无法根据某个参数来获取对应的实现类。
总结
在这篇文章中,我们通过对一些著名的 Java
开源框架可扩展机制的实现原理分析,引出了 Java
SPI
机制,接下来通过一个小的实战案例演示了 Java
SPI
机制的使用方式,并结合 JDK 源码,对 Java
SPI
实现原理进行了分析,最后我们还总结了使用原生的 Java
SPI
机制可能存在的一些不足,这里我们也留个伏笔,对于存在的不足,我们还有没有更好的实现方案呢?在后续的文章中我们会继续分析。