:::warning 针对多实例特性,需要进行二次更新….. :::
从 Dubbo 2.0 到 3.0 的演变的过程中,提出了轻核心,重拓展的理念,大量原本集成在框架内的模块被移到了拓展之中。
Dubbo 的拓展由 org.apache.dubbo.common.extension.ExtensionLoader
泛型类负责,该类不提供对外的构造函数,获取示例需要 getExtensionLoader
静态工厂方法获取,且内部通过缓存机制来保证相同类型的实现类只会存在一份。
在 ExtensionLoader
类上,有两个核心方法:
- getAdaptiveExtension - 针对同类型拓展(例如 Protocol),提供一个访问适配器。
- getExtension - 加载真实的拓展类实例
getAdaptiveExtension - 拓展动态适配器
其中 getAdaptiveExtension
最难理解,接下来逐个分析:
SPI 注解
cacheDefaultExtensionName
函数会读取接口类上的 @SPI
注解值,来确定该拓展的默认实现是什么,例如:
基类接口 | 说明 |
---|---|
@SPI("dubbo") org.apache.dubbo.rpc.Protocol |
默认协议是 Dubbo |
@SPI("netty") org.apache.dubbo.remoting.Transporter |
默认网络通道是 Netty |
@SPI("javassist") org.apache.dubbo.common.compiler.Compiler |
默认编译器是 javassist |
@SPI
注解还有另外一个作用,标记该类型拓展是否可以通过 Dubob SPI 机制拓展:
// => org.apache.dubbo.common.extension.ExtensionLoader#getExtensionLoader
// ExtensionLoader 只加载 @SPI 标记的拓展实现
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
// => org.apache.dubbo.common.extension.ExtensionLoader#withExtensionAnnotation
private static <T> boolean withExtensionAnnotation(Class<T> type) {
return type.isAnnotationPresent(SPI.class);
}
创建拓展动态适配器
回到 loadExtensionClasses
方法,这里相当于提前把实现类加载到 JVM 之中了,供后续的 getExtension 方法使用,下节再做展开。
进入 createAdaptiveExtensionClass
方法:
private Class<?> createAdaptiveExtensionClass() {
ClassLoader classLoader = findClassLoader();
// 为了支持 GraalVM 的 native 编译,所以此处把动态代理类最后生成的代码直接放进了代码库
// 但同时也导致了灵活性问题。
if (ApplicationModel.getEnvironment().getConfiguration().getBoolean(NATIVE, false)) {
try {
return classLoader.loadClass(type.getName() + "$Adaptive");
} catch (ClassNotFoundException e) {
// 此处不应该放行,自动生成代理类逻辑在 Native 环境下无法正常执行
// 已经反馈给社区
//ignore
e.printStackTrace();
}
}
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
//=> 这里有鸡和蛋的问题
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
return compiler.compile(code, classLoader);
}
最后一段,假如 compiler
需要 compiler
自己的适配器呢?那是先有鸡还是先有蛋呢?
Dubbo 做法是,当 @Adaptive
注解标记在类上的时候,这么这个适配器是不需要动态编译的.
具体代码为在 loadDirectory
的子过程 loadClass
中:
// => org.apache.dubbo.common.extension.ExtensionLoader#loadClass
// 如果发现 Adaptive 标记的代理类,则直接加载并缓存
if (clazz.isAnnotationPresent(Adaptive.class)) {
cacheAdaptiveClass(clazz, overridden);
}
// => org.apache.dubbo.common.extension.ExtensionLoader#getAdaptiveExtensionClass
// 执行完 getExtensionClasses 方法后,cachedAdaptiveClass 已经被设置值了,不会再进入动态
// 编译代码的阶段了。
private Class<?> getAdaptiveExtensionClass() {
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
回到编译器,Dubbo 在编译器拓展里面提供了三个实现,定义文件在 dubbo-common/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.common.compiler.Compiler
类实现 | 说明 |
---|---|
AdaptiveCompiler� | 编译器路由,会选择调用 JDK 或者 Javassist 实现功能,默认是 Javassist |
JdkCompiler | Dubbo 官方基于 JDK 自带的 JavaCompiler 封装的字节码注入工具类 |
JavassistCompiler | 在运行阶段,通过 Javassist 框架进行字节码注入 |
此处先不深究字节码注入细节,我们需要注意的是 Dubbo SPI 注入的类,是被自定义 ClassLoader 加载的,通过 Launcher$AppClassLoader
类加载器,是获取不到的(假如该类文件在 AppClassLoader
的 classpath 里面存在,那获取到的也不是同一个类,使用 ==
判断是 false,也不可混用 instance of
判定)。
相对的,默认情况下,通过 Java SPI 中的注入类是被 Launcher$AppClassLoader
加载的,是可以获取到的。
适配器简化模型
最后生成的拓展动态适配器可归结为如下伪代码:
// 在接口定义上标记 @SPI 注解,代表通过 Dubbo SPI 加载
// 并指定默认实现类
@SPI("DefaultExtensionName")
public interface YourInterface {
Result methodA(Param);
}
// 其中一个实现类
public YourInterfaceImpl implements {
@Override
Result methodA(Param) {
// Do somethings
return Result;
}
}
// 主角登场,动态拓展适配器,在 Dubbo 中是由
public YourInterface$Adaptive implements YourInterface {
@Override
Result methodA(Param) {
// 从入参里面提取需要加载具体实现类的名字
String extName = ExtractImplClassNameFrom(param);
// 通过类名,获取最终实现类的实例
YourInterface actualExtension = getExtension(extName);
return actualExtension.methodA(Param);
}
}
其中 getExtension
的具体实现,就是我们下一小节要讲的。
getExtension - 获取拓展实例
在 Dubbo 框架中,为了灵活性会通过 AdaptiveExtension
动态路由到 getExtension
来获取最终的实现类实例。
至此,我们会有一个疑问,Dubbo 为什么不直接使用 JDK SPI 而非得自定义一套增强的 Dubbo SPI?官方给出的理由如下,接下来我们将依此进行验证:
- 可以支持 K/V 形式的实现类定义,加快后续检索
- 可以通过 Dubbo IoC 进行依赖注入增强
前文提到,创建拓展动态适配器的时候,会提前加载实现类,其中核心方法是 loadExtensionClasses
,在 Dubbo SPI 中,该方法在功能目标上对标 JDK SPI 提供的 ServiceLoader.load
方法。
实现类加载策略
在该方法中,会依据 strategies
制定的加载策略来获取特定实现
// => org.apache.dubbo.common.extension.ExtensionLoader#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());
// 当初 Dubbo 捐献给 Apache 基金会的时候,包域名限定改成了 org.apache
// 此处是为了兼容历史代码
loadDirectory(extensionClasses, strategy.directory(), type.getName().replace("org.apache", "com.alibaba"), strategy.preferExtensionClassLoader(), strategy.overridden(), strategy.excludedPackages());
}
return extensionClasses;
}
而有意思的是,strategies
是通过 JAVA 标准的 SPI 机制加载的(非 Dubbo SPI),加载的配置文件为 dubbo-common
子模块下的 org.apache.dubbo.common.extension.LoadingStrategy
文件,默认定义了三个策略,如果不同目录的类实现定义存在冲突,则优先使用高优先级目录的类实现定义。优先级的实现可简单概括为:抢占式加载,禁止覆盖,高优先级策略优先加载。
策略类 | 策略配置:优先级/目录 | 备注 |
---|---|---|
DubboInternalLoadingStrategy | 高优先级 META-INF/dubbo/internal/ |
Dubbo 内部核心拓展 |
DubboLoadingStrategy | 中优先级 META-INF/dubbo/ |
用户定义的 Dubbo 拓展 |
ServicesLoadingStrategy | 低优先级 META-INF/services/ |
标准 JAVA SPI 目录 |
执行实现类加载(非实例化)
走到 loadDirectory
方法,该方法支持解析 2 种格式的配置文件
1)KV 形式
Dubbo SPI 特有,示例如下,最终会映射为实现类的 Map 集合
// 示例如下
// => dubbo-rpc/dubbo-rpc-api/src/main/resources/META-INF/dubbo/internal/org.apache.dubbo.rpc.Protocol
listener=org.apache.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=org.apache.dubbo.rpc.support.MockProtocol
2)兼容 JAVA SPI 配置
// 示例文件(实际应用中,Dubbo 是通过 JAVA SPI 加载的该策略文件,但 Dubbo SPI 也能处理)
// => dubbo-common/src/main/resources/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
兼容 JAVA SPI 明显需要解决一个问题,如何获取类的名字,作为 Map 容器的 KEY 值,这个答案在这 findAnnotationName
可以找到。如果获取名字失败,将会抛出异常,终止加载。
// => org.apache.dubbo.common.extension.ExtensionLoader#findAnnotationName
// 该方法其实也有历史包袱,@Extension 这个注解其实已经废弃
private String findAnnotationName(Class<?> clazz) {
// 一开始是尝试从 @Extension 注解获取类名的代码,已经标记 @Deprecated,省略相关代码
// ......
String name = clazz.getSimpleName();
if (name.endsWith(type.getSimpleName())) {
// 实现类名移除基类类名尾缀,为该实现类的名字
// 例如 DubboInternalLoadingStrategy 实现了 LoadingStrategy,则移除现在尾缀后
// 最终 name 值为 DubboInternal
name = name.substring(0, name.length() - type.getSimpleName().length());
}
return name.toLowerCase();
}
接下来聊聊配置解析成功后,实现类是如何加载的,核心关注 LoadClass
方法:
// => org.apache.dubbo.common.extension.ExtensionLoader#loadClass
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.");
}
if (clazz.isAnnotationPresent(Adaptive.class)) {
// 前文已经讲到,鸡和蛋的问题,不再重复
cacheAdaptiveClass(clazz, overridden);
} else if (isWrapperClass(clazz)) {
// 包装类
cacheWrapperClass(clazz);
} else {
// 普通实现类,需要带有一个公开的无参构造器
clazz.getConstructor();
// 前文提到的解析 JAVA SPI 配置文件时候的 name 补全策略
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);
}
}
// 逗号分隔的拓展实现类名,所以运行存在这样的配置:name1,name2=com.foo.XxxProtocol
String[] names = NAME_SEPARATOR.split(name);
if (ArrayUtils.isNotEmpty(names)) {
// 带生效条件的实现类,由 @Activate 注解标记生效条件
cacheActivateClass(clazz, names[0]);
for (String n : names) {
cacheName(clazz, n);
// 保持到拓展类缓存容器中,注意此处 overridden 是 false
// 不然前文提到加载策略优先级会失效(破坏了抢占式加载)
saveInExtensionClass(extensionClasses, clazz, n, overridden);
}
}
}
}
最终的,拓展实现类可以总结为三大类型:
类型 | 判定条件 |
---|---|
AdaptiveClass | @Adaptive 注解标记类在 Dubbo 3.0 内,目前依旧只有两个类标记 - AdaptiveCompiler - AdaptiveExtensionFactory 对应的缓存字段: Class<?> ExtensionLoader#cachedAdaptiveClass |
WrapperClass | 包装类:拥有一个公开的构造函数,有且只有一个类型为 type 接口类的参数。 例如 ProtocolListenerWrapper 的构造函数public ProtocolListenerWrapper(Protocol protocol) 就符合这个约定对应的缓存字段: Set<Class<?>> ExtensionLoader#cachedWrapperClasses |
ExtensionClass | 普通实现类,需要带有一个公开的无参构造器,内部细分为 - 附带生效条件的实现类,由 @Activate 注解标记生效条件 - 真真普通的实现类 对应的缓存字段: Holder<Map<String, Class<?>>> ExtensionLoader#cachedClasses |
Dubbo IOC 注入(实例化类)
注意到此为此,我们完成的是“实现类”的加载和缓存构建,此时还不存在实现类实例。接下来,我们开始探讨“实现类实例”是如何创建的,此处重点关注 Dubbo IoC 注入过程。
进入 createExtension
核心方法:
// => org.apache.dubbo.common.extension.ExtensionLoader#createExtension
private T createExtension(String name, boolean wrap) {
// ......
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 前文提到的普通拓展实现类,必须拥有一个无参构造器
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.getDeclaredConstructor().newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// Dubbo IOC
injectExtension(instance);
if (wrap) {
// 典型的职责链模式的应用,稍后会有详细分析
// ......
}
// 如果实例的超类是 org.apache.dubbo.common.context.Lifecycle
// 则触发生命周期的 initialize 钩子
initExtension(instance);
// 实例创建一次后,将会被缓存起来,所以通过 Dubbo IOC 获取的实例本质上都是单例
return instance;
} // ......
}
进入 injectExtension
,我们惊讶的发现,Dubbo IOC 的依赖注入代码相当清晰:
private T injectExtension(T instance) {
// 理解 objectFactory 是难点重点,后文单独说明
if (objectFactory == null) {
return instance;
}
try {
for (Method method : instance.getClass().getMethods()) {
// 只针对 Setter 方法进行依赖注入
if (!isSetter(method)) {
continue;
}
/**
* Check {@link DisableInject} to see if we need auto injection for this property
*/
// 可以手动标记忽略某个 Setter 注入方法
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
Class<?> pt = method.getParameterTypes()[0];
// 基础数据类型无法注入,如 int, long 等
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
String property = getSetterProperty(method);
// 通过工厂方法获取需要注入的依赖
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);
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
接下来我们回答 objectFactory
是什么,我们先可以将其概括为:依赖工厂
当 ExtensionLoader
初始化的时候:
// => org.apache.dubbo.common.extension.ExtensionLoader#ExtensionLoader
// 私有构造函数
private ExtensionLoader(Class<?> type) {
this.type = type;
// 当创建类型为非 ExtensionFactory 类型的拓展加载器时,都尝试创建依赖工厂,以用于后续的依赖注入
// 注意该拓展动态适配器是标记了 @Adaptive 注解的,不需要动态编译
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
ExtensionFactory
有两个依赖工厂实现,且 AdaptiveExtensionFactory
会尝试遍历所有依赖工厂,找到立即终止搜索并返回。
// 提供了公开静态方法,可以外部设置 Spring ApplicationContext,后续文章会介绍相关 spring-starter
// 当调用 getExtension 方法的时候,会遍历设置的 ApplicationContext 获取实例,该方法与 spi 互斥
spring=org.apache.dubbo.config.spring.extension.SpringExtensionFactory
// 注入 拓展动态适配器
spi=org.apache.dubbo.common.extension.factory.SpiExtensionFactory
WrapperClass 组成的职责链
我们可以将一些共性的逻辑,抽离到 WrapperClass 内。
到这步的时候,所有该类型的 WrapperClass 已经被缓存到了 cachedWrapperClasses
之中,逻辑如下:
// => org.apache.dubbo.common.extension.ExtensionLoader#createExtension
private T createExtension(String name, boolean wrap) {
// ......
if (wrap) {
List<Class<?>> wrapperClassesList = new ArrayList<>();
if (cachedWrapperClasses != null) {
wrapperClassesList.addAll(cachedWrapperClasses);
// 对包装类进行排序,依赖 @Activate 注解的 order 值,如果缺失则为 0
// 值越大,在洋葱模型的越内层.
wrapperClassesList.sort(WrapperComparator.COMPARATOR);
Collections.reverse(wrapperClassesList);
}
if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
for (Class<?> wrapperClass : wrapperClassesList) {
// @Wrapper 注解表明,使用该包装器需要什么条件
Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
if (wrapper == null
|| (ArrayUtils.contains(wrapper.matches(), name) && !ArrayUtils.contains(wrapper.mismatches(), name))) {
// (T) 向上转型为原始接口类,例如 Protocol
// 并对包装类再次执行依赖注入
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
}
}
// 现在我们再看 initExtension 方法,当实例被包装过后,有可能不再是 Lifecycle 实例
// 从严谨的角度来看,职责链和此处的 Hook 方法是冲突的,容易给用户造成不必要的困惑。
initExtension(instance);
// ......
}
如上就是 Dubbo SPI 拓展管理的骨架流程了。在 Dubbo 框架中, SPI 是一个核心流程,值得花时间去弄明白,这对后续概念的理解很有裨益。
getActivateExtension - 获取活跃拓展实例
逻辑相对简单,获取某类型下被 @Activate
标记的实现类,并返回其实例列表:
// => org.apache.dubbo.common.extension.ExtensionLoader#getActivateExtension
public List<T> getActivateExtension(URL url, String[] values, String group) {
// ......
// 遍历获取被 @Activate 注解的类
for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Object activate = entry.getValue();
String[] activateGroup, activateValue;
if (activate instanceof Activate) {
activateGroup = ((Activate) activate).group();
activateValue = ((Activate) activate).value();
} else if (activate instanceof com.alibaba.dubbo.common.extension.Activate) {
activateGroup = ((com.alibaba.dubbo.common.extension.Activate) activate).group();
activateValue = ((com.alibaba.dubbo.common.extension.Activate) activate).value();
} else {
continue;
}
cachedActivateGroups.put(name, new HashSet<>(Arrays.asList(activateGroup)));
// 获取生效条件
cachedActivateValues.put(name, activateValue);
}
// ......
// traverse all cached extensions
cachedActivateGroups.forEach((name, activateGroup) -> {
if (isMatchGroup(group, activateGroup)
&& !names.contains(name)
&& !names.contains(REMOVE_VALUE_PREFIX + name)
// 激活条件判断,此处可以实现依据 URL 参数配置按需激活拓展
// 如果没有配置判断条件,则直接放行
&& isActive(cachedActivateValues.get(name), url)) {
activateExtensionsMap.put(getExtensionClass(name), getExtension(name));
}
});
// ......
return new ArrayList<>(activateExtensionsMap.values());
}