Java SPI 原理
SPI
SPI(Service Provider Interface) 是 JDK 6(1.6)引入的一种服务发现机制,这里 Service 通常是一个 interface
或 class
,它定义了用户所能访问的接口; Provider 通常是 Service 实现类、子类或者定义了 provider
静态方法的任意类(JDK 9)。
面向对象程序设计中,推荐模块之间基于接口,而不是具体的实现类进行交互,避免暴露具体的实现类及其装配过程(SOLID原则)。但如果只从程序设计角度看,还不足以支撑 JDK 引入 SPI,毕竟面向接口编程也不是什么难事,况且 SPI 还会有反射带来的性能损耗。分析 JDK 内置 SPI 的原因,可以从 JDK 内置的支持 SPI 的包去分析:
- Driver:JDBC 4.0 开始支持 SPI,用户无需手动调用
Class.forName("oracle.jdbc.driver.OracleDriver")
注册驱动。 - PersistenceProvider:JPA 服务接口。
- LocaleServiceProvider:获取地区相关信息。
可以看到 JDK 内置 SPI 都有一个共同特点,那就是只要将实现类所在的 jar 包安装到 classpath 或 modulepath 就可以自动被 JDK 加载,而不用调整代码。换言之,这些 jar 就是插件,在程序发布后还能根据需要安装不同插件,进而调整程序行为。
ServiceLoader
SPI 描述了服务提供与发现的机制,ServiceLoader 则实现了对应的策略,提供切实可以的方式去定义与实现自己的 SPI。下面通过官方文档中的例子进行简要的说明。
定义 SPI
SPI 接口除不能描述服务提供者如何实现外,有两个一般准则:
- 根据需要定义足够的接口。
- 接口需要服务提供者是直接实现接口还是作为 proxy 或 factory 间接提供服务(有点绕,其实是对 SPI 接口的一个分类,下面讨论)。
下面是 ServiceLoader 文档中的接口定义:
1 | package com.example; |
定义了编解码器工厂接口。一般来讲,编解码器的实例化开销都比较高(分配缓冲区),所以 getEncoder
和 getDecoder
提供了一个 encodingName
来指定需要的编解码器,如果是不支持的编解码格式,则直接返回 null
,避免实例化所有编解码器后再一个个判断是否支持。这就是上面准则 2对应的 SPI 接口类型。
实现、部署 SPI
在 JDK 9 支持 module 后,又新增了一种实现 SPI 接口的方式,直接看代码:
1 | // Since JDK 6 |
StandardCodecs
直接实现 CodecFactory
并实现其中的两个方法,使用默认构造器初始化。JDK 9 之后我们可以使用声明了 “public static no-args method named ‘provider’ with a return type of Service” 的类作为服务提供者。
在 JDK 9之前,以 jar 文件方式发布服务提供者还需要提供一个 provider-configuration 文件,列举所有服务提供者,使服务提供者能够被识别,配置文件存放在 META-INF/services
下面,文件名是 SPI 接口的全限定名,并写入服务提供者的全限定名作为内容:
1 | # META-INF/services/com.example.CodecFactory |
每一行就代表一个服务提供者,每一行 “#” 及后面的字符都会被忽略。
如果以 module(JDK 9) 方式 发布服务提供者,则需要在 module declaration 使用 provides
指令声明服务提供者:
1 | module your.module.name { |
声明方式相比 jar 的 provider-configuration 要更集中一些。
使用 SPI
1 | ServiceLoader<CodecFactory> loader = ServiceLoader.load(CodecFactory.class); |
如果使用了 module,则还需要声明当前 module 为 Service 的消费者:
1 | module your.module.name { |
ServiceLoader 实现
不管是 JDK 9 之前还是之后,ServiceLoader 都是通过反射调用无参构造器或 provider
方法创建服务提供者实例。根据上面部署的方式,要拿到服务提供者,只需要读取到配置信息就可以了,即 ServiceLoader 的核心代码是配置信息的读取(个人看法):
核心代码在 ModuleServicesLookupIterator(JDK 9) 和 LazyClassPathLookupIterator 中,前者优先级更高。
1 | // ServiceLoader#LazyClassPathLookupIterator#nextProviderClass |
ServiceLoader 从 module 获取服务提供者相关部分比较复杂,所以只看解析部分吧:
1 | // ServiceLoader#ModuleServicesLookupIterator#loadProvider |
JDK 9 module 加载之后,多个 module 会组成一个 module layer,一个 module layer 可以映射到多个 class loader,也即可以被多个 class loader 访问到里面的类。那么服务提供者会从当前 class loader 可访问的 module 开始搜索,然后从其 parent class loader 继续搜索直到 bootstrap class loader。可以看到 module 与相比 provider-configuration ,没有了读文件操作,理论上性能更优。
其它
网上一些文章都指出了 ServiceLoader
遍历性能会有点差,这中间的瓶颈在文件 IO(JDK 9 module 中无),因为需要遍历 classpath。当发布后的程序不要支持插件这种动态能力,这种无意义的性能开销就需要被规避。常用的优化手段有缓存和静态服务注册表,后者可能就不会使用 ServiceLoader
实现了。
至于 ServiceLoader 不是线程安全的缺点,很多类库都不是线程安全的好伐,这个缺点太牵强了!!!
参考文章
Java Service Provider Interface