Dubbo 扩展点加载机制
本文介绍 Dubbo 的加载机制,并与 JDK 加载机制对比。及 @SPI、@Adaptive、@Activite 注解。以及核心类 ExtensionLoader 流程与原理。以及使用的类动态编译实现原理。
本文抄自《深入理解 Apache Dubbo 和实战》。但相关代码部分会更详细,读者也可以自行 fork 源码阅读。本文 Dubbo 源码版本为 2.7.5,个别地方会和原版书中描述有出入。
一、加载机制概述
Dubbo 良好的扩展性是基于 Dubbo SPI 加载机制以及使用合适的设计模式,实现了框架的接口与实现完全的解耦。
Dubbo 默认提供了很多可以直接使用的扩展点。Dubbo 几乎所有功能组件都是基于 SPI 实现的,后续文章会详细介绍。
Dubbo SPI 没有直接使用 Java SPI,而是做了一定改进,并且兼容 Java SPI。服务启动时,Dubbo 就会查找这些扩展点的具体实现。Dubbo 启停原理会在后续文章讲解。
1.1 Java SPI
Java SPI,即 Service Provider Interface,起初用于给厂商做插件开发。
Java SPI 是策略模式,一种接口多种实现。Java 只制定接口(规范),具体实现不在程序中直接确定,而取决于是由程序之外的配置,用来实现具体的装配。例如:
- 定义一个接口,如 com.test.spi.PrintService
- 编写一个接口的实现类
- 在 META-INF/services/目录下,创建一个以接口全路径名的文件 com.test.spi.PrintService
- 文件内容为其实现类的全路径名,如有多个使用分隔符分隔
- 代码中使用 java.util.ServiceLoader 加载具体实现类。
这样便实现了通过配置文件控制接口的具体实现类。
代码示例:
public interface PrintService {
void print(String s);
}
public class IPrintService implements PrintService {
@Override
public void print(String s) {
System.out.println(s);
}
}
// 在 META-INF/services/ 目录下创建文件,文件名为接口的全路径名,如 com.test.spi.PrintService
// 文件的内容为实现类的全路径名
public class JDKSPIDemo {
public static void main(String[] args) {
ServiceLoader<PrintService> serviceLoader = ServiceLoader.load(PrintService.class);
for (PrintService printService : serviceLoader) {
printService.print("I print");
}
}
}
因为我们只配置了一个实现类,所以会打印一次 “I print”
1.2 扩展点加载机制的改进
Dubbo SPI 做了一定的改进和优化,官方文档内容:
JDK 标准 SPI 会一次性加载所有实现,如果有些实现加载很耗时而且也没用上,则浪费资源。
- 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK标准的ScriptEngine,通过getName();获取脚本类型的名称, 但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉 了,和ruby对应不起来,当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。
- 增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。Java SPI 的 ServiceLoader 会把接口的所有实现类全部初始化,用户可以直接使用。Dubbo 的 SPI 只是记载了配置文件里的类,并且分成不同类型缓存在内存中,并且不会立刻初始化,性能上有更好的表现。
将上面的例子改为 Dubbo SPI
// 接口加上 @SPI 注解,实现类不变
@SPI("impl")
public interface PrintService {
void print(String s);
}
// 在 META-INF.dubbo.internal 下创建同名文件 文件内容为
// 文件内容为 impl=com.test.spi.IPrintService impl SPI 注解上的值,代表默认的实现
public class DubboSPIDemo {
public static void main(String[] args) {
PrintService printService = ExtensionLoader.getExtensionLoader(PrintService.class).getDefaultExtension();
printService.print("I print.");
}
}
Java SPI 加载失败,可能会因为各种原因导致异常信息被“吞掉”,导致追踪问题困难。Dubbo SPI 加载失败时,会先抛出真实异常并打印日志。
Dubbo SPI 自己实现了 IoC 和 Aop 机制。一个扩展点可以通过 setter 直接注入其他扩展的方法,T injectExtension(T instance) 方法实现了这个功能。
另外,Dubbo 支持包装扩展类,推荐把通用的抽象逻辑放到包装类中,用于实现扩展点的 AOP 特性。例如,类 ProtocolFilterWrapper 包装类扩展了 Protocol, 通用的逻辑判断全部放在了 export 方法,最终调用 Protocal 的 export 方法。和 Spring 动态代理的思想是一样的,在被代理类的前后插入自己的逻辑增强,最终调用被代理类。(可以自行结合代码查看)。
public class ProtocolFilterWrapper implements Protocol {
private final Protocol protocol;
...
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
if (UrlUtils.isRegistry(invoker.getUrl())) {
return protocol.export(invoker);
}
return protocol.export(buildInvokerChain(invoker, SERVICE_FILTER_KEY, CommonConstants.PROVIDER));
}
...
}
1.3 扩展点规范配置
规范名 | 规范说明 |
---|---|
SPI 配置文件路径 | META-INF/services/ (兼容JDK)、META-INF/dubbo/、META-INF/dubbo/internal |
SPI 配置文件名称 | 类全路径名 |
文件内容格式 | key=value 方式,多个用换行符分隔,key 会用作 @SPI 注解上的 value |
1.4 扩展点的分类和缓存
Dubbo SPI 可以分为 Class 缓存、实例缓存。这两种缓存又能根据扩展类的种类分为普通扩展类、包装扩展类(Wrapper 类)、自适应扩展类(Adaptive)等。
- Class 缓存:Dubbo SPI 获取扩展类时,先会从缓存中读取。如果缓存中不存在,则加载配置文件,根据配置把 Class 缓存到内存,不会直接全部初始化。
- 实例缓存:基于性能考虑,Dubbo 框架不仅缓存 Class,也会缓存 Class 实例化后的对象。每次获取的时候,线程缓存获取,缓存读不到,重新加载并写入缓存。这也是为什么 Dubbo SPI 比 Java SPI 性能好的原因,Dubbo 是按需实例化且做了缓存。
被缓存的 Class 和对象实例可以根据不同特性分为不同类别:
- 普通扩展类。最基础的,SPI 配置文件中的。
- 包装扩展类。这种 Wrapper 类没有具体的实现,只是做了通用逻辑的抽象,需要在构造方法中传入一个具体的扩展接口的实现。属于 Dubbo 的自动包装特性。
- 自适应扩展类。一个扩展接口会有多个实现类,具体用哪个可以不写死在代码或者配置里,可以通过传入 URL 中的某些参数动态来确定。属于扩展点的自适应特性,@Adaptive 注解。
- 其他缓存,如扩展类加载器缓存、扩展名缓存等。
扩展类缓存表:见下文 ExtensionLoader 源码。
1.5 扩展点特性
从官方文档得知,扩展类一共包含四种特性:自动包装、自动加载、自适应和自动激活。
1.5.1 自动包装
自动包装是 1.4 中提到的一种被缓存的扩展类,ExtensionLoader 在加载扩展时,如果发现这个扩展类包含其他其他扩展点作为构造参数的参数,则这个扩展类就会被认为是 Wrapper 类,如:
public class ProtocolFilterWrapper implements Protocol {
private final Protocol protocol;
// 实现了 Protocol,构造函数中又传入了一个 Protocol 类型的参数,框架会自动注入
public ProtocolFilterWrapper(Protocol protocol) {
if (protocol == null) {
throw new IllegalArgumentException("protocol == null");
}
this.protocol = protocol;
}
...
}
ProtocolFilterWrapper 虽然实现了 Protocol 接口,但构造函数又注入了一个 Protocol 类型的参数。这样 ProtocolFilterWrapper 会被认定为 Wrapper 类。是一种装饰器模式,把通用的抽象逻辑进行封装或对子类进行增强,让子类可以更加专注具体的实现。
1.5.2 自动加载
除了在构造函数中传入其他扩展实例,我们还经常使用 setter 方法设置属性值。如果某个扩展类是另外一个扩展点的成员属性,并拥有 setter 方法,框架会自动注入对应的扩展点实例。ExtensionLoader 在执行扩展点初始化的时候,会自动通过 setter 方法注入对应的实现类。这里存在一个问题,如果扩展类属性是一个接口,它有多种实现,那么具体注入哪个呢?这涉及第三个特性——自适应。
1.5.3 自适应
Dubbo SPI 中,我们使用 @Adaptive 注解,可以动态地通过 URL 中的参数来确定要使用哪个具体的实现类。从而解决自动加载中的实例注入问题。例如:
@SPI("netty")
public interface Transporter {
@Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
RemotingServer bind(URL url, ChannelHandler handler) throws RemotingException;
@Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
Client connect(URL url, ChannelHandler handler) throws RemotingException;
}
@Adaptive 的两个参数,值分别是 server 和 transporter。外部调用 bind 时,会动态地从 URL 中提取 server 作为 key 的 value 值,如果能匹配上陌路哥扩展实现类时则直接使用对应实现类;如果匹配不上,继续通过第二个参数 transporter 提取 value。如果都没匹配到,则抛出异常。
也就是说,@Adaptive 如果传入多个参数,则依次进行实现类匹配,到最后没有则抛出异常。
这种动态寻找实现类的方式比较灵活,但只能激活一个具体的实现类,如果需要多个实现类被同时激活,如 Filter 可以同时有多个过滤器;或者根据不同的条件,同时激活多个实现类,这就需要第四个特性——自动激活。
1.5.4 自动激活
使用 @Activate 注解,可以标记对应的扩展点默认被激活启用。该注解还可以传入不同的参数,设置扩展点在不同的条件下被自动激活。主要的使用场景是某个扩展点的多个实现类需要同时启用(比如 Filter 扩展点)。
二、扩展点注解
2.1 扩展点注解:@SPI
可用在类、接口和枚举类上,Dubbo 框架中都是在接口上用。作用是标记这个接口是一个 Dubbo SPI 接口,是一个扩展点,可以有多个不同的内置或用户定义的实现。运行时需要通过配置找到具体的实现类。
源码:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
/**
* default extension name
*/
String value() default "";
}
SPI 注解的 value 属性可以设置默认实现类。例如,Transporter 使用 netty 作为默认实现。
@SPI("netty")
public interface Transporter {
...
}
Dubbo 中有很多地方通过 getExtension(Class<T> type, String name)
来获取扩展点接口的具体实现,此时会对传入的 Class 做校验,判断是否是接口,以及是否有 @SPI 注解,两者缺一不可。
2.2 扩展点自适应注解:@Adaptive
@Adaptive 可以标记在类、接口、枚举类和方法上,整个 Dubbo 框架中,只有几个地方在类级别上,如 AdaptiveExtensionFactory 和 AdaptiveCompiler,其余都在方法上。如果标记在方法上,即方法级别的注解,则可以通过参数动态获得实现类。方法级别的注解在第一次 getExtension 时,会自动生成和编译一个动态的 Adaptive 类,从而达到动态实现类的效果。
例如,Transporter 接口 bind 和 connect 方法都有 @Adaptive 注解(see 1.5.3)。Dubbo 在初始化扩展点时,会生成一个 Transporter$Adaptive类,里面会实现这两个方法,方法中会有一些抽象的通用逻辑,通过 @Adaptive 中传入的参数,找到并调用真正的实现类。熟悉装饰器模式的读者会很容易理解这部分的逻辑。原理在第四部分讲解。
下面是自动生成的 Transporter$Adaptive#bind 实现代码
public org.apache.dubbo.remoting.Server bind(org.apache.dubbo.common.URL arg0, org.apache.dubbo.remoting.ChannelHandler arg1) throws org.apache.dubbo.remoting.RemotingException {
...
org.apache.dubbo.common.URL url = arg0;
// 通过 @Adaptive 注解中的两个 key 去寻找实现类的名称
String extName = url.getParameter("server",url.getParameter("transporter","netty"));
...
try {
// 根据 url 中的参数,尝试获取真正的扩展点实现类
extension = (org.apache.dubbo.remoting.Transporter)ExtensionLoader.getExtensionLoader(org.apache.dubbo.remoting.Transporter.class).getExtension(extName);
}catch(Exception e){
// 获取失败使用默认的 netty 实现
...
extension = (org.apache.dubbo.remoting.Transporter)ExtensionLoader.getExtensionLoader(org.apache.dubbo.remoting.Transporter.class).getExtension(netty);
}
// 最终调用具体扩展点实现类的 bind 方法
return extension.bind(arg0,arg1);
}
生成的源码中可以看出,自动生成的代码实现了很多通用功能,最终调用真正的接口实现。
该注解放在实现类上,则整个实现类会直接作为默认实现。不在自动生成上面的代码。在扩展点接口的多个实现里,只能有一个实现可以加 @Adaptive 注解。如果多个实现类都有该注解,则抛出异常:More than 1 adaptive class found。
@Adaptive 注解源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
// 可以设置多个 key,按顺序匹配。
String[] value() default {};
}
注解可以传入 value 数组,会依序进行匹配,如果匹配不到,则使用 @SPI 的默认值再去匹配,如果没有配置会抛出 IllegalStateException。
另外,如果包装类(Wrapper)没有用 @Adaptive 指定 key值,也没有填写默认值,则 Dubbo 会自动把接口名称根据驼峰大小分开,并用 “.” 连接起来,以此作为默认实现类的名称,如 org.apache.dubbo.xxx.YyyInvokerWrapper 会被转化成 yyy.invoker.wrapper。
最后,为什么有些实现类上会标注 @Adaptive 注解呢?主要是为了直接固定对应的实现而不需要动态生成代码实现。代码中,会缓存两个与 @Adaptive 有关的对象,一个缓存在 cachedAdaptiveClass,另一个缓存在 cachedAdaptiveInstance。扩展点初始化时,如果发现类上有 @Adaptive ,则直接赋值给 cachedAdaptiveClass,后续实例化类的时候,就不会再动态生成代码了,而是直接使用缓存的类生成对象并缓存到cachedAdaptiveInstance。如果注解在接口的方法上,则会根据参数,动态获得扩展点的实现,生成 Adaptive 类,再缓存到 cachedAdaptiveInstance。
2.3 扩展点自动激活注解:@Activate
@Activate 可以标记在类、接口、枚举类和方法上。主要使用在有多个扩展点实现、需要根据不同条件被激活的长江中,如 Filter 需要多个同时激活,因为每个 Filter 实现的不同的功能。@Activate 可以传入的参数很多:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Activate {
// URL 中的分组如果匹配则激活,则可以设置多个
String[] group() default {};
// 查找 URL 如果含有该 key,则激活
String[] value() default {};
// 填写扩展点列表,表示哪些扩展点要在本扩展点前
@Deprecated
String[] before() default {};
// 同上,在后
@Deprecated
String[] after() default {};
// 排序值
int order() default 0;
}
三、ExtensionLoader 的工作原理
ExtensionLoader 是整个扩展机制的主要逻辑类,实现了配置的加载、扩展类的缓存、自适应对象生成等所有工作。本节将结合源码讲解整个 ExtensionLoader 的工作流程。
首先看 ExtensionLoader 的使用:
ExtensionLoader<Transporter> extensionLoader = ExtensionLoader.getExtensionLoader(Transporter.class);
System.out.println(extensionLoader.getExtension("true"));
ExtensionLoader 私有了构造器,只有一个静态方法供外部访问,通过 getExtensionLoader 获取 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 an interface!");
}
// 必须要有 SPI 注解
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
// 先从缓存拿
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
// 缓存没有则使用私有的构造器创建对象并从缓存中获取返回。
if (loader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
// 构造函数 此时注入了自适应的 ExtensionFactory
private ExtensionLoader(Class<?> type) {
this.type = type;
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
3.1 工作流程
ExtensionLoader 的逻辑入口可以分为 getExtension、getAdaptiveExtension、getActivateExtension 三个。
getActivateExtension 对 getExtension 依赖重,getAdaptiveExtension 相对独立。
getActivateExtension 只是根据不同条件同时激活多个普通扩展类。因此该方法只会做一些通用逻辑判断,最终还是调用 getExtension 获得具体扩展点实现类。
代码实现,首先看 Fields
// 扩展类与对应的扩展类加载器缓存
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
// 扩展类与类初始化后的实例
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
private final Class<?> type;
private final ExtensionFactory objectFactory;
// 扩展类与扩展名缓存
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
// 普通扩展类缓存,不包括自适应扩展类和 Wrapper 类
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
// 扩展名与 @Activate 的缓存
private final Map<String, Object> cachedActivates = new ConcurrentHashMap<>();
// 扩展名与扩展对象缓存
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
// 实例化后的 Adaptive 扩展对象,只能同时存在一个
private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
// 实例化后的 Adaptive 扩展类
private volatile Class<?> cachedAdaptiveClass = null;
private String cachedDefaultName;
private volatile Throwable createAdaptiveInstanceError;
private Set<Class<?>> cachedWrapperClasses;
3.2 getExtension 的实现原理
getExtension(String name) 实现了一个完整的普通扩展类的加载过程。每一步都会先读缓存
流程:
- 框架读取 SPI 对应路径下的配置文件,根据配置加载所有扩展类并缓存(不初始化)。
- 根据传入的名称初始化对应的扩展类。
- 尝试查找符合条件的包装类
- 返回对应的扩展类实例
代码实现,这里就不贴了。请自行查看
这里有一步依赖注入,为 injectExtension 方法,类似 Spring IoC 的机制,实现原理为:反射获取所有拥有 setter
的参数类型,然后通过 ExtensionFactory 查找类型相同的扩展类实例,如果找到则method 调用 invoke ,完成 setter 注入。另外,Wrapper 类的构造参数注入,也是通过这个方法实现的。
3.3 getAdaptiveExtension 的实现原理
首先看源码中的测试用例
@Test
public void test_getAdaptiveExtension_defaultAdaptiveKey() throws Exception {
{
SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
Map<String, String> map = new HashMap<String, String>();
URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);
String echo = ext.echo(url, "haha");
assertEquals("Ext1Impl1-echo", echo);
}
{
SimpleExt ext = ExtensionLoader.getExtensionLoader(SimpleExt.class).getAdaptiveExtension();
Map<String, String> map = new HashMap<String, String>();
map.put("simple.ext", "impl2");
URL url = new URL("p1", "1.2.3.4", 1010, "path1", map);
String echo = ext.echo(url, "haha");
assertEquals("Ext1Impl2-echo", echo);
}
}
@SPI("impl1")
public interface SimpleExt {
// @Adaptive example, do not specify a explicit key.
@Adaptive
String echo(URL url, String s);
@Adaptive({"key1", "key2"})
String yell(URL url, String s);
// no @Adaptive
String bang(URL url, int i);
}
SPI 上默认实现为 impl1。
生成的自适应代码如下:
package org.apache.dubbo.common.extension.ext1;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt {
public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("simple.ext", "impl1");
if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([simple.ext])");
org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt)ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
return extension.echo(arg0, arg1);
}
public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([key1, key2])");
org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt)ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
return extension.yell(arg0, arg1);
}
public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) {
throw new UnsupportedOperationException("The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!");
}
}
package org.apache.dubbo.common.extension.ext1;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class SimpleExt$Adaptive implements org.apache.dubbo.common.extension.ext1.SimpleExt {
public java.lang.String bang(org.apache.dubbo.common.URL arg0, int arg1) {
throw new UnsupportedOperationException("The method public abstract java.lang.String org.apache.dubbo.common.extension.ext1.SimpleExt.bang(org.apache.dubbo.common.URL,int) of interface org.apache.dubbo.common.extension.ext1.SimpleExt is not adaptive method!");
}
public java.lang.String echo(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("simple.ext", "impl1");
if (extName == null)
throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([simple.ext])");
org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
return extension.echo(arg0, arg1);
}
public java.lang.String yell(org.apache.dubbo.common.URL arg0, java.lang.String arg1) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
String extName = url.getParameter("key1", url.getParameter("key2", "impl1"));
if (extName == null)
throw new IllegalStateException("Failed to get extension (org.apache.dubbo.common.extension.ext1.SimpleExt) name from url (" + url.toString() + ") use keys([key1, key2])");
org.apache.dubbo.common.extension.ext1.SimpleExt extension = (org.apache.dubbo.common.extension.ext1.SimpleExt) ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.extension.ext1.SimpleExt.class).getExtension(extName);
return extension.yell(arg0, arg1);
}
}
自适应扩展点是通过自动生成实现类字符串,然后动态编译并生成对象实现的。
代码生成过程可参考:
AdaptiveClassCodeGenerator.java
Dubbo 的编译器也是一个 Adaptive 接口,但 @Adaptive 是加在类 AdaptiveCompiler 上的。所有这个类就是默认实现,不用做代码生成和编译。
如果一个接口上的 @SPI 注解了默认实现,且方法上的 @Adaptive 也标明了默认实现,那么会使用哪个标注呢?从第二个单元测试用例动态生成的类可以看到,String extName = url.getParameter(“key1”, url.getParameter(“key2”, “impl1”)),是先根据key1找,再找key2,没有的话使用默认的实现。
3.4 getActivateExtension 实现原理
单元测试用例:
URL url = URL.valueOf("test://localhost/test");
List<ActivateExt1> list = getExtensionLoader(ActivateExt1.class)
.getActivateExtension(url, new String[]{}, "default_group");
Assertions.assertEquals(1, list.size());
Assertions.assertSame(list.get(0).getClass(), ActivateExt1Impl1.class);
@Activate(group = {"default_group"})
public class ActivateExt1Impl1 implements ActivateExt1 {
public String echo(String msg) {
return msg;
}
}
getActivateExtension(URL url, String[] values, String group)
可以获取所有自动激活扩展点。实现逻辑比较简单:
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<>();
List<String> names = values == null ? new ArrayList<>(0) : Arrays.asList(values);
if (!names.contains(REMOVE_VALUE_PREFIX + DEFAULT_KEY)) {
// 加载所有扩展点(先缓存获取)
getExtensionClasses();
// 遍历 cachedActivates 缓存
for (Map.Entry<String, Object> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Object activate = entry.getValue();
String[] activateGroup, activateValue;
// 必须要有 Activate 注解
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;
}
// 匹配 name 和 group,通过getExtension 获取实例后添加到 exts
if (isMatchGroup(group, activateGroup)
&& !names.contains(name)
&& !names.contains(REMOVE_VALUE_PREFIX + name)
&& isActive(activateValue, url)) {
exts.add(getExtension(name));
}
}
// 排序
exts.sort(ActivateComparator.COMPARATOR);
}
// URL 如果传入 -default,所有默认的 @Activate 都不被激活,只有 URL 指定的被激活
// 如果传入了 -开头的扩展点名,则该扩展点也不会激活
List<T> usrs = new ArrayList<>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if (!name.startsWith(REMOVE_VALUE_PREFIX)
&& !names.contains(REMOVE_VALUE_PREFIX + name)) {
if (DEFAULT_KEY.equals(name)) {
if (!usrs.isEmpty()) {
exts.addAll(0, usrs);
usrs.clear();
}
} else {
usrs.add(getExtension(name));
}
}
}
if (!usrs.isEmpty()) {
exts.addAll(usrs);
}
return exts;
}
该方法也是以 getExtension 为基础的。
3.5 ExtensionFactory 实现原理
ExtensionFactory 用于在 ExtensionLoader 注入依赖时获取实例是一个工厂类。
@SPI
public interface ExtensionFactory {
/**
* Get extension.
*
* @param type object type.
* @param name object name.
* @return object instance.
*/
<T> T getExtension(Class<T> type, String name);
}
工厂类也是一个 SPI 类,其实现有:
@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>();
// 添加所有 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) {
for (ExtensionFactory factory : factories) {
T extension = factory.getExtension(type, name);
if (extension != null) {
return extension;
}
}
return null;
}
}
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;
}
}
public class SpringExtensionFactory implements ExtensionFactory {
private static final Logger logger = LoggerFactory.getLogger(SpringExtensionFactory.class);
private static final Set<ApplicationContext> CONTEXTS = new ConcurrentHashSet<ApplicationContext>();
public static void addApplicationContext(ApplicationContext context) {
CONTEXTS.add(context);
if (context instanceof ConfigurableApplicationContext) {
((ConfigurableApplicationContext) context).registerShutdownHook();
}
}
public static void removeApplicationContext(ApplicationContext context) {
CONTEXTS.remove(context);
}
public static Set<ApplicationContext> getContexts() {
return CONTEXTS;
}
// currently for test purpose
public static void clearContexts() {
CONTEXTS.clear();
}
@Override
@SuppressWarnings("unchecked")
public <T> T getExtension(Class<T> type, String name) {
// 带有 SPI 注解的接口,要使用 SpiExtensionFactory 获取扩展类实例
//SPI should be get from SpiExtensionFactory
if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
return null;
}
// 根据实例 name 和 类型,从 Spring 容器获取实例
for (ApplicationContext context : CONTEXTS) {
T bean = BeanFactoryUtils.getOptionalBean(context, name, type);
if (bean != null) {
return bean;
}
}
logger.warn("No spring extension (bean) named:" + name + ", try to find an extension (bean) of type " + type.getName());
return null;
}
}
AdaptiveExtensionFactory 上有 @Adaptive 注解。因此,它是一开始的默认实现。除了该实现之外,还有 SpiExtensionFactory 和 SpringExtensionFactory。意思是除了可以从 Dubbo SPI 管理的容器获取扩展类实例,还可以从 Spring 容器获取。
AdaptiveExtensionFactory 是默认实现,在构造器中添加了所有的 ExtensionFactory 实例,在使用 ExtensionFactory 时,还是使用的 AdaptiveExtensionFactory 的 getExtension 方法,遍历所有实现依次加载,是有序的,为先 SPI 再 Spring,结合其他 ExtensionFactory 实现看,带有 @SPI 的接口是只使用 SpiExtensionFactory 加载的,如果不是,先 SPI 再 Spring。
四、扩展点动态编译的实现
Dubbo SPI 的自适应特性让整个框架非常灵活,动态编译是自适应特性的基础,Dubbo 生成自适应类是拼接类的字符串,是需要进行编译才能变成 Class 的。虽然反射可以动态代理一个类,但在性能上和直接编译好的 Class 会有一定差距。Dubbo SPI 通过动态生成代码,配合动态编译器,灵活创建自适应类。本节介绍 Dubbo SPI 动态编译器的种类以及对应实现原理。
4.1 总体结构
Dubbo 编译器接口为 Compiler:
/**
* Compiler. (SPI, Singleton, ThreadSafe)
*/
@SPI("javassist")
public interface Compiler {
/**
* Compile java source code.
*
* @param code Java source code
* @param classLoader classloader
* @return Compiled class
*/
Class<?> compile(String code, ClassLoader classLoader);
}
可以发现是 @SPI 注解,可扩展的,默认编译器是 javassist,单例且线程安全。
用户若想改变默认编译器,可以通过
<dubbo:application compiler="jdk"/>
配置。
其实现类有 AdaptiveCompiler、JavassistCompiler、JdkCompiler。
AdaptiveCompiler 和 AdaptiveExtensionFactory 同理,类上标注了 @Adaptive 注解,固定为默认实现,用于管理其他的 Compiler 实现。
@Adaptive
public class AdaptiveCompiler implements Compiler {
private static volatile String DEFAULT_COMPILER;
// set 方法在 ApplicationConfig 中被调用,dubbo 启动时解析配置中的标签设置默认值。
public static void setDefaultCompiler(String compiler) {
DEFAULT_COMPILER = compiler;
}
@Override
public Class<?> compile(String code, ClassLoader classLoader) {
Compiler compiler;
// 获取 loader
ExtensionLoader<Compiler> loader = ExtensionLoader.getExtensionLoader(Compiler.class);
String name = DEFAULT_COMPILER; // copy reference
if (name != null && name.length() > 0) {
compiler = loader.getExtension(name);
} else {
// ExtensionLoader 加载的默认实现为 @SPI 上注解的,即 Javassist
compiler = loader.getDefaultExtension();
}
return compiler.compile(code, classLoader);
}
}
4.2 Javassist 动态代码编译
Java 中动态生成 Class 的方式有很多,可以直接基于字节码方式生成,常见的有 CGLIB、ASM、Javassist 等。自适应扩展点使用了生成字符串代码再编译成 Class 的方式。
使用示例:
// 初始化类池
ClassPool classPool = ClassPool.getDefault();
// 创建类
CtClass ctClass = classPool.makeClass("Hello World");
// 创建方法
CtMethod ctMethod = CtNewMethod.make("public static void test() {System.out.println(\"Hello World\");}", ctClass);
// 添加方法
ctClass.addMethod(ctMethod);
// 生成类
Class aClass = ctClass.toClass();
// 反射调用实例的 test 方法
Object instance = aClass.newInstance();
Method m = aClass.getDeclaredMethod("test", null);
m.invoke(instance, null);
Dubbo,JavassistCompiler 通过不断正则匹配不同部位的代码,最终生成类。
4.3 JDK 动态代码编译
为 Dubbo 编译器另一种实现,是 JDK 原生编译器,包位于 javax.tools 下。
详略。
小结
本章介绍了 Dubbo SPI 与 Java SPI 区别,Dubbo SPI 的特性、配置规范和内部缓存等。其次介绍了最重要的3个注解 @SPI、@Adaptive、@Activate 的作用及原理。然后分析了 ExtensionLoader 的源码,以及 ExtensionFactory 原理。最后讲解了自适应动态编译的实现原理。
« Dubbo 启停原理解析
深入理解 Java 虚拟机第三版 »