概念介绍
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是专门给服务提供者使用的接口,也就是定义接口的人,和实现接口的人并不是同一个人
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
单看概念比较抽象,下面用演示代码具象一点看看到底是怎么回事
代码演示
SPI的核心是JDK中的这个类ServiceLoader
,可以加载当前环境中指定接口的实现。
这里接口定义者定义了一个日志服务接口LogService
,代码如下
public interface LogService {
void info(String info);
}
该接口定义了一个输出日志的方法,并且提供了一个打印日志的函数
public static void printLog(String info) {
ServiceLoader<LogService> serviceLoader = ServiceLoader.load(LogService.class);
for (LogService logService : serviceLoader) {
logService.info(info);
}
}
这里就用到了ServiceLoader加载当前环境中LogService
的实现,并且调用接口的info
方法去打印日志,如果这是我们调用printLog
将什么也不会打印,因为接口定义者只定义了接口,但是并没有实现接口,所以拿到的serviceLoader
里并没有实现类,之所以接口定义者没有去实现接口是因为他并不知道使用printLog
打印日志的调用方希望把日志打印到哪里去,可以打印到控制台,也可以打印到文件,也可以把日志信息通过网络发出去。
这里出现了一个服务提供者他会去实现接口,并且调用printLog
打印日志实现他自己对日志输出位置的需求。
这时服务提供者需要做两件事件
- 实现接口定义者定义的接口
- 创建文件夹
META-INF/services
,其中放入以该接口的全类名为文件名的文件,文件第一行写入你的实现类的全类名
这时再调用printLog
就会调用实现类实现需要的效果了
看看ServiceLoader
的源码其实就会发现,他内部其实是读取了这个文件夹META-INF/services/
下的文件内容,利用反射机制创建了接口的实现类对象。
以上演示代码提交到了github,https://github.com/chengpei/spi-demo
实际应用场景介绍
这里以hutool库中的模版引擎封装类TemplateUtil
为例,介绍在实际应用场景中,他是如何利用SPI机制实现多种模版引擎的兼容。
通过一行代码可以创建一个模版引擎
TemplateEngine templateEngine = TemplateUtil.createEngine(new TemplateConfig());
但是模版引擎是有很多实现类的,如图:
他是怎么确定要使用哪个实现类呢?这里其实如果我们在项目里添加了Freemarker的依赖的话,那么这里创建的就是FreemarkerEngine
,如果项目中添加了Beetl的依赖,那些这里创建的就是BeetlEngine
实现类,这里就是使用了SPI机制。
看源码这里是创建的文件,并且把所有的实现类都放进去了
根据源码最后创建引擎实现类时调用了这个方法
public static <T> T loadFirstAvailable(Class<T> clazz) {
final Iterator<T> iterator = load(clazz).iterator();
while (iterator.hasNext()) {
try {
return iterator.next();
} catch (ServiceConfigurationError ignore) {
// ignore
}
}
return null;
}
传进来的clazz当然是TemplateEngine.class
接口,这里的load实际上就是调用的ServiceLoader.load()
,这里循环会依次获取以上所有文件里的实现类,但是如果项目里没有beetl相关的依赖这里是会报错的,所以这里也捕获了异常什么也不做,继续找下一个实现类加载,因为的项目中有freemarker依赖所以加载FreemarkerEngine
实现类没有报错,这里就直接返回不再继续遍历了,所以这个方法就是加载第一个可用的实现类
以上就是hutool库中,SPI机制在模版引擎工具类中的应用了,通过演示代码实操了解原理,再结合其他开源项目中的应用,可以更好的帮助我们理解及运用。