spi 简介
spi 的全称是Service Provider Interface,主要作用是在让服务具备运行时加载接口的指定实现类的能力,java从 1.6 开始提供此机制(其实 1.3 开始就有了,只不过一直自嗨内部使用,没暴露外部方法给大家用而已),而各种框架有时也自己实现此机制以增强一些特有的功能(e.g:dubbo自己实现的 spi,spring-boot 类似的有spring factories)。
spi通常应用在框架中,辅助框架实现功能的插件化,让用户自己按照约定写的功能也能被框架加载运行。下面就举个 java spi 的例子,从例子中感受spi 的用法以及如何帮助框架实现插件化
java spi demo
ps:以下 demo 可以自行下载
git clone git@github.com:likemoongg/blog-code-demo.git
为了说明spi 的具体用途,在这里举个例子:为了实现通用的字典查询功能可以开发一种框架,可实现对各种不同字典的查询,字典可以由用户或第三方包定制。
框架侧代码
字典类的接口Dictionary
package dictionary.spi;
public interface Dictionary {
public String getDefinition(String word);
}
查询字典的服务DictionaryService
package dictionary;
import dictionary.spi.Dictionary;
import java.util.Iterator;
import java.util.ServiceConfigurationError;
import java.util.ServiceLoader;
public class DictionaryService {
private static DictionaryService service;
private ServiceLoader loader;
private DictionaryService() {
// 这里的 ServiceLoader 就是 java 原生 spi 的重要入口类
// 其入参是一个接口,返回的 ServiceLoader loader 可以加载出其实现类的实例
loader = ServiceLoader.load(Dictionary.class);
}
// 实现单例
public static synchronized DictionaryService getInstance() {
if (service == null) {
service = new DictionaryService();
}
return service;
}
// 扫描 目录META-INF/services/dictionary.spi.Dictionary下描述的所有的 Dictionary 的实现类,依次查找入参的 word
// 其中目录层级META-INF/services是 java spi 默认约定的目录
public String getDefinition(String word) {
String definition = null;
try {
// 利用 ServiceLoader loader 的迭代器遍历每一个实现类,寻找可以识别 word的字典
Iterator dictionaries = loader.iterator();
while (definition == null && dictionaries.hasNext()) {
Dictionary d = dictionaries.next();
definition = d.getDefinition(word);
}
} catch (ServiceConfigurationError serviceError) {
definition = null;
serviceError.printStackTrace();
}
return definition;
}
}
以上两个类就是框架提供的所有类了,我们可以看到框架还没有提供注释里所说的 META-INF/services/dictionary.spi.Dictionary 文件啊!用户运行起来肯定有问题呀!!
这个目录就是 spi 机制的关键也是我们能实现插件化的重要一步。从上面可以看到框架并没有和具体的某个实现类捆绑起来,而是通过 ServiceLoader 去寻找具体的实现类,接口对应哪些实现类就是 META-INF/services/dictionary.spi.Dictionary 文件所描述的关键信息啦。所以用户可以自己编写该文件,就相当于指定了 Dictionary 的实现类具体有哪些,也就实现了插件化!
用户侧的程序
使用框架的用户自己编写的具体Dictionary实现类有两个,一个汉语词典EnDictionary,一个汉语词典CnDictionary。
那么 META-INF/services/dictionary.spi.Dictionary 可以这么写(其实就是类的全限定名称啦~)
net.likemoon.dictionary.EnDictionary
net.likemoon.dictionary.CnDictionary
两个具体实现类如下
package net.likemoon.dictionary;
public class EnDictionary implements dictionary.spi.Dictionary{
@Override
public String getDefinition(String word){
if (word.matches("[a-zA-Z]+")) {
return "looking up English dictionary...";
}
return null;
}
}
public class CnDictionary implements Dictionary {
@Override
public String getDefinition(String word){
if (!word.matches("[a-zA-Z]+")) {
return "正在查阅中文字典...";
}
return null;
}
}
最后是完整的用户使用框架的的例子
package net.likemoon;
import dictionary.DictionaryService;
public class lookupDictionary {
public static void main(String[] args) {
DictionaryService dictionaryService = DictionaryService.getInstance();
System.out.println(dictionaryService.getDefinition("english"));
System.out.println(dictionaryService.getDefinition("中文"));
}
}
输出
looking up English dictionary...
正在查阅中文字典...
总结
上面的demo 中,框架始终只面向 Dictionary 接口编写功能。具体的字典实现类由java spi 获取。而用户可以在不侵入框架代码的情况下,通过编写约定的描述文件,让框架加载用户自己编写的实现类,扩展功能实现插件化。
当然除了用户(业务程序员)自己编写以外,引入第三方编写的插件 jar 包也可以实现相同的扩展效果。此时第三方的 jar包需包含:扩展的实现类+服务描述文件
可见 spi 的核心机制是接口到具体实现类的关联由一个文件描述。
总结 spi 的服务描述文件的要素。
1、文件通常要放在某个约定的目录(上面 demo 中使用的java spi 规定的就是META-INF/services)
2、文件名和文件内容要体现接口和实现类的关联(java spi 中该文件名需为接口名,文件内容为实现类全限定名)
3、文件可以编写多份,都能被相关加载类加载(这样框架可以有内置的实现类,同时用户和第三方扩展包可以加入自己写的实现类扩展功能)
具体的 spi 实现过程中还有很多细节,比如要大量使用Map 做缓存以加快非首次访问的速度,比如接口的实现类都需要提供无参数构造器方便进行实例化等等。
后续会具体讲解一下 dubbo 的 spi 的实现过程和诸多细节。