借用dubbo官网介绍的设计图,如下:
image** 说明:图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。**
为了更好的从源码的角度去看设计,需要先理清楚SPI到底是什么东西~
一、基本介绍
SPI 全称为Service Provider Interface,是一种服务发现机制。
本质:将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。
好处:优势体现在可以动态的替换实现类,这样可以更好的为我们的程序提供扩展功能。
二、SPI示例
2.1 java SPI 示例
首先定义一个接口:
package com.test;
import com.alibaba.dubbo.common.extension.SPI;
// @SPI("apple")
public interface Fruit {
void eat();
}
两个不同的实现类:
package com.test;
public class Apple implements Fruit {
@Override
public void eat() {
System.out.println("Hello, I am apple");
}
}
package com.test;
public class Banana implements Fruit {
@Override
public void eat() {
System.out.println("Hello, I am Banana.");
}
}
接下来 META-INF/services 文件夹下创建一个文件,名称为 Fruit 的全限定名 com.test.Fruit。文件内容为实现类的全限定的类名,如下:
imagecom.test.Apple
com.test.Banana
测试如下:
package com.test;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
import java.util.ServiceLoader;
public class JavaSPITest {
public static void main(String[] args) {
// java spi机制
ServiceLoader<Fruit> serviceLoader = ServiceLoader.load(Fruit.class);
System.out.println("Java SPI");
serviceLoader.forEach(Fruit::eat);
// dubbo spi机制
ExtensionLoader<Fruit> extensionLoader =
ExtensionLoader.getExtensionLoader(Fruit.class);
Fruit apple = extensionLoader.getExtension("apple");
apple.eat();
Fruit banana = extensionLoader.getExtension("banana");
banana.eat();
}
}
Hello, I am apple
Hello, I am Banana.
Process finished with exit code 0
Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在** META-INF/dubbo** 路径下(同样在该路径下根据接口的全限定名称创建文件),配置内容如下:
apple=com.test.Apple
banana=com.test.Banana
与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外,在测试 Dubbo SPI 时,需要在** Fruit 接口上标注 @SPI 注解**。
另外,还增加了IOC和AOP等特性,这部分在源码解析时会体现。
统一说明一下,Java SPI 和Dubbo SPI区别?
1.Java SPI虽然也实现了延迟加载,但是基本只能遍历全部,做不到按需进行指定实例化,导致了资源浪费。
2.Dubbo SPI实现了按需加载指定的实现类,另外还增加IOC和AOP等特性。
三、Dubbo SPI 源码分析
代码如下:
ExtensionLoader<Fruit> extensionLoader = ExtensionLoader.getExtensionLoader(Fruit.class);
Fruit apple = extensionLoader.getExtension("apple");
说明:首先通过 ExtensionLoader 的 getExtensionLoader 方法获取一个 ExtensionLoader 实例,然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。
代码如下:
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap();
// ...
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
if (type == null) {
throw new IllegalArgumentException("Extension type == null");
} else if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
} else if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type + ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
} else {
// loader从缓存中获取
ExtensionLoader<T> loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
if (loader == null) {
// 没有则新建,将loader放入map中
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type));
loader = (ExtensionLoader)EXTENSION_LOADERS.get(type);
}
return loader;
}
}
public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
// 获取默认的拓展实现类
return getDefaultExtension();
}
// Holder,用于持有目标对象
final Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
// 双重检查
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 创建拓展实例
instance = createExtension(name);
// 将实例设置到holder中
holder.set(instance);
}
}
}
return (T) instance;
}
重点来看看双重检查具体做了哪些事情,有哪些门道~上代码
package org.apache.dubbo.common.utils;
/**
* Helper Class for hold a value.
*/
public class Holder<T> {
private volatile T value;
public void set(T value) {
this.value = value;
}
public T get() {
return value;
}
}
代码中使用了volatile关键字,有什么作用呢?具体请见解析java-volatile解析
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>(64);
private T createExtension(String name) {
// 从配置文件中加载所有的拓展类,可得到“配置项名称”到“配置类”的映射关系表
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 通过反射创建实例
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 向实例中注入依赖
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
// 循环创建 Wrapper 实例
for (Class<?> wrapperClass : wrapperClasses) {
// 将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。
// 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量
instance = injectExtension(
(T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("...");
}
}
createExtension 方法包含了如下的步骤:
1.通过 getExtensionClasses 获取所有的拓展类
2.通过反射创建拓展对象
3.向拓展对象中注入依赖
4.将拓展对象包裹在相应的 Wrapper 对象中
第一个步骤加载扩展类的关键,第三和第四个步骤是Dubbo IOC和AOP的具体实现。
3.1 获取所有的扩展类
在通过名称获取拓展类之前,首先需要根据配置文件解析出拓展项名称到拓展类的映射关系表(Map<名称, 拓展类>),之后再根据拓展项名称从映射关系表中取出相应的拓展类即可
private Map<String, Class<?>> getExtensionClasses() {
// 从缓存中获取已加载的拓展类
Map<String, Class<?>> classes = cachedClasses.get();
// 双重检查
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// 加载拓展类
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
先检查缓存,若缓存未命中,则通过 synchronized 加锁。加锁后再次检查缓存,并判空。此时如果 classes 仍为 null,则通过 loadExtensionClasses 加载拓展类.
下面分析 loadExtensionClasses 方法的逻辑。
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;
}
/**
* extract and cache default extension name if exists
*/
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 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。
加载策略代码如下:
private static volatile LoadingStrategy[] strategies = loadLoadingStrategies();
// ...
private static LoadingStrategy[] loadLoadingStrategies() {
return stream(load(LoadingStrategy.class).spliterator(), false)
.sorted()
.toArray(LoadingStrategy[]::new);
}
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
LoadingStrategy.png
加载过程解析
ServiceLoader.load方法,利用的是jdk自带的spi机制,加载 LoadingStrategy类的实现类,
jdk的spi机制约定了,接口实现类的描述文件存放位置为:META-INF/services/,文件名称为接口的全限定名,即 org.apache.dubbo.common.extension.LoadingStrategy,文件内容如下:
org.apache.dubbo.common.extension.DubboInternalLoadingStrategy
org.apache.dubbo.common.extension.DubboLoadingStrategy
org.apache.dubbo.common.extension.ServicesLoadingStrategy
loadExtensionClasses 方法总共做了两件事情,一是对 SPI 注解进行解析,二是调用 loadDirectory 方法加载指定文件夹配置文件。SPI 注解解析过程比较简单,无需多说。下面我们来看一下 loadDirectory 做了哪些事情。
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
// fileName = 文件夹路径 + type 全限定名
String fileName = dir + type.getName();
try {
Enumeration<java.net.URL> urls;
ClassLoader classLoader = findClassLoader();
// 根据文件名加载所有的同名文件
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);
}
}
} catch (Throwable t) {
logger.error("...");
}
}
网友评论