这一篇我们先不讲Dubbo中的具体业务逻辑,我们来打基础,聊一聊Dubbo中的SPI机制。
Dubbo SPI是干啥的
了解一个技术,得先知道它是为了解决什么问题而产生的。那么Dubbo SPI是干什么的呢?
按照官网的描述,Dubbo是一款高性能的RPC框架,是为了解决应用间的服务治理问题而诞生的。
服务治理会涉及到很多方面的内容,如网络连接、集群容错、服务路由、负载均衡等。这么多的内容,每个都有不同的解决方案。如网络连接,可以由netty实现,也可以由mina实现。
Dubbo并没有局限于某一种解决方案,而是博采众长。它通过一种机制,让用户可以自行扩展加载想要的功能实现。这种机制就是Dubbo SPI。
Dubbo SPI与Java SPI的渊源
你可能了解过Java SPI机制(如果不了解,建议了解一下),Dubbo SPI是在Java SPI的基础之上,又进行了一层功能的扩展得到的。相较于Java SPI方式,Dubbo SPI具有以下几个方面的优势:
- 按需加载
Java SPI无论你是否需要这个扩展类,都会将其加载到内存中。这样可能会造成资源浪费。Dubbo则不然,采用了一种按需加载扩展的方式,避免不必要的资源浪费 - 扩展间的IOC和AOP
Dubbo SPI机制还实现了一种扩展间的注入与切入功能。简单来说,一个扩展类可以注入另一个扩展类中,外层包装的扩展可以做更多的事,如:流量统计、监控等,好处不言而喻。
Dubbo SPI使用
Dubbo SPI的使用和Java SPI非常相像,也并不复杂,这里就不再赘述。如果你不太了解,建议去官网看一下。
一个普通扩展类是如何加载的
在Dubbo中,标注了@SPI的接口,即被认为是Dubbo SPI扩展类.。
接下来我们聊聊一个普通Dubbo SPI扩展类是如何加载的。首先从Dubbo中最长见到的一种扩展调用方式开始:
ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(DubboProtocol.NAME);
这行代码顾名思义,就是先查找Protocol这个类的加载器,然后利用加载器获取 Dubbo协议扩展类。
前半段的加载器获取逻辑我们暂时忽略,先从getExtension来说起。
不看源码实现,先想一想,如果要你实现一个getExtension方法,你会做哪些事情?
- 扩展类加载完了之后,你是不是要将它缓存起来,方便下次直接获取?
- 扩展类是从哪里获取的?对,配置文件。那么是不是要先读取文件,知道有哪些扩展类?
- 上面说到扩展类支持IOC和AOP,那么在实例化扩展类的时候,是否应该有对扩展类中进行注入的逻辑?
带着上面几个猜测,我们来看一下getExtension的源码,看看一个普通扩展类是如何加载进来的。
public T getExtension(String name) {
// $-- 空校验
if (name == null || name.length() == 0)
throw new IllegalArgumentException("Extension name == null");
// $-- 特殊处理,true则进行默认扩展类加载
if ("true".equals(name)) {
return getDefaultExtension();
}
// $-- 尝试根据name从缓存中获取类实例,没有则创建
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
// $-- double check
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// $-- 创建扩展类,并进行缓存
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
在getExtension方法里,Dubbo主要进行了空校验,并对一些特殊逻辑进行判断处理。接着就是一套“查缓存,缓存不存在创建并缓存”的套路了。在Dubbo的源码里存在大量这样的缓存套路使用,这样的缓存使用对提升效率是非常明显的。
注意:这里缓存的是扩展类的实例
接下来看一下createExtension创建扩展类的逻辑。
private T createExtension(String name) {
// $-- 先获取该name对应的Class类
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);
}
// $-- 扩展类实例的依赖注入(setter)
injectExtension(instance);
// $-- 对包装扩展类进行依赖注入(constructor)
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
type + ") could not be instantiated: " + t.getMessage(), t);
}
}
createExtension先要根据name获取对应的扩展类,如果系统中都没有这个扩展类,那么生成扩展类实例就无从谈起了。
然后又是一套缓存的套路,通过反射创建了该扩展类的实例,放入缓存中。
接下来就是对扩展类进行IOC注入的逻辑了,主要对扩展类实例内部的setter方法进行注入。以上面DubboProtocol的加载为例,这里就是找DubboProtocol类中的setter方法,如果有扩展类可以注入,就进行注入。
随后是对包装扩展类进行依赖注入,使用的是构造器注入。这里与上述setter方法是不同的。同样以DubboProtocol为例,这里是对系统中以Protocol为构造函数的扩展类进行注入,注入的就是当前的DubboProtocol类。相当于给DubboProtocol外面装饰了一下再返回(装饰器模式)。举个例子,如:ProtocolListenerWrapper类
从文件中加载扩展类
关于扩展类注入的逻辑我们稍后再聊,先看一下Dubbo从文件中加载扩展类Class的实现逻辑。
方法getExtensionClasses会加载当前系统中所有的扩展点类
private Map<String, Class<?>> getExtensionClasses() {
// $-- 缓存老套路
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
// $-- 加载所有扩展类,生成包含所有扩展类Class的一个Map
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}
这里没有啥逻辑,依然是一个缓存的老套路。不同的是这里缓存的是扩展类Class,而不是实例。
加载的逻辑还要往下看
private Map<String, Class<?>> loadExtensionClasses() {
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if ((value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
// $-- @SPI注解只允许指定一个默认值
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
// $-- @SPI指定了一个默认值,缓存起来
if (names.length == 1) cachedDefaultName = names[0];
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
// $-- 加载扩展类资源目录
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadDirectory(extensionClasses, DUBBO_DIRECTORY);
loadDirectory(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
这里首先对扩展类@SPI注解的使用进行了一下校验。如果@SPI注解中配置了两个默认值,则抛异常。方法中的type就是我们要加载的Protocol接口,Protocol接口上使用了@SPI(“dubbo”)进行修饰,代表默认使用dubbo扩展类。
随后就是文件的读取与扩展类的加载了。这里可以看到,Dubbo默认的扩展类目录有以下三个:
- META-INF/services/
- META-INF/dubbo/
- META-INF/dubbo/internal/
那么它是如何读取文件,加载扩展类资源的呢,且看loadDirectory方法
private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) {
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("Exception when load extension class(interface: " +
type + ", description file: " + fileName + ").", t);
}
}
这里的主要逻辑实际上是获取类加载器,然后将文件的读取交给loadResource方法
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));
try {
String line;
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;
int i = line.indexOf('=');
if (i > 0) {
// $-- name为i扩展类的key,如 @SPI("dubbo")中的dubbo,line为类名
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
// $-- 反射创建class并加载
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
} 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);
}
}
}
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", class file: " + resourceURL + ") in " + resourceURL, t);
}
}
到这里,我们终于看到熟悉的文件读取操作了。
Dubbo扩展类文件的格式一般都是 “key=value” 样式的,并且支持 “#” 作为注释符的。
具体的加载类操作依然要往下看,loadClass方法是真正的加载类方法。
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
// $-- 校验扩展类与接口是否类型匹配
if (!type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + "is not subtype of interface.");
}
// $-- 该类上是否使用了Adaptive注解,有则进行缓存
if (clazz.isAnnotationPresent(Adaptive.class)) {
if (cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (!cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
} else if (isWrapperClass(clazz)) {
// $-- 是否为包装扩展类(构造函数注入),是则加入set缓存
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} else {
clazz.getConstructor();
// $-- 此处应该是兼容旧方法(@Extension注解)的处理逻辑
if (name == null || name.length() == 0) {
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 (names != null && names.length > 0) {
// $-- 是否为自激活扩展类,是则加入自动激活缓存
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
// $-- 普通扩展类,类名加入缓存,类加入缓存
for (String n : names) {
if (!cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
}
}
}
}
}
这里首先校验了扩展类类型。
随后对@Adaptive注解进行了判断处理。如果类上标注了@Adaptive,则代表其为默认实现,会进行缓存。关于@Adaptive注解,我们这里暂时不说,后续再聊。
接着判断了该扩展类是否存在包装扩展类,如果有的话,就加入到缓存。包装扩展类的逻辑,我们在注入的时候再统一讲,这里先知道有这么一层处理逻辑。
如果上述特殊场景都不满足,那么我们就直接进入默认的加载处理逻辑:首先判断了扩展类的name的取值,此处兼容了对旧的@Extension注解的处理逻辑。随后就自激活扩展类(@Activate注解修饰)和普通扩展类分别加入到相应的缓存中。
这样将三个目录都读取完成之后,扩展类就被直接加载到缓存中了。(o゜▽゜)o☆
扩展类注入
现在让我们将注意力转回到createExtension中。获取到扩展类后,直接进行实例化,然后就是重头戏注入了。
注入分为两种场景,分别是扩展类setter方法注入和包装扩展类构造器注入。
扩展类setter注入
setter方法注入由方法injectExtension实现
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
// $-- set方法注入
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
// $-- 方法上存在@DisableInject注解的,此处不进行注入
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
// $-- 获取要注入的类型
Class<?> pt = method.getParameterTypes()[0];
try {
// $-- 获取要注入的扩展类名,如 setProtcol ==> protocol
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
// $-- 获取注入类实例
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
// $-- 调用set方法进行注入
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
objectFactory为扩展加载器工厂,它的主要作用是生成扩展加载器,下面会详细介绍,这里先忽略。
整个setter注入的逻辑还是非常清晰的,配合代码中的注释,相应应该不用我来解释什么了。
包装扩展类构造器注入
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
首先判断该扩展类是否有包装,这里是通过cachedWrapperClasses缓存来判断的。
对于包装扩展类,随后进行包装扩展类的实例化,以及该包装扩展类的setter注入。
整个逻辑还是非常简单的,唯一的问题在于cachedWrapperClass是如何赋值的呢?
回想读取文件加载类时,会进行该类是否有包装的判断。如果有包装,则加入到cachedWrapperClasses缓存中。
else if (isWrapperClass(clazz)) {
// $-- 是否为包装扩展类(构造函数注入),是则加入set缓存
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
}
isWrapperClass是其判断代码,主要是判断要加载的类是否有当前接口的构造函数
private boolean isWrapperClass(Class<?> clazz) {
try {
// $-- 判断Clazz是否有type类型的构造函数,没有则抛异常
clazz.getConstructor(type);
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
这里你可能有一点懵,我们举Protocol的例子来说明一下吧。
当进行文件读取,加载扩展类读取时,会加载到类ProtocolListenerWrapper。这个类里存在以Protocol为参数的构造方法,说明ProtocolListenerWrapper这个类可以进行包装扩展,因此就会在cachedWrapperClasses中记录下来。
当我们通过createExtension来创建dubbo类型的Protocol时,判断到其包装扩展类缓存cachedWrapperClasses中存在ProtocolListenerWrapper类型的包装类时,就会对DubboProtocol进行装饰,返回的是ProtocolListenerWrapper类。(DubboProtocol的包装类不只ProtocolListenerWrapper,这里仅仅是作为一个例子进行理解)
另外需要注意的是,代码中并没有对包装的顺序进行定义,所以理论上,文件读取时,加载到cachedWrapperClasses缓存中的顺序会直接影响到包装的结果。
ExtensionLoader的获取
上述就是整个普通扩展类加载的流程了,不知道你是否了然于胸了呢?
结合我个人的经历,当我初读这段代码时,对type这个字段很迷惑,特别是在包装扩展类缓存判断这一块儿。那么这个type到底是啥?
别急,让我们将视线转换回一切的起点
ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(DubboProtocol.NAME);
你可能已经猜到了,这里的Protocol.class就是type!
现在是时候分析一下这段代码的前半段了:如何获取某个扩展类的ExtensionLoader?
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 interface!");
}
// $-- 是否有@SPI注解校验
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
// $-- 缓存套路
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
// $-- 这里创建type类型的ExtensionLoader
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
获取扩展类加载器的这段代码里,主要先对要加载的类做了一些校验,然后是缓存的老套路。我们主要来看一下创建ExtensionLoader的方法,也就是ExtensionLoader的构造方法。
private ExtensionLoader(Class<?> type) {
this.type = type;
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
到这里可以看到,我们一直疑惑的type原来是在构造方法里被赋值的。type代表的实际上是我们将要获取的扩展类的类型定义。
objectFactory是扩展类加载器工厂,用来生成扩展类加载器ExtensionLoader的。这里先判断了一下我们要加载的类是否就是扩展类加载器工厂本身,如果是本身的话,则返回null,不用加载。否则,就再通过扩展类加载机制获取ExtensionLoader的扩展类。
ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
像不像一个“套娃”?😂
Dubbo SPI中的ExtensionFactory体系
说到这里,我们顺便聊聊Dubbo 扩展类工厂的实现吧。
在Dubbo中,存在三种类型的扩展类加载器工厂。
Dubbo中存在ExtensionFactory的三个实现类:SpiExtensionFactory、SpringExtensionFactory、AdaptiveExtensionFactory。其中AdaptiveExtensionFactory上有@Adaptive注解,代表这是ExtensionFactory的默认实现。
AdaptiveExtensionFactory的源码如下
@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {
// $-- 持有了所有的ExtensionFactory
private final List<ExtensionFactory> factories;
public AdaptiveExtensionFactory() {
// $-- 通过SPI获取扩展类加载器工厂,然后放入factories中
ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
for (String name : loader.getSupportedExtensions()) {
list.add(loader.getExtension(name));
}
factories = Collections.unmodifiableList(list);
}
@Override
public <T> T getExtension(Class<T> type, String name) {
// $-- 本质上还是调用了SPI和Spring两个ExtensionFactory来getExtension,顺序是SPI->Spring
for (ExtensionFactory factory : factories) {
T extension = factory.getExtension(type, name);
if (extension != null) {
return extension;
}
}
return null;
}
}
实际上AdaptiveExtensionFactory只是一层壳,内部也是调用其他两个ExtensionFactory的getExtension方法来实现的,且顺序为Spi->Spring
SpringExtensionFactory的中getExtension方法如下
public <T> T getExtension(Class<T> type, String name) {
// $-- 遍历Spring容器,根据名称获取bean
for (ApplicationContext context : contexts) {
if (context.containsBean(name)) {
Object bean = context.getBean(name);
if (type.isInstance(bean)) {
return (T) bean;
}
}
}
logger.warn("No spring extension (bean) named:" + name + ", try to find an extension (bean) of type " + type.getName());
// $-- 如果要获取的bean类型为Object,返回null给它吧(毕竟所有类都是Object的子类)
if (Object.class == type) {
return null;
}
// $-- 遍历Spring容器,根据类型获取bean
for (ApplicationContext context : contexts) {
try {
return context.getBean(type);
} catch (NoUniqueBeanDefinitionException multiBeanExe) {
logger.warn("Find more than 1 spring extensions (beans) of type " + type.getName() + ", will stop auto injection. Please make sure you have specified the concrete parameter type and there's only one extension of that type.");
} catch (NoSuchBeanDefinitionException noBeanExe) {
if (logger.isDebugEnabled()) {
logger.debug("Error when get spring extension(bean) for type:" + type.getName(), noBeanExe);
}
}
}
logger.warn("No spring extension (bean) named:" + name + ", type:" + type.getName() + " found, stop get bean.");
return null;
}
Spring扩展加载器工厂主要是从Spring上下文中获取对应的bean,先根据名称匹配,没匹配到再根据类型匹配,如果还没有匹配到,则返回null。
SpiExtensionFactory的getExtension方法如下
public <T> T getExtension(Class<T> type, String name) {
if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
// $-- 根据类型获取所有的扩展点加载器
ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
// $-- 如果缓存的扩展点加载器不为空,则直接返回Adaptive实例
if (!loader.getSupportedExtensions().isEmpty()) {
return loader.getAdaptiveExtension();
}
}
return null;
}
可以看到,Spi扩展点加载器工厂的getExtension方法非常简单。默认返回的是Adaptive类型的实例。
总结
以上就是一个普通扩展类加载的大致内容了。
由于篇幅已经比较长了,自激活扩展(@Activate)和自适应扩展(@Adaptive)源码将在下一篇博文介绍。