java SPI机制
可以参考的视频:
这个看了视频和文档都没有看懂呢?感觉现在的状态也太差了吧?有点奇怪了!
我觉得现在还是需要去指定一些目标!现在我不太想追求速度了,现在追求扎实的基础!(优先去先将思维导图的内容先学了!其他的若是实在看不懂也可以先放着!)
虽然看不懂,所以我觉得还是优先将流程保存下来,要么多看几遍,要么就放着。
什么是 SPI?
SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。
与 API 的区别
- 调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API
- 这种情况下,接口和实现都是放在实现方的包中。调用方通过接口调用实现方的功能,而不需要关心具体的实现细节。
- 当接口存在于调用方这边时,这就是 SPI 。
- 由接口调用方确定接口规则,然后由不同的厂商根据这个规则对这个接口进行实现,从而提供服务。
SLF4J 示例
Service Provider Interface
先建立 Service Provider Interface
项目:
│ service-provider-interface.iml
│
├─.idea
│ │ .gitignore
│ │ misc.xml
│ │ modules.xml
│ └─ workspace.xml
│
└─src
└─edu
└─jiangxuan
└─up
└─spi
Logger.java
LoggerService.java
Main.class
根据要求将 spi
的 java
文件写好,运行 main
函数之后会发现:
此时我们只是空有接口,并没有为 Logger
接口提供任何的实现,所以输出结果中没有按照预期打印相应的结果。
这个时候将项目打包为 jar
包。
Service Provider
建立新项目 Service Provider
:需要导入上面的jar
包
│ service-provider.iml
│
├─.idea
│ │ .gitignore
│ │ misc.xml
│ │ modules.xml
│ └─ workspace.xml
│
├─lib
│ service-provider-interface.jar
│
└─src
├─edu
│ └─jiangxuan
│ └─up
│ └─spi
│ └─service
│ Logback.java
│
└─META-INF
└─services
edu.jiangxuan.up.spi.Logger
新建 Logback
类来继承 Logger
类。
然后在新建 SPI
的全类名(edu.jiangxuan.up.spi.Logger
),写入 Logback
的全类名:edu.jiangxuan.up.spi.service.Logback
Java 中的 SPI 机制就是在每次类加载的时候会先去找到: class 相对目录下的 META-INF 文件夹下的 services 文件夹下的文件(即 edu.jiangxuan.up.spi.Logger
),将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类 (即 Logback.java
),找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。
一些规范要求:
- 文件名一定要是接口的全类名
- 内容一定要是实现类的全类名
- 实现类可以有多个,直接在配置中换行就好了(多个实现类的时候,会一个一个的迭代加载)。
然后一样将 service-provider
项目打包成 jar
包,即服务提供方的实现。
通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。
测试
LoggerService loggerService = LoggerService.getService();
loggerService.info("你好");
loggerService.debug("测试Java SPI 机制");
输出:
//生效
Logback info 打印日志:你好
Logback debug 打印日志:测试 Java SPI 机制
//若不导入具体实现的包
info 中没有发现 Logger 服务提供者
debug 中没有发现 Logger 服务提供者
这里若想要换一种实现,只需要换一个实现(service-provider
)的 jar
包(即 SLF4J
原理)。
ServiceLoader(SPI 的关键)
主要流程:
- 通过 URL 工具类从
jar
包的/META-INF/services
目录下面找到对应的文件, - 读取这个文件的名称找到对应的 spi 接口,
- 通过
InputStream
流将文件里面的具体实现类的全类名读取出来, - 根据获取到的全类名, 先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象,
- 将构造出来的实例对象添加到 Providers 的列表中。
一个简易实现版本的 ServiceLoader
:
package edu.jiangxuan.up.service;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Constructor;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
public class MyServiceLoader<S> {
// 对应的接口 Class 模板
private final Class<S> service;
// 对应实现类的 可以有多个,用 List 进行封装
private final List<S> providers = new ArrayList<>();
// 类加载器
private final ClassLoader classLoader;
// 暴露给外部使用的方法,通过调用这个方法可以开始加载自己定制的实现流程。
public static <S> MyServiceLoader<S> load(Class<S> service) {
return new MyServiceLoader<>(service);
}
// 构造方法私有化
private MyServiceLoader(Class<S> service) {
this.service = service;
this.classLoader = Thread.currentThread().getContextClassLoader();
doLoad();
}
// 关键方法,加载具体实现类的逻辑
private void doLoad() {
try {
// 读取所有 jar 包里面 META-INF/services 包下面的文件,这个文件名就是接口名,然后文件里面的内容就是具体的实现类的路径加全类名
Enumeration<URL> urls = classLoader.getResources("META-INF/services/" + service.getName());
// 挨个遍历取到的文件
while (urls.hasMoreElements()) {
// 取出当前的文件
URL url = urls.nextElement();
System.out.println("File = " + url.getPath());
// 建立链接
URLConnection urlConnection = url.openConnection();
urlConnection.setUseCaches(false);
// 获取文件输入流
InputStream inputStream = urlConnection.getInputStream();
// 从文件输入流获取缓存
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
// 从文件内容里面得到实现类的全类名
String className = bufferedReader.readLine();
while (className != null) {
// 通过反射拿到实现类的实例
Class<?> clazz = Class.forName(className, false, classLoader);
// 如果声明的接口跟这个具体的实现类是属于同一类型,(可以理解为Java的一种多态,接口跟实现类、父类和子类等等这种关系。)则构造实例
if (service.isAssignableFrom(clazz)) {
Constructor<? extends S> constructor = (Constructor<? extends S>) clazz.getConstructor();
S instance = constructor.newInstance();
// 把当前构造的实例对象添加到 Provider的列表里面
providers.add(instance);
}
// 继续读取下一行的实现类,可以有多个实现类,只需要换行就可以了。
className = bufferedReader.readLine();
}
}
} catch (Exception e) {
System.out.println("读取文件异常。。。");
}
}
// 返回spi接口对应的具体实现类列表
public List<S> getProviders() {
return providers;
}
}
总结
SPI
的本质还是通过反射来完成的:我们按照规定将要暴露对外使用的具体实现类在 META-INF/services/
文件下声明。
优点:通过 SPI 机制能够大大地提高接口设计的灵活性,
缺点:
- 遍历加载所有的实现类,这样效率还是相对较低的;
- 当多个
ServiceLoader
同时load
时,会有并发问题。