目录
什么是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
// 每个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 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension方法获取扩展点对象。这其中getExtensionLoader 用于从EXTENSION_LOADERS全局缓存中获取与扩展点对应的 ExtensionLoader扩展点执行器,若缓存未命中,则创建一个新的实例
// 缓存 当前接口下所有完整的扩展点实例
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;
}
拿到扩展点执行器后调用ExtensionLoader 的 getExtension 方法,此方法有两个参数name为扩展点名称,wrap标识是否进行Wrapper增强(Wrapper是SPI的一个高级特性,后续章节会讲到)
- 判断扩展点名称是否为true如果是则返回默认的扩展点
- 如果不为true,则获取全局缓存cachedInstances中的扩展点实例
- 接下来就是采用了单例模式的双重检查锁机制来检查缓存,缓存未命中则创建扩展点
下面我们来看一下创建扩展点对象的过程是怎样的。
/**
* 创建扩展点
* 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);
}
......此处省略下面会讲到
}
创建扩展点对象的过程:
- 通过 getExtensionClasses方法获取所有的扩展点
- 通过反射创建扩展点对象
- 向扩展点对象实例中注入依赖
- 将扩展点包裹在相应的 Wrapper 对象中
以上步骤中,第一个步骤是加载扩展点的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。在接下来的讲解中,我将会重点分析 getExtensionClasses 方法的逻辑,以及简单分析 Dubbo IOC 的具体实现。
//缓存解析完的当前接口所有扩展点的 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;
}
我们在通过名称获取扩展点之前,首先需要根据配置文件加载解析出名称到扩展点的映射,也就是 Map<名称, 扩展点>。之后再从 Map 中筛选出响应的扩展点即可。这里也是先检查缓存,若缓存未命中,则采用了单例模式的双重检查锁机制来检查缓存,则加载扩展点。前面所分析的 getExtension 方法中有相似的代码。下面分析 loadExtensionClasses 方法的逻辑。
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
private static LoadingStrategy[] loadLoadingStrategies() {
// 使用 JDK SPI 完成 LoadingStrategy的读取,可以在 dubbo-common模块中找到对应的配置
return stream(load(LoadingStrategy.class).spliterator(), false)
.sorted()
.toArray(LoadingStrategy[]::new);
}
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;
}
// 缓存默认扩展点名称
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];
}
}
}
调用loadExtensionClasses 方法
- 缓存默认扩展点名称
- 之后就是根据约定的好的指定目录下读取配置文件
- 调用loadLoadingStrategies内部使用了JDK SPI来完成 LoadingStrategy的读取,可以在 dubbo-common模块中找到对应的配置
- 调用 loadDirectory 方法加载指定文件夹配置文件
下面我们来看一下 loadDirectory 做了哪些事情
// 完成加载解析及筛选
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir, String type,
boolean extensionLoaderClassLoaderFirst, boolean overridden, String... excludedPackages) {
String fileName = dir + type;
try {
Enumeration<java.net.URL> urls = null;
ClassLoader classLoader = findClassLoader();
// try to load from ExtensionLoader's ClassLoader first
if (extensionLoaderClassLoaderFirst) {
ClassLoader extensionLoaderClassLoader = ExtensionLoader.class.getClassLoader();
if (ClassLoader.getSystemClassLoader() != extensionLoaderClassLoader) {
urls = extensionLoaderClassLoader.getResources(fileName);
}
}
if (urls == null || !urls.hasMoreElements()) {
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
}
// 读取到的文件的完整路径
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL resourceURL = urls.nextElement();
loadResource(extensionClasses, classLoader, resourceURL, overridden, excludedPackages);
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " +
type + ", description file: " + fileName + ").", t);
}
}
该方法先通过 classLoader 获取所有资源链接,然后再通过 loadResource 方法加载资源,接下来我们看一下 loadResource 方法的实现。
// 完成加载解析与区分
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader,
java.net.URL resourceURL, boolean overridden, String... excludedPackages) {
try {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
String line;
String clazz = null;
// 按行读取
while ((line = reader.readLine()) != null) {
// 去掉注释
final int ci = line.indexOf('#');
if (ci >= 0) {
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
// 按=拆分 截取键与值。比如 dubbo=com.alibaba....DubboProtocol
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();// name
clazz = line.substring(i + 1).trim();// com.example.spi.Windows
} else {
clazz = line;
}
if (StringUtils.isNotEmpty(clazz) && !isExcluded(clazz, excludedPackages)) {
// 完成类加载
loadClass(extensionClasses, resourceURL, Class.forName(clazz, true, classLoader), name, overridden);
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException(
"Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL +
", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
}
}
} catch (Throwable t) {
logger.error("Exception occurred when loading extension class (interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}
loadResource 方法用于读取和解析配置文件,并通过反射加载类,最后调用 loadClass 方法进行其他操作。该方法的逻辑如下。
private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*[,]+\\s*");
// 缓存用户自定义的自适应实例Class
private volatile Class<?> cachedAdaptiveClass = null;
// 缓存所有的wrapper class
private Set<Class<?>> cachedWrapperClasses;
// 缓存 默认激活的 Activate信息,key是扩展点名称,value是标注的Activate注解
private final Map<String, Object> cachedActivates = new ConcurrentHashMap<>();
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
// 对加载到的 Class 进行筛选区分
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name,
boolean overridden) throws NoSuchMethodException {
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error occurred when loading extension class (interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + " is not subtype of interface.");
}
// 类上有 Adaptive 注解 ,则缓存
if (clazz.isAnnotationPresent(Adaptive.class)) {
cacheAdaptiveClass(clazz, overridden);
} else if (isWrapperClass(clazz)) {// 如果是wrapper 则缓存
cacheWrapperClass(clazz);
} else {
clazz.getConstructor();// 确保有无参构造
if (StringUtils.isEmpty(name)) {
name = findAnnotationName(clazz);
if (name.length() == 0) {
throw new IllegalStateException(
"No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);
}
}
String[] names = NAME_SEPARATOR.split(name);
if (ArrayUtils.isNotEmpty(names)) {
// 缓存 默认激活的 Class
cacheActivateClass(clazz, names[0]);
for (String n : names) {
cacheName(clazz, n);
// 将 clazz 存入 extensionClasses 中
saveInExtensionClass(extensionClasses, clazz, n, overridden);
}
}
}
}
loadClass 方法操作了不同的缓存,比如 cachedAdaptiveClass(自适应缓存)、cachedWrapperClasses(自动包装缓存)、cachedActivates (扩展点缓存)等等
- 1如果类上有 Adaptive 注解 ,则写入cachedAdaptiveClass缓存
- 判断是否写入cachedWrapperClasses缓存,判断依据则是看是否有接口类型的构造
- 最后将将 clazz 存入 extensionClasses中
到此,关于缓存类加载的过程就分析完了,接下来我们一直返回到createExtension方法,继续下面的逻辑。
// 缓存扩展点的原始实例对象【原始实例:未wrapper,未inject】
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>(64);
/**
* 创建扩展点
* 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);
}
}
回到createExtension方法完成了配置文件解析及加载,筛选之后,
- 创建并保存扩展点原始实例
- 完成扩展点实例的 inject 操作,对原始实例进行依赖注入
- 完成对扩展点实例的Wrapper包装
下面我们在讲下injectExtension方法
private final ExtensionFactory objectFactory;
private ExtensionLoader(Class<?> type) {
this.type = type;
objectFactory =
(type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
private T injectExtension(T instance) {
if (objectFactory == null) {
return instance;
}
try {
for (Method method : instance.getClass().getMethods()) {
if (!isSetter(method)) {// 仅支持set注入
continue;
}
/*
* Check {@link DisableInject} to see if we need autowire injection for this property
*/
// set方法上标注 DisableInject 则放弃注入
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
// 拿到 set方法的参数类型
Class<?> pt = method.getParameterTypes()[0];
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
/*
* Check {@link Inject} to see if we need auto-injection for this property
* {@link Inject#enable} == false will skip inject property phase
* {@link Inject#InjectType#ByName} default inject by name
*/
String property = getSetterProperty(method);// 获取set方法对应的属性名称
// set方法上可以标注 Inject ,指定注入的方式
Inject inject = method.getAnnotation(Inject.class);
if (inject == null) {
injectValue(instance, method, pt, property);
} else {
if (!inject.enable()) {
continue;
}
if (inject.type() == Inject.InjectType.ByType) {
injectValue(instance, method, pt, null);
} else {
injectValue(instance, method, pt, property);
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
private String getSetterProperty(Method method) {
return method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
}
public static boolean isPrimitive(Class<?> cls) {
return cls.isPrimitive() || cls == String.class || cls == Boolean.class || cls == Character.class
|| Number.class.isAssignableFrom(cls) || Date.class.isAssignableFrom(cls);
}
/**
* 完成set注入
* @param instance 当前实例对象
* @param method setXxx方法对应的method
* @param pt 要注入的类型
* @param property 对应的属性名称
*/
private void injectValue(T instance, Method method, Class<?> pt, String property) {
try {
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
- 判断是否支持set注入,不支持则continue
- 如果set方法有标注@DisableInject注解,则continue
- 拿到 set方法的参数类型,如果是原始类型,则continue
- 调用injectValue方法,根据类型与属性名称参数通过调用objectFactory的getExtension获取扩展点对象
- ExtensionFactory的内部也是通过SPI进行实现的,切断通过ExtensionLoader构造器可以发现,ExtensionFactory最后是获取的自适应的实例,而且ExtensionFactory还支持的从不同的扩展点工厂获取实例,包括SPI、Adaptive自适应、Spring
// 完成对扩展点实例的包装
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);
先判断全局缓存中的cachedWrapperClasses是否为空(在loadClass的时候已经将所有的Wrapper Class缓存起来了)
如果不为空时,会对Wrapper进行排序,排序规则会先识别@Activate注解,这个注解中会有order属性来新增包装顺序的控制,通常,数值越小优先级越高。如果没用@Activate注解,则会根据配置文件中的配置先后顺便来包装,配置越靠后,Wrapper则越先执行,如图:
接下来就是遍历所有的wrapper class,并识别有没有标注了@Wrapper注解,Wrapper默认是对所有扩展点进行包装,而@Wrapper注解中支持了对某个扩展点的包装或排除的控制,如图:
matches参数:代表需要包装的SPI扩展点,内部填入的是SPI扩展点名称
mismatches参数:代表要排除的SPI扩展点
到这里extensionLoader.getExtension的整个调用链就已经分析完成了。
小结
本篇文章介绍了SPI 并对Dubbo的SPI源码进行了分析。在 Dubbo SPI 中还有一块重要的逻辑没有进行分析,那就是 Dubbo SPI 的扩展点自适应机制。该机制的逻辑较为复杂,我将会在下一篇文章中进行分析。
最后送大家一句话,不要觉得源码很难从而恐惧阅读源码,要有着求贤若渴的心态,享受学习的过程学习源码中的设计思路及高级的写法,想象在实际业务中有没有可能用到,享受学习之后的成就感。