vlambda博客
学习文章列表

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();
}

接下来定义两个实现类,分别为DemoServiceImplADemoServiceImplB

public class DemoServiceImplA implements DemoService { @Override public void sayHello() { System.out.println(">>>> THIS IS A!"); }}
public class DemoServiceImplB implements DemoService { @Override 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接口,即是一个扩展点,运行是可以找到具体的实现类

@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public @interface SPI { /**实现key的名称**/  String value() default "";}

例子:org.apache.dubbo.registry.client.ServiceDiscovery 使用@SPI("zookeeper") Dubbo中很多地方通过SpiExtensionFactory.

@Adaptive

  • 作用:标记在类(AdaptiveCompiler)、接口(AdaptiveExtensionFactory)、枚举类和方法上,方法级别注解在第一次getExtension时,会自动生成和编译一个动态的Adaptive类,从而达到动态实现类的效果。

@Activate

  • 作用:可以标记在类、接口、枚举类和方法上。有两种注解方式:一种是注解在类上,一种是注解在方法上

  1. 注解在类上,而且是注解在实现类上,目前dubbo只有AdaptiveCompiler和AdaptiveExtensionFactory类上标注了此注解,这是些特殊的类,ExtensionLoader需要依赖他们工作,所以得使用此方式。

  2. 注解在方法上,注解在接口的方法上,除了上面两个类之外,所有的都是注解在方法上。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步:

  1. 读取SPI对应路径下的配置文件,首先尝试从本地缓存cachedInstances中获取,如果获取不到,那么调用createExtension(name)创建扩展类实例并本地缓存

  2. 传入name初始化对应的扩展类

  3. 查找符合条件的包装类

  4. 返回对应的扩展类对象

源码分析:

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步骤:

  1. 生成package、import、类名称等头部信息。

  2. 遍历接口所有方法

  3. 生成参数为空校验代码

  4. 生成默认实现类名称

  5. 生成获取扩展点名称的代码

  6. 生成获取具体扩展实现类代码

  7. 生成调用结果代码

配置文件:

# 位置:dubbo-common\src\main\resources\META-INF\dubbo\internal\com.fanweiwei.spi.adaptive.AdaptiveExt2# Comment 1dubbo=com.fanweiwei.spi.adaptive.DubboAdaptiveExt2cloud=com.fanweiwei.spi.adaptive.SpringCloudAdaptiveExt2thrift=com.fanweiwei.spi.adaptive.ThriftAdaptiveExt2

接口代码:

//@SPI
@SPI("dubbo")
public interface AdaptiveExt2 {

   @Adaptive
   String echo(String msg, URL url);
}

实现类:

public class DubboAdaptiveExt2 implements AdaptiveExt2 { @Override public String echo(String msg, URL url) { return "dubbo"; }}
public class SpringCloudAdaptiveExt2 implements AdaptiveExt2 { @Override public String echo(String msg, URL url) { return "spring cloud"; }}
public class ThriftAdaptiveExt2 implements AdaptiveExt2 { @Override public String echo(String msg, URL url) { return "thrift"; }}

Test 代码:

 /** * @SPI("dubbo") 如果在spi中没有体现,那么需要在url中有所体现 * 输出:spring cloud */ @Test 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 */ @Test 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 */ @Test 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)); }
  • 分析:

从上面的几个测试用例,可以得到下面的结论:

  1. 在类上加上@Adaptive注解的类,是最为明确的创建对应类型Adaptive类。所以他优先级最高。

  2. @SPI注解中的value是默认值,如果通过URL获取不到关于取哪个类作为Adaptive类的话,就使用这个默认值,当然如果URL中可以获取到,就用URL中的。

  3. 可以再方法上增加@Adaptive注解,注解中的value与链接中的参数的key一致,链接中的key对应的value就是spi中的name,获取相应的实现类。

源码分析:https://www.jianshu.com/p/dc616814ce98

3.getActivateExtension介绍

对应是@Activate注解,其主要分为4个步骤:

  1. 检查缓存,如果缓存中没有,则初始化所有扩展类实现的集合

  2. 遍历整个@Activate注解集合,根据传入URL匹配,得到所有集合符合条件的扩展类的实现

  3. 遍历所有用户自定义扩展类名称,根据用户URL配置的顺序,调整扩展点激活顺序

  4. 返回所有自动激活类集合

配置文件

#位置:~\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

接口代码

@SPIpublic 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()); }
  • 结论:从上面的几个测试用例,可以得到下面的结论:

    1. 根据loader.getActivateExtension中的group和搜索到此类型的实例进行比较,如果group能匹配到,就是我们选择的,也就是在此条件下需要激活的。

    2. @Activate中的value是参数是第二层过滤参数(第一层是通过group),在group校验通过的前提下,如果URL中的参数(k)与值(v)中的参数名同@Activate中的value值一致或者包含,那么才会被选中。相当于加入了value后,条件更为苛刻点,需要URL中有此参数并且,参数必须有值。

    3. @Activate的order参数对于同一个类型的多个扩展来说,order值越小,优先级越高。

4.ExtensionFactory的实现原理

由上面介绍个方法,终究一点都是同个ExtensionLoader类开始的,它是整个SPI的核心,下面我们将要简单分析以下ExtensionLoader。ExtensionLoader类本身通过工厂方法ExtensionFactory创建的,并且这个工厂类上也有SPI注解,如图:

/*** ExtensionFactory* 扩展点工厂,加载接口的实现类。这个接口包括Dubbo中的SPI接口和一般类型的接口*/@SPIpublic 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接口有三个实现类:SpiExtensionFactorySpringExtensionFactoryAdaptiveExtensionFactory

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上下文由究竟是什么时候保存起来,这个主要由ReferenceBeanServiceBean中调用方法保存上下文,后面如有时间会具体分析

② 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接口的定义如下:

@SPI("javassist")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导读