1. 简介
Dubbo 良好的扩展性与两个方面密不可分,一是整个框架中针对不同的场景,恰到好处的使用了各种设计模式,二是接下来要讲的加载机制。基于 Dubbo SPI 加载机制,让整个框架的接口和具体实现类解耦,从而奠定了整个框架良好可扩展性的基础。
2. Java SPI
在讲解 Dubbo SPI 之前,我们先了解下 Java SPI 是怎么使用的。SPI 全称 Service Provider Interface,起初是提供给厂商做插件开发的。Java SPI 使用了策略模式,一个接口多种实现。我们只声明接口,具体的实现并不在程序中直接确定,而是由程序之外的配置掌控,用于具体实现的装配。具体步骤如下:
(1) 定义一个接口及对应的方法。
(2)编写该接口的一个实现类。
(3)在 META-INF/services/ 目录下,创建一个以接口全路径命名的文件,如 com.test.spi.PrintService。
(4)文件内容为具体实现类的全路径名,如果有多个,则用分行符分割。
(5)在代码中通过 java.util.ServiceLoader 来加载所有的实现类。
3. Dubbo SPI
与 Java SPI 相比,Dubbo SPI 做了一定的改进和优化:
- JDK 标准的 SPI 会一次性实例化扩展点的所有实现,如果没用上也加载,则浪费资源。而 Dubbo SPI 只是加载配置文件中的类,而不会立即全部初始化。
- 增加了对扩展 IOC 和 AOP 的支持,一个扩展可以直接 setter 注入其他扩展。
3.1 代码示例
声明扩展点:
package com.alibaba.dubbo.examples.spi.api;
import com.alibaba.dubbo.common.extension.SPI;
@SPI("print")
public interface PrintService {
void printInfo();
}
声明扩展点的实现类:
public class PrintServiceImpl implements PrintService {
@Override
public void printInfo() {
System.out.println("hello,world");
}
}
在 META-INF/dubbo/ 目录下,新建配置文件 com.alibaba.dubbo.examples.spi.api.PrintService,内容如下:
print=com.alibaba.dubbo.examples.spi.impl.PrintServiceImpl
3.2 扩展点的配置规范
Dubbo SPI 和 Java SPI 类似,需要在 META-INF/dubbo/ 下放置对应的 SPI 配置文件,文件名称需要命名为接口的全路径名。配置的内容为 key=扩展点实现类的全路径名,如果有多个实现类则使用换行符分隔。其中 key 为 Dubbo SPI 注解中传入的参数。另外,Dubbo SPI 还兼容了 Java SPI 的配置路径,在 Dubbo 启动时,会默认扫描这三个目录下的配置文件:META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal。
3.3 扩展点的特性
从 Dubbo 官方文档中可以知道,扩展类一共包含四种特性:自动包装、自动加载、自适应和自动激活。
1. 自动包装
自动包装是一种扩展类,ExtensionLoader
在加载扩展时,如果发现这个扩展类包含其他扩展点实例作为构造函数的参数,则这个扩展类就会被认定为是Wrapper
类,例如:
public class ProtocolFilterWrapper implements Protocol {
private final Protocol protocol;
public ProtocolFilterWrapper(Protocol protocol) {
if (protocol == null) {
throw new IllegalArgumentException("protocol == null");
}
this.protocol = protocol;
}
......
}
ProtocolFilterWrapper
继承了Protocol
接口,同时其构造函数中又注入了一个Protocol
类型的参数。因此ProtocolFilterWrapper
会被认定为Wrapper
类。这是一种装饰模式,把通用的抽象逻辑封装或对子类进行增强,让子类可以更加专注具体的实现。
2. 自动加载
除了在构造函数中传入其他扩展实例,我们还经常使用setter
方法设置属性值,如果某个扩展类是另一个扩展点类的成员属性,并且拥有setter
方法,那么框架也会自动注入对应的扩展点实例。ExtensionLoader
在执行扩展点初始化的时候,会自动通过setter
方法注入对应的实现类。这里有个问题,如果扩展类属性是一个接口,他有多种实现,那么具体注入哪个呢?这就涉及第三个特性——自适应。
3. 自适应
在 Dubbo SPI 中,我们使用@Adaptive
注解,可以动态地通过 URL 中的参数来确定要使用哪个具体的实现类。从而解决自动加载中的实例注入问题。@Adaptive
注解使用示例如下:
@SPI("netty")
public interface Transporter {
@Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
Server bind(URL url, ChannelHandler handler) throws RemotingException;
@Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
Client connect(URL url, ChannelHandler handler) throws RemotingException;
}
@Adaptive
传入两个 Constants 中的参数,他们的值分别是"server"、“transporter”。当外部调用Transporter#bind
方法时,会动态从传入的参数"URL"中提取 key 参数“server”的 value 值,如果能匹配上某个扩展实现类则直接使用对应的实现类;如果未匹配上,则继续通过第二个 key 参数“transporter”提取 value 的值。如果都未匹配上,则抛出异常。也就是说,如果@Adaptive
中传入了多个参数,则依次进行实现类的匹配,直到最后抛出异常。
这种动态寻找实现类的方式比较灵活,但只能激活一个具体的实现类,如果需要激活多个实现类,如 Filter 可以同时有多个过滤器;根据不同的条件,同时激活多个实现类,如何实现?这就涉及最后一个特性——自动激活。
4. 自动激活
使用@Activate
注解,可以标记对应的扩展点默认被激活启用。该注解还可以通过传入不同的参数,设置扩展点在不同条件下被自动激活。主要的使用场景是某个扩展点的多个实例需要同时启用(比如 Filter 扩展点)。
3.4 扩展点注解
@SPI
注解一般使用在接口上,作用是标记这个接口是一个 Dubbo SPI 接口,既是一个扩展点。注解中有一个 value 属性,表示这个接口的默认实现类。例如上面的@SPI("netty")
,我们可以看到Transporter
接口使用Netty
作为默认实现。
@Adaptive
注解可以标注在类、接口、方法上。如果标注在接口的方法上,则可以通过参数动态的获取实现类。方法级别注解在第一次getExtension
时,会自动生成和编译一个动态的Adaptive
类,从而达到动态实现类的效果。例如:Transporter
接口在 bind 和 connect 两个方法上添加了@Adaptive
注解。Dubbo 在初始化扩展点时,会自动生成一个Transporter$Adaptive
类,里面会实现这两个方法,方法里面会有一些通用的抽象逻辑,通过@Adaptive
传入的参数,找到并调用真正的实现类。当注解放在实现类上时,则整个实现类会直接作为默认实现,不再自动生成的Adaptive
类。在扩展点接口的多个实现里,只能有一个实现类可以加@Adaptive
注解,否则抛出异常。
3.5 ExtensionLoader 工作原理
ExtensionLoader
是整个扩展机制的主要逻辑类,逻辑入口可以分为getExtension
、getAdaptiveExtension
、getActivateExtension
三个,分别是获取普通扩展类、获取自适应扩展类、获取自动激活扩展类。
3.5.1 getExtension 的实现原理
当调用getExtension(String name)
方法时,会先检查缓存中是否有现成的数据,没有则调用createExtension
开始创建。如果 name 为 “true”,则加载并返回默认扩展类。
在调用createExtension
开始创建的过程中,也会检查缓存中是否有配置信息,如果不存在扩展类,则会到 META-INF/services/、META-INF/dubbo/、META-INF/dubbo/internal/ 这几路径中读取所有配置文件,得到对应扩展点实现类的全称。扩展点配置信息加载过程源码如下:
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;
}
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);
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];
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadDirectory(extensionClasses, DUBBO_DIRECTORY);
loadDirectory(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}
检查是否有@SPI
注解。如果有,则获取注解中的 value 值,作为扩展点默认实现。然后加载路径下的配置文件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);
}
}
通过getResources
或者getSystemResources
得到配置文件,然后遍历并解析配置文件,得到扩展实现类,并加入缓存。加载完扩展点配置后,再通过反射获得所有扩展实现类并缓存起来。注意,此处仅仅是把 Class 加载到 JVM 中,但并没有做 Class 初始化。在加载 Class 文件时,会根据 Class 上的注解来判断扩展点类型,再根据类型分类做缓存,此处逻辑在ExtensionLoader#loadClass
:
private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {
...
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)) {
// 如果是包装类(Wrapper),则直接直接加入包装扩展类的 Set 集合
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} else {
// 不是自适应类,也不是包装类,只剩下普通扩展类了
clazz.getConstructor();
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) {
// 如果扩展类有 @Activate 注解,则加入自动激活类缓存
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());
}
}
}
}
}
加载完毕扩展信息之后,完成了createExtension
的第一步:
private T createExtension(String name) {
// 根据 key 获取对应的扩展点实现类
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()) {
// 处理包装扩展类
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);
}
}
下一步就是根据传入的 name 找到对应的类并通过Class.forName
方法进行初始化,并为其注入依赖的其他扩展类(自动加载特性),我们看injectExtension
:
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
// 找到 setter 方法
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
Class<?> pt = method.getParameterTypes()[0];
try {
// 通过字符串截取,获得小写开头的类名。如 setTestService,截取之后是 testService
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
// 根据 key 获取对应的扩展点实例
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
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;
}
injectExtension
方法体实现了类似 Spring 的 IoC 机制,其实现原理比较简单:首先通过反射获取类的所有方法,然后遍历以字符串“set”开头的方法,得到 set 方法的参数类型,再通过ExtensionFactory
寻找参数类型相同的扩展类实例,如果找到就设置进去。
当扩展类初始化之后,会检查一次包装扩展类Set<Class<?>> cachedWrapperClasses
,查找包含与扩展点类型相同的构造函数,为其注入刚初始化的扩展类。从源码可知,包装类的构造函数注入也是通过injectExtension
方法实现的。
3.5.2 getAdaptiveExtension 的实现原理
由之前的流程我们可以知道,getAdaptiveExtension()
方法中,会为扩展点接口自动生成实现类字符串,实现类主要包含以下逻辑:为接口中每个有@Adaptive
注解的方法生成默认实现(没有注解的方法则生成空实现),每个默认实现都会从 URL 中提取@Adaptive
参数值,并且以此为依据动态加载扩展点。然后框架会使用不同的编译器,把实现类字符串编译为自适应类并返回。
如果一个接口上既有@SPI("impl")
注解,方法上又有@Adaptive("impl2")
注解,那么会使用哪个 key 作为默认实现呢?其优先通过@Adaptive
注解传入的 key 去查找扩展实现类;如果没有找到,则通过@SPI
注解中 key 去查找;如果@SPI
注解中没有默认值,则会采用驼峰规则,把类名转化为 key,再去查找。
驼峰规则:比如类名为 SimpleExt,则转化后的 key 为 simple.ext。
3.5.3 getActivateExtension 的实现原理
由于@Activate
使用场景较少,并且实现原理较为简单,感兴趣的同学自行去了解。
3.6 ExtensionFactory 的实现原理
ExtensionFactory
类似于Spring中的BeanFactory
,它是扩展类的 bean 工厂。我们看下它的实现类:
既然工厂接口有多个实现,那么是怎么确定使用哪个工厂实现的呢?我们可以看到
AdaptiveExtensionFactory
这个实现类工厂上有@Adaptive
注解。因此,AdaptiveExtensionFactory
会作为默认实现类。
除了AdaptiveExtensionFactory
,还有SpiExtensionFactory
和SpringExtensionFactory
两个工厂。也就是说,我们除了可以从 Dubbo SPI 管理的容器中获取扩展点实例,还可以从 Spring 容器获取。
我们先看SpringExtensionFactory
:
public class SpringExtensionFactory implements ExtensionFactory {
private static final Set<ApplicationContext> contexts = new ConcurrentHashSet<ApplicationContext>();
public static void addApplicationContext(ApplicationContext context) {
contexts.add(context);
BeanFactoryUtils.addApplicationListener(context, shutdownHookListener);
}
...
@Override
@SuppressWarnings("unchecked")
public <T> T getExtension(Class<T> type, String name) {
// 先根据名称去获取
for (ApplicationContext context : contexts) {
if (context.containsBean(name)) {
Object bean = context.getBean(name);
if (type.isInstance(bean)) {
return (T) 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 上下文保存在 Set 集合中,当调用getExtension
获取扩展类时,会遍历 Set 集合中的所有 Spring 上下,先根据名字依次去获取,如果没有获取到,再根据类型去获取。
那么 Spring 的上下文又是什么时候被保存的呢?我们可以通过代码搜索得知,在ReferenceBean
和ServiceBean
中会调用静态方法保存 Spring 上下文,即一个服务被发布或者被引用的时候,对应的 Spring 上下文会被保存下来。
我们再看SpiExtensionFactory
:
public class SpiExtensionFactory implements ExtensionFactory {
@Override
public <T> T getExtension(Class<T> type, String name) {
if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
if (!loader.getSupportedExtensions().isEmpty()) {
return loader.getAdaptiveExtension();
}
}
return null;
}
}
主要是获取扩展点接口对应的 Adaptive 实现类。例如:某个扩展点实现类 ClassA 上有@Adaptive
注解,则使用SpiExtensionFactory#getExtension
会直接返回 ClassA 实例。
我们再看AdaptiveExtensionFactory
:
/**
* AdaptiveExtensionFactory
* 该类的作用是管理其他的 ExtensionFactory
*/
@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {
private final List<ExtensionFactory> factories;
/**
* 构造方法会加载其他扩展工厂
*/
public AdaptiveExtensionFactory() {
ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
/*
* 获取 getExtensionClasses() 是不会加载被 @Adaptive 注解的实现类的
* 即这里只是把其他两个 ExtensionFactory 放入 factories 中
*/
for (String name : loader.getSupportedExtensions()) {
list.add(loader.getExtension(name));
}
factories = Collections.unmodifiableList(list);
}
@Override
public <T> T getExtension(Class<T> type, String name) {
for (ExtensionFactory factory : factories) {
T extension = factory.getExtension(type, name);
if (extension != null) {
return extension;
}
}
return null;
}
}
这个默认工厂在构造方法中就获取了其他所有扩展类工厂并缓存起来,包括SpringExtensionFactory
和SpiExtensionFactory
。被AdaptiveExtensionFactory
缓存的工厂会通过TreeSet
进行自然排序,SPI 排在前面,Spring 排在后面。当调用getExtension
方法时,会遍历所有的工厂,先从 SPI 容器获取扩展类;如果没有找到,再从 Spring 容器中查找。我们可以理解为,AdaptiveExtensionFactory
持有了所有的工厂实现,它的getExtension
方法只是遍历它持有的所有工厂,最终还是调用 SPI 或者 Spring 工厂实现的getExtension
方法。
4. 小结
我们没有讲扩展点的动态编译,其实现手法跟ExtensionFactory
类似,其采用含有@Adaptive
注解的AdaptiveCompiler
作为Compiler
的默认实现,主要作用是为了管理其他Compiler
(JavassistCompiler
和JdkCompiler
)。由于Compiler
接口上采用@SPI("javassist")
,说明 Javassist 编译器作为默认编译器。