目录
什么是SPI
SPI 全称为 Service Provider Interface,一种解耦接口和实现的手段,其实现原理是将接口的实现类全名称配置在配置文件中,程序运行阶段去读取配置文件加载实现类,这个机制为程序带来 了很强的扩展性,使 得我们可以很方便的 基于某接口规范去使用任何第三方的实现。
SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。下面,我们先来了解一下 Java SPI 与 Dubbo SPI 的使用方法,然后再来分析 Dubbo SPI 的源码。
SPI 的工作原理
-
定义服务接口:首先定义一个服务接口,通常这个接口会包含在 JAR 文件中。
-
提供服务实现:开发者可以通过创建该接口的具体实现类来提供服务。实现类的信息会被写入到
META-INF/services
目录下的一个文件中,文件名就是服务接口的全限定名,而文件内容则是实现类的全限定名。 -
查找服务实现:当需要使用该服务时,可以通过
ServiceLoader
类来加载所有可用的服务实现。ServiceLoader
会在类路径下搜索META-INF/services
目录中的配置文件,并加载其中列出的服务实现。
SPI 的作用
-
插件化:SPI 提供了一种在运行时动态发现并加载服务实现的方法,这使得系统可以在不知道具体实现的情况下使用各种不同的插件。
-
扩展性:通过 SPI 可以轻松地添加新的服务实现而不必修改现有的代码。
-
灵活性:SPI 允许在部署时选择不同的服务实现,这样可以根据环境的不同选择最合适的实现。
SPI 的缺点
-
性能开销:每次启动应用时,SPI 都会扫描
META-INF/services
目录下的配置文件,如果有很多服务需要加载,这可能会导致较大的性能开销。 -
初始化延迟:由于 SPI 是在运行时加载服务实现的,所以如果服务实现比较复杂或者有很多实现类,那么第一次使用服务可能会有明显的延迟。
-
兼容性问题:如果多个服务提供者同时存在,可能会导致命名冲突或其他兼容性问题。
-
安全性考虑:SPI 加载的类来自外部,如果没有适当的沙箱机制或安全检查,可能会引入安全风险。
尽管 SPI 存在一些缺点,但它仍然是 Java 中非常有用的设计模式,特别是在需要实现插件化和模块化系统时。开发者应该权衡其优缺点,并在适当的情景下使用它。
简单使用JDK的SPI与Dubbo的SPI
Dubbo为什么要使用SPI机制
dubbo作为一个rpc框架,在它发送RPC请求时,整个过程会经历很多个关键事件节点,比如集群容错,负载均衡,数据序列化,通信协议编码,网络传输等,每个关键节点都有抽象出对应的接口,而且有多种不同的实现,且用户可自行扩展,那实际运行阶段 dubbo如何根据用户的配置参数来选择具体的实现呢,这就促使dubbo需要一种可插拔的接口实现发现机制。
dubbo采用微内核架构,将每一个功能接口当作一个可插拔的扩展点接口,内核层面只负责按流程组装并引导执行每个扩展点接口 ,具体的功能和逻辑由具体的扩展点实现来完成,提高了系统的扩展性和灵活性,而SPI就是实现微内核的手段。
dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 的实现类配置放置在 META-INF/dubbo 路径下
Dubbo SPI 源码分析
上面我们简单演示了 Dubbo SPI 的使用方法。接下来我们来分析其源码,
dubbo版本:2.7.19-relesse
我们首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension方法获取扩展点对象。这其中,getExtensionLoader 用于从EXTENSION_LOADERS全局缓存中获取与扩展点对应的 ExtensionLoader扩展点执行器,若缓存未命中,则创建一个新的实例
// 每个SPI接口都对应一个ExtensionLoader实例
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);
@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
}
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!");
}
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
拿到扩展点执行器后调用ExtensionLoader 的 getExtension 方法,此方法有两个参数name为扩展点名称,wrap标识是否进行Wrapper增强(Wrapper是SPI的一个高级特性,后续章节会讲到)
- 判断扩展点名称是否为true如果是则返回默认的扩展点
- 如果不为true,则获取全局缓存cachedInstances中的扩展点实例
- 接下来就是采用了单例模式的双重检查锁机制来检查缓存,缓存未命中则创建扩展点
// 缓存 当前接口下所有完整的扩展点实例
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
@SuppressWarnings("unchecked")
public T getExtension(String name) {
return getExtension(name, true);
}
// 获取或创建扩展点实例
private Holder<Object> getOrCreateHolder(String name) {
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
holder = cachedInstances.get(name);
}
return holder;
}
// 获取扩展点
public T getExtension(String name, boolean wrap) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
return getDefaultExtension();
}
// 扩展点都是单实例的
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name, wrap);
holder.set(instance);
}
}
}
return (T) instance;
}
下面我们来看一下创建扩展点对象的过程是怎样的
- 通过 getExtensionClasses方法获取所有的扩展点
- 通过反射创建扩展点对象
- 向扩展点对象实例中注入依赖
- 将扩展点包裹在相应的 Wrapper 对象中
以上步骤中,第一个步骤是加载扩展点的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。在接下来的讲解中,我将会重点分析 getExtensionClasses 方法的逻辑,以及简单分析 Dubbo IOC 的具体实现。
/**
* 创建扩展点
* 1、完成文件解析,加载,
* 2、完成基础实例的创建
* 3、完成实例的注入
* 4、完成实例的wrapper包装
*/
@SuppressWarnings("unchecked")
private T createExtension(String name, boolean wrap) {
/**
* getExtensionClasses方法很重要:
* 完成了配置文件解析及加载,筛选
*/
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null || unacceptableExceptions.contains(name)) {
throw findException(name);
}
try {
// 创建并保存扩展点原始实例
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 完成扩展点实例的 inject 操作
injectExtension(instance);
// 完成对扩展点实例的包装
if (wrap) {
List<Class<?>> wrapperClassesList = new ArrayList<>();
if (cachedWrapperClasses != null) {
wrapperClassesList.addAll(cachedWrapperClasses);
wrapperClassesList.sort(WrapperComparator.COMPARATOR);// wrapper 排序
Collections.reverse(wrapperClassesList);
}
if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
for (Class<?> wrapperClass : wrapperClassesList) {
// wrapper 类上可以用 Wrapper 注解来标注 当前wrapper 是否对某扩展点进行增强
Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
if (wrapper == null
|| (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
// wrapper 有接口类型的构造
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
}
}
initExtension(instance);
return instance;// 返回最终的扩展点实例
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}
我们在通过名称获取扩展点之前,首先需要根据配置文件加载解析出名称到扩展点的映射,也就是 Map<名称, 扩展点>。之后再从 Map 中筛选出响应的扩展点即可。这里也是先检查缓存,若缓存未命中,则采用了单例模式的双重检查锁机制来检查缓存,则加载扩展点。前面所分析的 getExtension 方法中有相似的代码。下面分析 loadExtensionClasses 方法的逻辑。
//缓存解析完的当前接口所有扩展点的 Class,key是扩展点名称
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
/**
* 完成 当前SPI扩展点接口的加载,解析,筛选
* @return
*/
private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = loadExtensionClasses();// 只加载解析1次
cachedClasses.set(classes);
}
}
}
return classes;
}
loadExtensionClasses 方法总共做了两件事情
- 对 SPI 注解进行解析,缓存默认扩展点名称
- 调用 loadDirectory 方法加载指定文件夹配置文件
下面我们来看一下 loadDirectory 做了哪些事情
// 注解解析,缓存默认扩展点名称
private void cacheDefaultExtensionName() {
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation == null) {
return;
}
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("More than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if (names.length == 1) {
cachedDefaultName = names[0];
}
}
}
/**
* synchronized in getExtensionClasses
*/
private Map<String, Class<?>> loadExtensionClasses() {
cacheDefaultExtensionName();// 缓存默认扩展点名称
Map<String, Class<?>> extensionClasses = new HashMap<>();
for (LoadingStrategy strategy : strategies) {
loadDirectory(extensionClasses, strategy.directory(), type.getName(), strategy.preferExtensionClassLoader(),
strategy.overridden(), strategy.excludedPackages());
loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"),
strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
}
return extensionClasses;
}
后续明日待完善。。。。
小结
提示:这里可以添加总结
例如:
提供先进的推理,复杂的指令,更多的创造力。
标签:Dubbo,name,instance,扩展,剖析,SPI,源码,type From: https://blog.csdn.net/weixin_55721317/article/details/141960672