说一说 Java、Spring、Dubbo 三者 SPI 机制的原理和区别
什么是SPI机制?
SPI全称为Service Provider Interface,是一种动态替换发现的机制,一种解耦非常优秀的思想,SPI可以很灵活的让接口和实现分离,让api提供者只提供接口,第三方来实现,然后可以使用配置文件的方式来实现替换或者扩展,在框架中比较常见,提高框架的可扩展性。
简单来说SPI是一种非常优秀的设计思想,它的核心就是解耦、方便扩展。
Java SPI机制--ServiceLoader
ServiceLoader是Java提供的一种简单的SPI机制的实现,Java的SPI实现约定了以下两件事:
- 文件必须放在
META-INF/services/目录底下 - 文件名必须为接口的全限定名,内容为接口实现的全限定名
这样就能够通过ServiceLoader加载到文件中接口的实现。
接下来我们来简单实现一个基于ServiceLoader的案例.
//1.创建一个接口
public interface LoaderBanlance {
public void load();
}
//2.创建该接口的实现类
public class LoaderBanlanceImpl1 implements LoaderBanlance {
@Override
public void load() {
System.out.println("LoaderBanlanceImpl 已加载");
}
}
public class LoaderBanlanceImpl2 implements LoaderBanlance {
@Override
public void load() {
System.out.println("LoaderBanlanceImpl2 已加载");
}
}
这里,基础的代码就已经有了,接下来我们去创建文件.
//1.文件的内容为实现类的全类名
com.itheima.service.impl.LoaderBanlanceImpl1
com.itheima.service.impl.LoaderBanlanceImpl2
到这里我们的前置代码就准备完成了,接下来编写测试类.
public class LoaderDemo {
public static void main(String[] args) {
ServiceLoader<LoaderBanlance> loader = ServiceLoader.load(LoaderBanlance.class);
Iterator<LoaderBanlance> iterator = loader.iterator();
while (iterator.hasNext()) {
//这里迭代器会遍历配置文件内容,去生成对应的实现类
LoaderBanlance loaderBanlance = iterator.next();
loaderBanlance.load();
System.out.println("获取到对象"+loaderBanlance);
}
}
}
运行结果如下:
这里我们可以看到,不止实现类对象被我们获取到了,我们还可以使用实现类的方法.
那么接下来就有一个问题了,ServiceLoader是如何实现的呢?(这里以JDK17的源码为例,JDK版本不同可能导致源码不一样)
private Class<?> nextProviderClass() {
if (configs == null) {
try {
//PREFIX = "META-INF/ services/"
//这里拼接了一个配置文件路径
//(META-INF/services/com.itheima.service.LoaderBanlance)
String fullName = PREFIX + service.getName();
//下面这堆代码就是利用不同的类加载器获取资源
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else if (loader == ClassLoaders.platformClassLoader()) {
// The platform classloader doesn't have a class path,
// but the boot loader might.
if (BootLoader.hasClassPath()) {
configs = BootLoader.findResources(fullName);
} else {
configs = Collections.emptyEnumeration();
}
} else {
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 解析配置文件内容,生成实现类名迭代器
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return null;
}
// parse()会解析ClassLoader获取的资源
pending = parse(configs.nextElement());
}
//解析下一个全类名
String cn = pending.next();
try {
return Class.forName(cn, false, loader); // 通过类加载器加载类(不初始化类)
} catch (ClassNotFoundException x) { // 类未找到(可能因模块隔离或路径错误)
fail(service, "Provider " + cn + " not found"); // 记录错误并终止加载
return null;
}
}
结合源码,我们可以了解到,JAVA SPI就是通过去获取META-INF/services/目录底下的以**接口全类名命名的文件,然后通过ClassLoader获取到资源,其实就是接口的全限定名文件对应的资源,然后交给****parse**方法解析资源.
所以其实不难发现ServiceLoader实现原理比较简单,总结起来就是通过IO流读取META-INF/services/接口的全限定名文件的内容,然后反射实例化对象。
Spring SPI机制--SpringFactoriesLoader
Spring的SPI机制的约定如下:
- 配置文件必须在
META-INF/目录下,文件名必须为spring.factories - 文件内容为键值对,一个键可以有多个值,只需要用逗号分割就行,同时键值都需要是类的全限定名,键和值可以没有任何类与类之间的关系,当然也可以有实现的关系。
所以也可以看出,Spring的SPI机制跟Java的不论是文件名还是内容约定都不一样。
同样,我们也通过一个案例来实现:
//1.创建一个接口
public interface LoadBanlance {
void load();
}
//2.创建接口实现类
public class LoadBlanceImpl implements LoadBanlance {
@Override
public void load() {
System.out.println("LoadBlanceImpl 已经加载.....");
}
}
接下来我们去创建文件:
基础构建完成,接下来创建一个测试类:
public class Test {
public static void main(String[] args) {
List<LoadBanlance> loadBanlances = SpringFactoriesLoader
.loadFactories(LoadBanlance.class, LoadBanlance.class.getClassLoader());
for (LoadBanlance loadBanlance : loadBanlances) {
loadBanlance.load();
System.out.println("获取到:"+loadBanlance);
}
}
}
运行结果如下:
我们依旧结合源码解析一下:
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
// 1. 检查缓存中是否已存在该 ClassLoader 的解析结果
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
Map<String, List<String>> result = new HashMap();
//2.依旧是通过类加载器获取META-INF/spring.factories下的
try {
Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");
//3.遍历配置文件的url
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
//将配置文件解析为 Properties 对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
//遍历 Properties 中的每个键值对,
while(var6.hasNext()) {
//......
//将实现类名添加到对应接口的列表中
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
//将接口列表去重后转为一个不可变集合
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
//添加到缓存,避免重复解析配置文件,提升性能。
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
IOException ex = var14;
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", ex);
}
}
}
tip : Spring Boot 的自动装配其实就是Spring SPI的应用场景
Spring SPI也支持加载多个实现类,全类名用逗号隔开:
与Java SPI机制对比
首先Spring的SPI机制对Java的SPI机制对进行了一些简化,Java的SPI每个接口都需要对应的文件,而Spring的SPI机制只需要一个spring.factories文件。
其次是内容,Java的SPI机制文件内容必须为接口的实现类,而Spring的SPI并不要求键值对必须有什么关系,更加灵活。
第三点就是Spring的SPI机制提供了获取类限定名的方法loadFactoryNames,而Java的SPI机制是没有的。通过这个方法获取到类限定名之后就可以将这些类注入到Spring容器中,用Spring容器加载这些Bean,而不仅仅是通过反射。
但是Spring的SPI也同样没有实现获取指定某个指定实现类的功能,所以要想能够找到具体的某个实现类,还得依靠具体接口的设计。
Dubbo SPI机制--ExtensionLoader
ExtensionLoader是dubbo的SPI机制的实现类。每一个接口都会有一个自己的ExtensionLoader实例对象,这点跟Java的SPI机制是一样的。
同样地,Dubbo的SPI机制也做了以下几点约定:
- 接口必须要加@SPI注解
- 配置文件可以放在
META-INF/services/、META-INF/dubbo/internal/、META-INF/dubbo/、META-INF/dubbo/external/这四个目录底下,文件名也是接口的全限定名 - 内容为键值对,键为短名称(可以理解为spring中Bean的名称),值为实现类的全限定名
我们在接口上添加注解:
@SPI
public interface LoadBanlance {
void load();
}
这里我们选择META-INF/dubbo/ 的方式
在文件中,也是采用键值对的形式
public class Test {
public static void main(String[] args) {
ExtensionLoader<LoadBanlance> extensionLoader = ExtensionLoader
.getExtensionLoader(LoadBanlance.class);
extensionLoader.getExtension("random").load();
extensionLoader.getExtension("random2").load();
LoadBanlance random = extensionLoader.getExtension("random");
LoadBanlance random2 = extensionLoader.getExtension("random2");
System.out.println(random);
System.out.println(random2);
}
}
编写测试类并运行,结果如下:
这里可以看出,Dubbo可以获取指定的实现类,而Dubbo其实还有一些其他的特性这里就不一 一展开
总结
| 维度 | Java SPI | Spring SPI | Dubbo SPI |
|---|---|---|---|
| 核心类 | ServiceLoader | SpringFactoriesLoader | ExtensionLoader |
| 配置文件路径 | META-INF/services/接口全限定名 | META-INF/spring.factories | META-INF/dubbo/接口全限定名 |
| 加载方式 | 全量加载,无缓存 | 按需加载,支持 Spring 容器缓存 | 按需加载,支持多级缓存和懒初始化 |
| 扩展点发现 | 基于文件系统遍历 | 基于 Spring 容器扫描 | 基于类加载器和配置文件优先级合并 |
| 依赖注入 | 不支持 | 支持 Spring 容器注入 | 支持 Setter 注入和自动装配 |
| 动态性 | 静态加载 | 运行时动态加载(结合 Spring 上下文) | 动态代理(如 @Adaptive)和自适应路由 |
内容主要参考:阿里一面:说一说Java、Spring、Dubbo三者SPI机制的原理和区别大家好,我是三友~~ 今天来跟大家聊一聊Ja - 掘金