02.dubbo源码解析之Dubbo扩展点加载
Posted WallaceF技术打怪升级之旅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了02.dubbo源码解析之Dubbo扩展点加载相关的知识,希望对你有一定的参考价值。
1.Java SPI 机制理解
查看:Java中SPI机制深入及源码解析
2.dubbo 对 SPI 机制的扩展点
问题1:为什么dubbo不采用java spi,而是自己实现一个SPI机制呢
Java SPI的使用很简单。也做到了基本的加载扩展点的功能。但Java SPI有以下的不足:
需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要的实现。
配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
扩展如果依赖其他的扩展,做不到自动注入和装配
不提供类似于Spring的IOC和AOP功能
扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持
所以Java SPI应付一些简单的场景是可以的,但对于Dubbo,它的功能还是比较弱的。Dubbo对原生SPI机制进行了一些扩展。接下来,我们就更深入地了解下Dubbo的SPI机制。
Dubbo 改进了 JDK 标准的 SPI 的以下问题:
JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
其实最核心的改进是第一个和第三个问题。
问题2:dubbo spi基本使用
我们继续使用上面的例子。由于Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader
类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo
路径下,配置内容如下。
serviceA = com.fanweiwei.spi.impl.DemoServiceImplA
serviceB = com.fanweiwei.spi.impl.DemoServiceImplB
首先,我们定义一个接口,名称为 DemoService
@SPI("demo")
public interface DemoService {
void sayHello();
}
接下来定义两个实现类,分别为DemoServiceImplA
和DemoServiceImplB
public class DemoServiceImplA implements DemoService {
public void sayHello() {
System.out.println(">>>> THIS IS A!");
}
}
public class DemoServiceImplB implements DemoService {
public void sayHello() {
System.out.println(">>>> THIS IS B!");
}
}
需要在 DemoService 接口上标注 @SPI 注解。下面来演示 Dubbo SPI 的用法:
public class SpiTest {
public static void main(String[] args) {
ExtensionLoader<DemoService> loader = ExtensionLoader.getExtensionLoader(DemoService.class);
// service A
DemoService serviceA = loader.getExtension("serviceA");
serviceA.sayHello();
// service B
DemoService serviceB = loader.getExtension("serviceB");
serviceB.sayHello();
}
}
程序将输出
log4j:WARN No appenders could be found for logger (org.apache.dubbo.common.logger.LoggerFactory).
log4j:WARN Please initialize the log4j system properly.
log4j:WARN See http://logging.apache.org/log4j/1.2/faq.html#noconfig for more info.
>>>> THIS IS A!
>>>> THIS IS B!
具体demo代码请查看:C:\Java\IdeaProjects\dubbo\dubbo-common
问题3. dubbo的扩展点的分类与缓存
Dubbo 扩展点的加载
在阅读本文前,如果你阅读过Java SPI 相关内容,大概能回忆起来有 /META-INF/services (dubbo-common模块)
这样一个目录。在这个目录下有一个以接口命名的文件,文件的内容为接口具体实现类的全限定名。在 Dubbo 中我们也能找到类似的设计。
META-INF/services/(兼容JAVA SPI)
META-INF/dubbo/(自定义扩展点实现)
META-INF/dubbo/internal/(Dubbo内部扩展点实现)
例子如下:META-INF/dubbo/com.alibaba.dubbo.rpc.Protocol
,内容为:xxx=com.alibaba.xxx.XxxProtocol
实现内容:
package com.alibaba.xxx;
import com.alibaba.dubbo.rpc.Protocol;
public class XxxProtocol implemenets Protocol {
// ...
}
注意:扩展点使用单一实例加载(请确保扩展实现的线程安全性),Cache在ExtensionLoader中
Dubbo SPI 可以分为Class缓存、实例缓存。这两项又能扩展为 普通扩展类、包装扩展类(Wrapper类)、自适应扩展类(Adaptive类)等,而这对应缓存加载后,统一在 ExtensionLoader
中存储,具体如下:
org.apache.dubbo.common.extension.ExtensionLoader
//普通扩展类缓存,不包自适应扩展类和Wrapper类
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
//Wrapper类缓存
private Set<Class<?>> cachedWrapperClasses;
//自适应扩展类缓存
private volatile Class<?> cachedAdaptiveClass = null;
//扩展名与扩展对象缓存
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
//实例化后的自适应(Adaptive)扩展对象,能同时存在一个
private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
//扩展类与扩展名缓存
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
//扩展类与对应的扩展类加载器缓存
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>(64);
//扩展类与类初始化后的实例
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>(64);
//扩展名与@Activate的缓存
private final Map<String, Object> cachedActivates = new ConcurrentHashMap<>();
扩展点注解解析
@SPI
作用:标记这个是一个Dubbo SPI接口,即是一个扩展点,运行是可以找到具体的实现类
(RetentionPolicy.RUNTIME)
({ElementType.TYPE})
public SPI {
/**实现key的名称**/
String value() default "";
}
例子:org.apache.dubbo.registry.client.ServiceDiscovery 使用@SPI("zookeeper") Dubbo中很多地方通过SpiExtensionFactory.
@Adaptive
作用:标记在类(AdaptiveCompiler)、接口(AdaptiveExtensionFactory)、枚举类和方法上,方法级别注解在第一次getExtension时,会自动生成和编译一个动态的Adaptive类,从而达到动态实现类的效果。
@Activate
作用:可以标记在类、接口、枚举类和方法上。有两种注解方式:一种是注解在类上,一种是注解在方法上
注解在类上,而且是注解在实现类上,目前dubbo只有AdaptiveCompiler和AdaptiveExtensionFactory类上标注了此注解,这是些特殊的类,ExtensionLoader需要依赖他们工作,所以得使用此方式。
注解在方法上,注解在接口的方法上,除了上面两个类之外,所有的都是注解在方法上。ExtensionLoader根据接口定义动态的生成适配器代码,并实例化这个生成的动态类。被Adaptive注解的方法会生成具体的方法实现。没有注解的方法生成的实现都是抛不支持的操作异常UnsupportedOperationException。被注解的方法在生成的动态类中,会根据url里的参数信息,来决定实际调用哪个扩展
具体属性解析参考:org.apache.dubbo.common.extension.Activate @Activate的参数
参数名 | 效果 |
---|---|
String[] group() | URL中的分组如果匹配则激活 |
String[] value() | URL中如果包含该key值,则会激活 |
String[] before() | 填写扩展点列表,表示哪些扩展点要在本扩展点之前激活 |
String[] after() | 表示哪些扩展点需要在本扩展点之后激活 |
int order() | 排序信息 |
@SPI、@Activate 区别:
Activate注解表示一个扩展是否被激活(使用),可以放在类定义和方法上,dubbo用它在spi扩展类定义上,表示这个扩展实现激活条件和时机
具体使用方法:https://www.jianshu.com/p/bc523348f519
问题4.ExtensionLoader工作原理
ExtensionLoader: 是最核心的类,负责扩展点的加载和生命周期管理。
ExtensionLoader 的方法比较多,比较常用的方法有:
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type)
//getExtension --> 获取普通扩展类
public T getExtension(String name)
//getActivateExtension --> 获取自动激活的扩展类(cachedActivates存储)
public T getAdaptiveExtension()
Dubbo中随处可见这样的代码:
LoadBalance lb = ExtensionLoader.getExtensionLoader(LoadBalance.class).getExtension(loadbalanceName)
//getAdaptiveExtension --> 获取自适应扩展扩展类(cachedAdaptiveInstance存储)
RouterFactory routerFactory = ExtensionLoader.getExtensionLoader(RouterFactory.class).getAdaptiveExtension()
1.getExtension介绍 :
getExtension(String name)
是整个扩展加载器中最核心的方法,实现一个完成的普通扩展类加载过程。其初始化过程可分为4步:
读取SPI对应路径下的配置文件,首先尝试从本地缓存
cachedInstances
中获取,如果获取不到,那么调用createExtension(name)
创建扩展类实例并本地缓存传入
name
初始化对应的扩展类查找符合条件的包装类
返回对应的扩展类对象
源码分析:
public T getExtension(String name) {
Holder<Object> holder = cachedInstances.get(name);
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<Object>());
holder = cachedInstances.get(name);
}
Object instance = holder.get();
// 从缓存中获取,如果不存在就创建
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
在调用createExtension
开始创建的过程中,如果不存在扩展类,则会从META-INF/services/
、META-INF/dubbo/
、META-INF/dubbo/internal/
这几个路径下读取所以配置文件,通过io解析字符串,从而获取到对应扩展类的全称
private T createExtension(String name) {
// 根据扩展点名称得到扩展类,比如对于LoadBalance,根据random得到RandomLoadBalance类
Class<?> clazz = getExtensionClasses().get(name);
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 使用反射调用nesInstance来创建扩展类的一个示例
EXTENSION_INSTANCES.putIfAbsent(clazz, (T) clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 对扩展类示例进行依赖注入
injectExtension(instance);
// 如果有wrapper,添加wrapper
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
}
当中getExtensionClasses()
为扩展点配置信息方法加载并分类缓存起来,对应上面提到ExtensionLoader属性(cache缓存地方)。以下代码有删减
ExtensionLoader#loadClass 中截取的某些代码
if (clazz.isAnnotationPresent(Adaptive.class)) {
//class对象有@Adaptive,存储到cachedAdaptiveClass
cacheAdaptiveClass(clazz, overridden);
} else if (isWrapperClass(clazz)) {
//clazz对象为Wrapper包装扩展类,存储到cachedWrapperClasses
cacheWrapperClass(clazz);
} else {
//普通的class类,存储于cachedClasses
clazz.getConstructor();
if (StringUtils.isEmpty(name)) {
name = findAnnotationName(clazz);
}
}
依赖注入
通过ExtensionLoader#createExtension
下截取代码:
// 对扩展类示例进行依赖注入
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
// 如果有wrapper,添加wrapper
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
方法injectExtension提现了类似Spring IOC机制,原理是通过反射获取类的所有方法,然后遍历以set开头的方法,得到set方法的参数类型,再通过ExtensionFactory寻找参数类型相同的扩展实例,如找到,再设置进去
2.getAdaptiveExtension介绍
与getExtension()
一样,在getAdaptiveExtension()
方法中,主要分为7步骤:
生成package、import、类名称等头部信息。
遍历接口所有方法
生成参数为空校验代码
生成默认实现类名称
生成获取扩展点名称的代码
生成获取具体扩展实现类代码
生成调用结果代码
配置文件:
# 位置:dubbo-common\src\main\resources\META-INF\dubbo\internal\com.fanweiwei.spi.adaptive.AdaptiveExt2
# Comment 1
dubbo=com.fanweiwei.spi.adaptive.DubboAdaptiveExt2
cloud=com.fanweiwei.spi.adaptive.SpringCloudAdaptiveExt2
thrift=com.fanweiwei.spi.adaptive.ThriftAdaptiveExt2
接口代码:
//@SPI
@SPI("dubbo")
public interface AdaptiveExt2 {
@Adaptive
String echo(String msg, URL url);
}
实现类:
public class DubboAdaptiveExt2 implements AdaptiveExt2 {
public String echo(String msg, URL url) {
return "dubbo";
}
}
public class SpringCloudAdaptiveExt2 implements AdaptiveExt2 {
public String echo(String msg, URL url) {
return "spring cloud";
}
}
public class ThriftAdaptiveExt2 implements AdaptiveExt2 {
public String echo(String msg, URL url) {
return "thrift";
}
}
Test 代码:
/**
* @SPI("dubbo") 如果在spi中没有体现,那么需要在url中有所体现
* 输出:spring cloud
*/
public void test() {
ExtensionLoader<AdaptiveExt2> loader = ExtensionLoader.getExtensionLoader(AdaptiveExt2.class);
AdaptiveExt2 adaptiveExtension = loader.getAdaptiveExtension();
System.out.println(adaptiveExtension.getClass());
URL url = URL.valueOf("test://localhost/test?adaptive.ext2=cloud");
System.out.println(adaptiveExtension.echo("d", url));
}
/**
* @SPI + URL 的链接中增加指定的参数
* 输出:dubbo
*/
public void test() {
ExtensionLoader<AdaptiveExt2> loader = ExtensionLoader.getExtensionLoader(AdaptiveExt2.class);
AdaptiveExt2 adaptiveExtension = loader.getAdaptiveExtension();
System.out.println(adaptiveExtension.getClass());
URL url = URL.valueOf("test://localhost/test?adaptive.ext2=dubbo");
System.out.println(adaptiveExtension.echo("d", url));
}
/**
* @SPI("dubbo")
* @Adaptive({"t"})
* 显示:thrift
*/
public void test() {
URL url = URL.valueOf("test://localhost/test?t=thrift");
ExtensionLoader<AdaptiveExt2> loader = ExtensionLoader.getExtensionLoader(AdaptiveExt2.class);
//可以通过指定名称来动态切换需要的类
AdaptiveExt2 adaptiveExtension = loader.getAdaptiveExtension();
System.out.println(adaptiveExtension.getClass());
System.out.println(adaptiveExtension.echo("d", url));
}
分析:
从上面的几个测试用例,可以得到下面的结论:
在类上加上@Adaptive注解的类,是最为明确的创建对应类型Adaptive类。所以他优先级最高。
@SPI注解中的value是默认值,如果通过URL获取不到关于取哪个类作为Adaptive类的话,就使用这个默认值,当然如果URL中可以获取到,就用URL中的。
可以再方法上增加@Adaptive注解,注解中的value与链接中的参数的key一致,链接中的key对应的value就是spi中的name,获取相应的实现类。
源码分析:https://www.jianshu.com/p/dc616814ce98
3.getActivateExtension介绍
对应是@Activate注解,其主要分为4个步骤:
检查缓存,如果缓存中没有,则初始化所有扩展类实现的集合
遍历整个@Activate注解集合,根据传入URL匹配,得到所有集合符合条件的扩展类的实现
遍历所有用户自定义扩展类名称,根据用户URL配置的顺序,调整扩展点激活顺序
返回所有自动激活类集合
配置文件
#位置:~\dubbo-common\src\main\resources\META-INF\dubbo\internal\com.fanweiwei.spi.activate.ActivateExt1
group=com.fanweiwei.spi.activate.GroupActivateExtImpl
value=com.fanweiwei.spi.activate.ValueActivateExtImpl
order1=com.fanweiwei.spi.activate.OrderActivateExtImpl1
order2=com.fanweiwei.spi.activate.OrderActivateExtImpl2
com.fanweiwei.spi.activate.ActivateExt1Impl1
接口代码
public interface ActivateExt1 {
String echo(String msg);
}
Test 代码:
/**
* 显示:
* 1
* class com.fanweiwei.spi.activate.ActivateExt1Impl1
*/
@Test
public void testDefault() {
ExtensionLoader<ActivateExt1> loader = ExtensionLoader.getExtensionLoader(ActivateExt1.class);
URL url = URL.valueOf("test://localhost/test");
//查询组为default_group的ActivateExt1的实现
List<ActivateExt1> list = loader.getActivateExtension(url, new String[]{}, "default_group");
System.out.println(list.size());
System.out.println(list.get(0).getClass());
}
/**
* 显示
* 1
* class com.fanweiwei.spi.activate.GroupActivateExtImpl
*/
@Test
public void test2() {
URL url = URL.valueOf("test://localhost/test");
//查询组为group2的ActivateExt1的实现
List<ActivateExt1> list = ExtensionLoader.getExtensionLoader(ActivateExt1.class).getActivateExtension(url, new String[]{}, "group2");
System.out.println(list.size());
System.out.println(list.get(0).getClass());
}
结论:从上面的几个测试用例,可以得到下面的结论:
根据loader.getActivateExtension中的group和搜索到此类型的实例进行比较,如果group能匹配到,就是我们选择的,也就是在此条件下需要激活的。
@Activate中的value是参数是第二层过滤参数(第一层是通过group),在group校验通过的前提下,如果URL中的参数(k)与值(v)中的参数名同@Activate中的value值一致或者包含,那么才会被选中。相当于加入了value后,条件更为苛刻点,需要URL中有此参数并且,参数必须有值。
@Activate的order参数对于同一个类型的多个扩展来说,order值越小,优先级越高。
4.ExtensionFactory的实现原理
由上面介绍个方法,终究一点都是同个ExtensionLoader类开始的,它是整个SPI的核心,下面我们将要简单分析以下ExtensionLoader。ExtensionLoader类本身通过工厂方法ExtensionFactory
创建的,并且这个工厂类上也有SPI注解,如图:
/**
* ExtensionFactory
* 扩展点工厂,加载接口的实现类。这个接口包括Dubbo中的SPI接口和一般类型的接口
*/
@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接口
② 接口中只有一个方法getExtension 用于获取接口的扩展点实例
③ 接口中有两个参数,一个是接口的类型,一个扩展实例的名称
我们在看一下ExtensionFactory接口在Dubbo框架中结构层次:
从上图可以看出 ExtensionFactory接口有三个实现类:SpiExtensionFactory
、SpringExtensionFactory
、AdaptiveExtensionFactory
4.1 问题:Dubbo和Spring容器之间如何打通?
解析:SpringExtensionFactory提供了spring上下文的静态方法,把spring上下文保存到Set集合中,当getExtension获取扩展类是,会遍历Set集合所有Spring上下文,如果没有匹配,返回null,具体如下代码:
ps: dubbo-2.7.x版本(master版本)
① SpringExtensionFactory
public class SpringExtensionFactory implements ExtensionFactory{
/** 存储spring的上下文 **/
private static final Set<ApplicationContext> CONTEXTS = new ConcurrentHashSet<ApplicationContext>();
//spring上下文先通过这里保存
public static void addApplicationContext(ApplicationContext context) {
CONTEXTS.add(context);
...
}
public <T> T getExtension(Class<T> type, String name) {
//SPI should be get from SpiExtensionFactory
if (type.isInterface() && type.isAnnotationPresent(SPI.class)) {
return null;
}
//遍历Set集合中的的spring上下文,先从name中查找
for (ApplicationContext context : CONTEXTS) {
T bean = BeanFactoryUtils.getOptionalBean(context, name, type);
if (bean != null) {
return bean;
}
}
....
//根据那么和type找不到返回null
return null;
}
}
而spring上下文由究竟是什么时候保存起来,这个主要由
ReferenceBean
、ServiceBean
中调用方法保存上下文,后面如有时间会具体分析
② SpiExtensionFactory
解析:SpiExtensionFactory主要就是获取扩展点接口对应的Adaptive实现类
//根据类型获取所有的扩展点加载器
ExtensionLoader<T> loader = ExtensionLoader.getExtensionLoader(type);
//如果缓存的扩展点类不为空,则直接返回Adaptive实例
if (!loader.getSupportedExtensions().isEmpty()) {
return loader.getAdaptiveExtension();
}
ps:SpiExtensionFactory最终回到实现AdaptiveExtensionFactory上,也就是@Adaptive注解
③ AdaptiveExtensionFactory
解析:用于缓存所有工厂实现,包括SpiExtensionFactory、SpringExtensionFactory
private final List<ExtensionFactory> factories;
ExtensionLoader<ExtensionFactory> loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class);
//遍历所欲的工厂名单,获取对应的工厂,并保存到factories列表中
List<ExtensionFactory> list = new ArrayList<ExtensionFactory>();
for (String name : loader.getSupportedExtensions()) {
list.add(loader.getExtension(name));
}
factories = Collections.unmodifiableList(list);
AdaptiveExtensionFactory缓存的工厂会通过TreeSet进行排序,Spi排在前面,Spring排在后面。当调用getExtension方法时,会遍历所有的工厂,先从SPI容器中获取扩展类,如果没找到,再从Spring容器中查找 getExtension 方法
public <T> T getExtension(Class<T> type, String name) {
//遍历所有工厂类,顺序是Spi-->Spring
for (ExtensionFactory factory : factories) {
T extension = factory.getExtension(type, name);
if (extension != null) {
return extension;
}
}
return null;
}
5.Dubbo扩展点动态编译的实现
Dubbo SPI通过代码的动态生成,并配合动态编译器,灵活地在原始类基础上创建新的自适应类。Dubbo中有三种代码编译器,分别是JDK编译器(java原生),Javassist编译器,AdaptiveCompiler编译器。
dubbo动态编译的实现类图如下:
Compiler接口的定义如下:
public interface Compiler {
/**
* 编译java源码
* @param code java源码
* @param classLoader TODO
* @return Compiled class
*/
Class<?> compile(String code, ClassLoader classLoader);
}
而其实现类AdaptiveCompiler
分析
//设置默认的编译器名称
public static void setDefaultCompiler(String compiler) {
DEFAULT_COMPILER = compiler;
}
//通过ExtensionLoader获取对应的编译器扩展类实现,并调用真正的compile做编译
public Class<?> compile(String code, ClassLoader classLoader) {
...
return compiler.compile(code, classLoader);
}
AbstractCompiler的主要抽象逻辑如下:
通过正则匹配出包路径、类名,配出全路径类名
通过Class.forName加载该类返回,防止重复编译
调用doCompile方法进行编译
dubbo Javassist动态代码编译:
相关资料:https://juejin.im/entry/5aeb89e6518825671c0e6812
dubbo JDK动态代码编译
JdkCompiler是Dubbo编译器的另一种实现,使用了JDK自带的编译器。
END:资料选自
dubbo 官网
java spi文档
dubbo导读
以上是关于02.dubbo源码解析之Dubbo扩展点加载的主要内容,如果未能解决你的问题,请参考以下文章