vlambda博客
学习文章列表

《二》Dubbo核心之SPI机制

2.1 先了解Java SPI

Java SPI 起初是提供给厂商做插件开发的,使用了策略模式,一个接口多种实现,只声明接口,具体的实现不在程序中直接确定,由配置掌控。

1.定义接口和方法 。

2.编写接口的实现类。

3.在META/INF/services/目录下创建以接口全路径命名的文件。

4.文件内容为接口的实现类全路径,如果有多个,分行隔离。

5.代码中通过java.util.ServiceLoader来加载具体的实现类。

Java SPI缺点:

  1. JDK 标准的 SPI 会一次性加载实例化扩展点的所有实现,什么意思呢?就是如果你在 META-INF/service 下的文件里面加了 N 个实现类,那么 JDK 启动的时候都会一次性全部加载。那么如果有的扩展点实现初始化很耗时或者如果有些实现类并没有用到, 那么会很浪费资源

  2. 如果扩展点加载失败,会导致调用方报错,可能会因为各种原因导致异常信息被“吞掉”,而且这个错误很难定位到是这个原因 Dubbo 优化后的 SPI 机制

2.2 Dubbo SPI

Dubbo SPI做了一定的改进和优化。

①.Dubbo 的 SPI 扩展机制,有两个规则
  1. 需要在 resource 目录下配置 META-INF/dubbo 或者 META-INF/dubbo/internal 或者 META-INF/services,并基于 SPI 接口去 创建一个文件

  2. 文件名称和接口名称保持一致,文件内容和 SPI 有差异,内容是 KEY 对应 Value。

ps : Dubbo 针对的扩展点非常多,可以针对协议、拦截、集群、路由、负载均衡、序列化、容器... 几乎里面用到的所有功能,都可 以实现自己的扩展,我觉得这个是 dubbo 比较强大的一点。

3.增加了对扩展IOC和AOP的支持,一个扩展可以直接通过setter注入其他扩展。Dubbo SPI只是加载配置文件中的类,并分成不同种类缓存在内存中,而不是立即全部初始化,在性能上更优秀。

4.Dubbo SPI在扩展加载失败的时候会先抛出真实异常并打印日志,扩展点加载失败的时候不会影响其他扩展点和整个框架的使用。

5.dubbo启动时会默认扫描三个目录下的文件:META-INF/services/ 、META-INF/dubbo/、META-INF/dubbo/internal/

名称为全路径类名,内容格式为 key =value形式

zookeeper =org.apache.dubbo.ZookeeperDynamicConfigurationFactory

②.扩展点的分类和缓存

DubboSPI 分为Class缓存和实例缓存,两种缓存根据扩展类分为普通扩展类、包装Wrapper扩展类、自适应扩展类(Adaptive)等。

  • Class缓存:DubboSPI获取扩展类时,会先从缓存中读取,如不存在则加载配置文件。根据配置把Class缓存到内存中,并不会直接全部初始化。

  • 实例缓存:基于性能考虑也会缓存Class实例化后的对象,按需实例化再进行缓存DubboSPI

2.1普通扩展类:最基础的配置在SPI配置文件中的扩展类实现
2.2自动包装扩展类 :这种Wrapper无具体实现,只是做了同样逻辑的抽象,需要在构造方法中传入具体的扩展接口。
2.3自适应扩展类 :在运行时,通过传入URL中的参数动态来确定,属于扩展点的自适应特性(@Adaptive)
2.4自动激活扩扩展类 :@Activate
三.核心实现类ExtensionLoader源码分析
Clazz clazz = ExtensionLoader.getExtensionLoader(Clazz.class).getExtension
 //1. 通过 getExtensionClasses 会查找指定目录获取所有的拓展类保存到map-> getExtensionClasses方法
//2. 通过反射创建拓展对象
//3. 向拓展对象中注入依赖
//4. 将拓展对象包裹在相应的 Wrapper 对象中   -> injectExtension方法注入依赖(通过反射调用 setter 方法设置依赖)
//以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。

1.ExtensionLoader.getExtensionLoader

该方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例

2.ExtensionLoader.getExtension

该方法主要获取拓展类对象.

3.getExtension #( #createExtension  ->#getExtensionClasses  ->#injectExtension)

 //1. 通过 getExtensionClasses 会查找指定目录获取所有的拓展类保存到map-> getExtensionClasses方法
//2. 通过反射创建拓展对象
//3. 向拓展对象中注入依赖
//4. 将拓展对象包裹在相应的 Wrapper 对象中   -> injectExtension方法注入依赖(通过反射调用 setter 方法设置依赖)
//以上步骤中,第一个步骤是加载拓展类的关键,第三和第四个步骤是 Dubbo IOC 与 AOP 的具体实现。

getExtensionClasses :1.从缓存中获取已加载的拓展类 2.加载拓展类loadExtensionClasses()3.调用loadDirectory方法加载指定文件夹配置文件到HashMap中。

四.扩展点注解
4.1 静态扩展点注解@SPI
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
String value() default "";
}

可以使用在类、接口和枚举上,Dubbo都是使用在接口上,通过传入不同的value来设置接口的默认实现比较静态化,也成为静态扩展点。

例如Transporter接口上标注netty,所以采用默认netty实现

@SPI("netty")
public interface Transporter {}

4.2自适应扩展点注解@Adaptive

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
 //数组可以设置多个Key
   String[] value() default {};
}
@Adaptive
public class AdaptiveExtensionFactory implements ExtensionFactory {}
@Adaptive
public class AdaptiveCompiler implements Compiler {}
@SPI("netty")
public interface Transporter {
   @Adaptive({Constants.SERVER_KEY, Constants.TRANSPORTER_KEY})
     //在运行时,通过传入URL中的参数动态来确定,属于扩展点的自适应特性
    //传入的两个参数(server,url)来进行实现类的进行匹配真正的实现类
   Server bind(URL url, ChannelHandler handler) throws RemotingException;
   @Adaptive({Constants.CLIENT_KEY, Constants.TRANSPORTER_KEY})
   Client connect(URL url, ChannelHandler handler) throws RemotingException;

}
@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;
}

@Adaptive标注在方法上,Dubbo初始化扩展点时,会生成一个Transporter$Adaptive类,里面会实现这两个方法,方法里面有一些通用的逻辑,通过@Adaptive传入的两个参数(server,url)来进行实现类的进行匹配真正的实现类。

4.2.1 @Adaptive实现源码

1.如果 cachedAdaptiveClass 不存在,dubbo 会动态生成一个代理类 Protocol$Adaptive. 前面的名字 protocol 是根据当前 ExtensionLoader 所加载的扩展点来定义的 。

2.createAdaptiveExtensionClass 

动态生成字节码,然后进行动态加载。那么这个时候锁返回的 class,如果加载的是 Protocol.class,应该是 Protocol$Adaptive 这个 cachedDefaultName 实际上就是扩展点接口的@SPI 注解对应的名字,如果此时加载的是 Protocol.class,那么 cachedDefaultName=dubbo

3.注意:@Adaptive 加载到类上表示这是一个自定义的适配器类,表示我们再调用 getAdaptiveExtension 方法的时候不需要走上面这么复杂的过程。会直接加载到 AdaptiveExtensionFactory。然后在getAdaptiveExtensionClass()方法处判断 。


4.3自动激活扩展点注解 @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;
}

可以使用在类、接口和枚举和方法上,主要使用在有多个扩展点的实现、需要根据不同条件激活的场景中,如Filter需要多个同时激活,因为每个Filter的实现都是不同的功能

1.自动激活扩展点,有点类似我们讲 springboot 的时候用到的 conditional,根据条件进行自动激活。但是这里设计的初衷是,对于一个类会加载多个扩展点的实现,这个时候可以通过自动激活扩展点进行动态加载, 从而简化配置我们的配置工作 @Activate 提供了一些配置来允许我们配置加载条件,比如 group 过滤,比如 key 过滤。

2.我们可以看看 org.apache.dubbo.Filter 这个类,它有非常多的实现,比如说 CacheFilter,这个缓存过滤器,配置信息 如下 group 表示客户端和和服务端都会加载,value 表示 url 中有 cache_key 的时候

@Activate(group = {CONSUMER, PROVIDER}, value = CACHE_KEY)
public class CacheFilter implements Filter {}

下一篇将详细分析ExtensionLoader类源码的实现