
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 机制可能存在的一些不足,这里我们也留个伏笔,对于存在的不足,我们还有没有更好的实现方案呢?在后续的文章中我们会继续分析。