Apache Dubbo 是一款 RPC 微服务框架,提供了包括 Java、Golang 等在内的多种语言 SDK 实现。可扩展性是一种设计理念,我们希望在现有的架构或设计基础上,以最小的改动来适应未来的变化。Dubbo SPI 的加载机制,让整个框架的接口和具体实现完全解耦,从而奠定了整个框架良好的可扩展性。
Dubbo SPI 没有直接使用 Java SPI,而是在其基础上做了相应的改进,形成了一套自己的规范和特性。下面我们从最基础的 Java SPI 介绍一下 SPI 的思想。
SPI (Service Provider Interface)最初是提供给厂商做插件开发的。Java SPI 使用了策略模式,一个接口会有多个实现。我们只声明接口,具体的实现并不在程序中直接确定,而是由程序之外的配置控制,用于最终的装配。一个简单的实现步骤如下:
META-INF/services/
目录下创建一个接口全路径命名的文件,如 com.moguhu.spi.HelloService
java.util.ServiceLoader
来加载具体的实现下面我们看下具体的代码实现:
获取实现类的工具方法 SpiServiceHelper
的代码实现:
/**
* SPI类加载辅助工具
*
* @author xuefeihu created on 2021/05/31.
*/
public class SpiServiceHelper {
private final static Map<String, Object> singletonServices = new HashMap<>();
public synchronized static <S> S load(Class<S> service) {
return StreamSupport.stream(ServiceLoader.load(service).spliterator(), false).map(SpiServiceHelper::singletonFilter)
.findFirst().orElseThrow(ServiceLoadException::new);
}
public synchronized static <S> Collection<S> loadAll(Class<S> service) {
return StreamSupport.stream(ServiceLoader.load(service).spliterator(), false).map(SpiServiceHelper::singletonFilter)
.collect(Collectors.toList());
}
@SuppressWarnings("unchecked")
private static <S> S singletonFilter(S service) {
if (service.getClass().isAnnotationPresent(MoSingleton.class)) {
String className = service.getClass().getCanonicalName();
Object singletonInstance = singletonServices.putIfAbsent(className, service);
return singletonInstance == null ? service : (S) singletonInstance;
} else {
return service;
}
}
}
Dubbo SPI 和 Java SPI 类似,需要在 META-INF/dubbo/
目录下放置对应的 SPI 配置文件,文件名为接口的全路径名。如下所示:
这里面需要注意的是,除了资源路径与 Java SPI 不同之外,文件内容中也多了个 key,如上图中的dubbo、hessian2、fastjson 等。这里与dubbo 最开始 2.0.x
版本中的 SPI 也有所不同(@SPI
原先叫做 @Extension
),原先每个SPI 的实现的名称是写在每个实现类上的。接口和实现类都要有 @Extension
注解,实现类的注解上面会写这个SPI 实现的名字。而最新的扩展则是只需要在接口上加上@SPI
,然后配置文件上带有对应 SPI 实现类的名称,从一定程度上做了简化。
Dubbo SPI 是按需加载的,具体逻辑在ExtensionLoader
类中实现,可以分为 Class 缓存、实例缓存。这两种缓存又能根据扩展类的种类分为普通扩展类、包装扩展类(Wrapper)、自适应扩展类(Adaptive)等。
Class 缓存:Dubbo SPI 获取扩展类时,首先会从缓存中获取。如果缓存中不存在,则会加载配置文件,将Class 缓存加载到内存中,类似于懒加载。
实例缓存:ExtensionLoader
中不仅会缓存Class,也会缓存Class实例化后的对象。整体原则就是会从缓存中优先获取,缓存中没有的再使用 Class.forName()
将类加载到内存中。
被缓存的Class 和对象实例可以分为以下几类:
1. 普通扩展类,最基础的,配置在SPI 配置文件中的扩展类实现
2. 包装扩展类,包装类通常不会直接承载具体的实现,它是对ExtensionLoader
加载的策略类的包装,通过这个包装可以实现Filter、Listener 等效果。如 Protocol 的包装类 ProtocolFilterWrapper
、ProtocolListenerWrapper
3. 自适应扩展类,可以理解为对不同策略类的包装,这个类是由Dubbo 动态生成的,里面的内容大概可以理解为就是一个策略的组装,最终的业务调用还是由各个策略类实现。不同策略的选择通常是由 URL 参数决定的。
4. 其他缓存,如扩展类加载器缓存、扩展名缓存等。
Dubbo SPI 一共包含 4 种特性,包含:自动包装(Wrapper)、自动装载(Autowire)、自适应(Adaptive)、自动激活(Active),下面分别介绍一下这4类特性。其中 Wrapper、Autowire、Adaptive 这3 个功能,在 Dubbo 开源的第一版(2.0.7
)中就已经提供,后续只是简单的加了一个 Active 的功能。
在ExtensionLoader
加载扩展时,如果发现这个类有1个参数并且参数为ExtensionLoader
中的 T 类型时,就会自动认为是 wrappers(老版本命名叫做 autoproxies),也就是表示对真正的扩展包了一层。
public class ProtocolListenerWrapper implements Protocol {
public ProtocolListenerWrapper(Protocol protocol) {
if (protocol == null) {
throw new IllegalArgumentException("protocol == null");
}
this.protocol = protocol;
}
}
从上面可以看出,ProtocolListenerWrapper
有个1个参数且入参为 Protocol
的构造方法,此时ProtocolListenerWrapper
就是一个包装类,当获取一个Protocol
的扩展时,如:DubboProtocol
,此时就会为 DubboProtocol
自动作为入参,被包装为ProtocolListenerWrapper
的类型。因为 ProtocolListenerWrapper
也是一个Protocol
类型,此时ExtensionLoader
可以返回此包装类给上游使用。包装类主要是用于增强 扩展类的功能,比如 Protocol
的包装类有 2个:ProtocolListenerWrapper
和 ProtocolFilterWrapper
,分别为 Protocol
增加了过滤器和 监听器,以扩展其功能。
自动装载类似于Spring 中的 Autowire,也就是自动装配。当一个扩展类是另外一个扩展类的成员变量时,并且有 setter 方法,此时ExtensionLoader
就会自动new 一个扩展类,自动注入。通常被注入的会是一个接口类型,如 Protocol
,则会被注入为 Protocol$Adaptive
,也就是一个自适应类型。
自适应的扩展可以认为是策略模式中,组装策略的那部分代码,而策略本身是由 URL 参数中的参数决定的。下面我们看下一个 Adaptive 的例子。
首先我们提供一个扩展接口 SimpleExt
,其中有3个方法,echo()
、yell()
允许做自适应,而 bang()
不允许做自适应。
"impl1") (
public interface SimpleExt {
// @Adaptive example, do not specify a explicit key.
String echo(URL url, String s);
"key1", "key2"}) ({
String yell(URL url, String s);
// no @Adaptive
String bang(URL url, int i);
}
经过ExtensionLoader
创建出来的自适应扩展代码,大概如下所示,也就是说具体通过哪个策略,是通过 URL 中对应的参数决定的:
package com.alibaba.dubbo.common.extensionloader.ext1;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
import com.alibaba.dubbo.common.extensionloader.ext1.SimpleExt;
public class SimpleExt$Adaptive implements SimpleExt {
public java.lang.String echo(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null)
throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("simple.ext", "impl1");
if(extName == null)
throw new IllegalStateException("Fail to get extension(SimpleExt) name from url(" + url.toString() + ") use keys([simple.ext])");
SimpleExt extension = (SimpleExt)ExtensionLoader.getExtensionLoader(SimpleExt.class).getExtension(extName);
return extension.echo(arg0, arg1);
}
public java.lang.String yell(com.alibaba.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null)
throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
if(extName == null)
throw new IllegalStateException("Fail to get extension(SimpleExt) name from url(" + url.toString() + ") use keys([key1, key2])");
SimpleExt extension = (SimpleExt)ExtensionLoader.getExtensionLoader(SimpleExt.class).getExtension(extName);
return extension.yell(arg0, arg1);
}
public java.lang.String bang(com.alibaba.dubbo.common.URL arg0, int arg1) {
throw new UnsupportedOperationException("method bang is not adaptive method!");
}
}
自动激活是ExtensionLoader
后续新增的功能,通过 @Adaptive
注解实现。可以标记对应的扩展点是否可以被激活使用,目前主要用于Filter 链构建前的,可用Filter列表获取。因为是后续新增的功能,笔者对比了Dubbo 2.6.0
和 2.0.7
两个版本里面Filter 列表获取的差异,老的里面是通过配置写死的 Filter 列表顺序,而新的顺序则是通过 @Adaptive
在各个扩展实现类中标注的,此时它可以标注分组(Filter
链可以用在服务端/客户端),也可以标注分组内的顺序。此时依赖 @Active
注解就可以构建出原来写死在代码里的 Filter
顺序列表了。
ExtensionLoader 是 Dubbo SPI 的核心类,里面提供了一系列的静态方法用于获取扩展类的对象。下面我们看下 getExtension()
、getAdaptiveExtension()
和getActiveExtension()
的实现流程。
从上图可以看出,getExtension()
首先会调用 createExtension()
创建扩展,然后会读取该扩展的所有实现 Class 到内存中。当然这里面会判断之前是否已经加载过了该接口的扩展Class。之后会判断这个实现类的Class 在内存中是否已经有了实例,如果之前已经new 过了对象,则直接返回即可。如果是第一次获取,则会实例化该Class,然后对该对象中的 setter 方法做自动自动注入扩展(通常是 Adaptive类型)。然后该扩展接口如果有 Wrapper 类,则会依次包装,也就是在new 出来的对象上再包上一层,最后返回这个 Wrapper 类的实例。当然对于 Wrapper 的实例也会对其 setter 方法进行自动注入相应的扩展。
getAdaptiveExtension()
首先会判断缓存中是否已经初始化过Adaptive 对象,如果已经存在则直接返回。如果不存在,则调用 createAdaptiveExtension()
创建Adaptive扩展。此时和getExtension()
一样会去先加载接口的所有扩展Class到内存中,然后根据Adaptive注解生成Adaptive扩展类的代码,并编译为Class对象返回。之后会实例化该对象,并自动注入setter 属性,最后返回。
getActiveExtension()
首先会加载所有的扩展Class 到内存中,然后根据@Active
中配置的 before、after、order 进行排序。最后对于用户自定义扩展类,根据URL入参进行排序,最后返回符合条件的有序扩展类实例列表。
对于 ExtensionFactory
,是Dubbo 后续版本引入的。笔者对比了 Dubbo 2.6.0
和 2.0.7
两个版本,发现这个工厂仅仅是在做自动注入是被使用到的。下面我们看下 2个版本的自动注入地方的代码实现。
private void injectExtension(Object instance) {
try {
for (Method method : instance.getClass().getMethods()) {
if (method.getName().startsWith("set") && method.getParameterTypes().length == 1 && Modifier.isPublic(method.getModifiers())) {
Class<?> pt = method.getParameterTypes()[0];
if (pt.isInterface()) {
try {
// 对于自动装配的 bean, 直接获取 adaptive 类型的扩展
Object adaptive = getExtensionLoader(pt).getAdaptiveExtension();
method.invoke(instance, adaptive);
} 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);
}
}
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())) {
Class<?> pt = method.getParameterTypes()[0];
try {
// 如 setName, 解析后 property = name
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
// 这里通过 ExtensionFactory 获取扩展类的实例
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;
}
通过上面的对比我们可以看出,ExtensionFactory
仅仅是在 setter 方法注入时,用于获取对应类型的扩展的工厂。对于Dubbo SPI 本身的扩展获取,和 ExtensionFactory
并没有什么直接关系,笔者认为其主要意义在于和 Spring Bean 的打通。下面我们看一下ExtensionFactory
的实现。
从 ExtensionFactory
的实现可以看出,有3个实现类。其中 SpiExtensionFactory
里面实现是和 2.0.7
版本中的实现是一致的,也就是直接获取被注入类型的 Adaptive 扩展。SpringExtensionFactory
表示从 Spring 上下文中获取实例注入,其主要代码如下:
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;
}
}
}
return null;
}
上面的代码很简单,就是直接从 Spring 上下文中获取,但是 context 是怎么注入到其中的呢。对于Bean的组装,主要实在 dubbo-config
层进行组装的,这里面会监听Spring 的初始化,然后将context注入。最后一个是 AdaptiveExtensionFactory,当然也是通常使用的一个,主要原因在于其可以自动选择使用 SPI 还是Spring 的Bean,大概代码如下:
@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>();
// 只有 SPI 和 Spring 2个
for (String name : loader.getSupportedExtensions()) {
list.add(loader.getExtension(name));
}
factories = Collections.unmodifiableList(list);
}
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;
}
}
可以看出AdaptiveExtensionFactory 是@Adaptive
标注的,因此factories 中只有 2 个工厂,且用TreeSet 存储的顺序为 SPI、Spring。也就是说注入 Bean 会优先使用 Dubbo SPI 的扩展,然后再使用Spring 的Bean。
Compiler 类也是 Dubbo 后续版本中引入的,动态编译主要用于生成 Adaptive 类时使用的。在老的版本中,是直接在ExtensionLoader
中写死的用 javassist 进行编译的。在新的版本中,做了抽象并做了一定的改进。其类图大致如下:
这里面和ExtensionFactory 的抽象比较类似,其中扩展主要有 Javassist 和 JDK 2种类型。这里面需要提一下的是,AbstractCompiler 这个类中的代码,首先他会拼接出全路径名,然后尝试Class.forName()加载这个类,这里是防止重复编译。如果已经加载过,此时就直接返回了。如果不存在,则调用子类方法去编译生成的code。
public Class<?> compile(String code, ClassLoader classLoader) {
code = code.trim();
Matcher matcher = PACKAGE_PATTERN.matcher(code);
String pkg;
if (matcher.find()) {
pkg = matcher.group(1);
} else {
pkg = "";
}
matcher = CLASS_PATTERN.matcher(code);
String cls;
if (matcher.find()) {
cls = matcher.group(1);
} else {
throw new IllegalArgumentException("No such class name in " + code);
}
// 生成类全路径名
String className = pkg != null && pkg.length() > 0 ? pkg + "." + cls : cls;
try {
// 尝试加载,失败则进行编译,防止重复编译
return Class.forName(className, true, ClassHelper.getCallerClassLoader(getClass()));
} catch (ClassNotFoundException e) {
if (!code.endsWith("}")) {
throw new IllegalStateException("The java code not endsWith \"}\", code: \n" + code + "\n");
}
try {
// 调用子类实现
return doCompile(className, code);
} catch (RuntimeException t) {
throw t;
} catch (Throwable t) {
throw new IllegalStateException("Failed to compile class, cause: " + t.getMessage() + ", class: " + className + ", code: \n" + code + "\n, stack: " + ClassUtils.toString(t));
}
}
}
这里可以发现,原先的代码里并没有做防止重复编译的限制,而提取后则限制了。这是因为原先代码只在 ExtensionLoader
中使用,而其本身就会判断实例是否存在,也就是相当于防止了重复编译。这里面提取出来之后,可能会提供给更多的入口使用,因此做了限制。但是笔者看了下代码,也就ExtensionLoader
在生成 Adaptive 扩展时使用了。这从一定程度上看,这个抽象是不是有点鸡肋了。
Dubbo 中的扩展能力是从 JDK 标准的 SPI 扩展点发现机制加强而来,它改进了 JDK 标准的 SPI 的加载效率和稳定性。用户能够基于 Dubbo 提供的扩展能力,可以很方便的实现:按需加载、增加扩展类的 IOC 能力、增加扩展类的 AOP 能力、具备动态选择扩展实现的能力等。从 Dubbo 扩展的设计目标可以看出,它的设计思想可以延伸到类似其他的中间件上面,比如:Apache ShenYu 的 SPI 扩展机制,就参考了 Dubbo SPI 的设计。也就是说Dubbo SPI 是一种通用的代码可扩展设计,当系统增加新功能时,不需要对现有系统的结构和代码进行修改,仅仅新增一个扩展即可。
风度玉门,信也科技后端研发专家
Java、大数据、前端、测试等各种技术岗位热招中,欢迎扫码了解~