Dubbo-SPI

568 阅读10分钟
SPI的全名为Service Provider Interface,是JDK内置的一种服务提供发现机制。简单来说,它就是一种动态替换发现服务实现者的机制。为了实现在模块装配的时候不在程序里动态指明,这就需要一种服务发现机制。java的SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。究其思想和"Callback"差不多,“Callback”的思想是在我们调用API的时候,我们可以自己写一段逻辑代码,传入到API里面,API内部在合适的时候回调用它,从而实现某种程度的“定制”。

系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案、xml解析模块、jdbc模块的方案等。面向对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。很多SPI的接口是Java核心库的一部分,是由Bootstrap ClassLoader来加载的,而SPI实现的Java类一般是由系统类加载器来加载的。Bootstrap ClassLoader是无法找到 SPI 的实现类的,因为它只加载 Java 的核心库,它也不能代理给系统类加载器,因为它是系统类加载器的祖先类加载器。也就是说类加载器的代理模式无法解决这个问题。线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。SPI是针对厂商或者插件的,jdbc4.0以前,开发人员还需要基于Class.forName("xxx")的方式来装载驱动,jdbc4也基于spi的机制来发现驱动提供商了,可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者。


java的SPI的具体约定如下:当服务的提供者,提供了服务接口的一种实现之后,在jar包的
META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名, 并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要在代码里指定。 jdk提供服务实现查找的一个工具类java.util.ServiceLoader。

ServiceLoader以延迟方式查找和实例化提供者,也就是说根据需要进行。服务加载器维护到目前为止已经加载的提供者缓存。每次调用 iterator 方法返回一个迭代器,它首先按照实例化顺序生成缓存的所有元素,然后以延迟方式查找和实例化所有剩余的提供者,依次将每个提供者添加到缓存。可以通过 reload 方法清除缓存。

ServiceLoader:一个简单的服务提供者加载设施。ServiceLoader也像ClassLoader一样,能装载类文件,但是使用时有区别,具体区别如下:

  •  ServiceLoader装载的是一系列有某种共同特征的实现类,而ClassLoader是个万能加载器;
  • ServiceLoader装载时需要特殊的配置,使用时也与ClassLoader有所区别;
  • ServiceLoader还实现了Iterable接口

Dubbo的扩展点加载从JDK标准的SPI扩展点发现机制加强而来。Dubbo改进了JDK标准的SPI的以下问题:

  • JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。
  • 增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。

  • Dubbo-SPI的约定
在扩展类的jar包内,放置扩展点配置文件META-INF/dubbo/接口全限定名,内容为配置名=扩展实现类全限定名,多个实现类用换行符分隔。(注意:这里的配置文件是放在你自己的jar包内,不是dubbo本身的jar包内,Dubbo会扫描ClassPath所有jar包内同名的这个文件,然后进行合并)


  • 扩展Dubbo协议的示例
在协议的实现jar包内放置文本文件:META-INF/dubbo/com.alibaba.dubbo.rpc.Protocol,内容为:xxx=com.alibaba.xxx.XxxProtocol
注意: 扩展点使用单一实例加载(请确保扩展实现的线程安全性),CacheExtensionLoader中。
扩展点自动包装:
自动Wrap扩展点的Wrapper类:ExtensionLoader会把加载扩展点时(通过扩展点配置文件中内容),如果该实现有拷贝构造函数,则判定为扩展点Wrapper类。Wrapper同样实现了扩展点接口。Wrapper不是扩展点实现,用于从ExtensionLoader返回扩展点时,Wrap在扩展点实现外。即从ExtensionLoader中返回的实际上是Wrapper类的实例,Wrapper持有了实际的扩展点实现类。扩展点的Wrapper类可以有多个,也可以根据需要新增。通过Wrapper类可以把所有扩展点公共逻辑移至Wrapper中。新加的Wrapper在所有的扩展点上添加了逻辑,有些类似AOP(Wraper代理了扩展点)。
扩展点自动装配:
加载扩展点时,自动注入依赖的扩展点。扩展点实现类的成员如果为其他扩展点类型,ExtensionLoader在会自动注入依赖的扩展点。ExtensionLoader通过扫描扩展点实现类的所有set方法来判断其成员,即ExtensionLoader会执行扩展点的拼装操作。
扩展点自适应:
扩展点的Adaptive实例ExtensionLoader注入的依赖扩展点是一个Adaptive实例,
直到扩展点方法执行时才决定调用是一个扩展点实现。Dubbo使用URL对象(包含了Key-Value)传递配置信息。扩展点方法调用会有URL参数。这样依赖的扩展点也可以从URL拿到配置信息,所有的扩展点自己定好配置的Key后,配置信息从URL上从最外层传入。URL在配置传递上即是一条总线。Adaptive实例的逻辑是固定,指定提取的URL的Key,即可以代理真正的实现类上,可以动态生成。在Dubbo的ExtensionLoader的扩展点类开对应的Adaptive实现是在加载扩展点里动态生成。指定提取的URL的Key通过@Adaptive注解在接口方法上提供。
下面是Dubbo的Transporter扩展点的代码:
public interface Transporter {
@Adaptive({"server", "transport"})
Server bind(URL url, ChannelHandler handler) throws RemotingException;
@Adaptive({"client", "transport"})
Client connect(URL url, ChannelHandler handler) throws RemotingException;
}


Dubbo配置模块中,扩展点均有对应配置属性或标签,通过配置指定使用哪个扩展实现扩展点自动激活:对于集合类扩展点,可以同时加载多个实现,此时,可以用自动激活来简化配置

Dubbo的扩展点加载从JDK标准的SPI(Service Provider Interface)
扩展点发现机制加强而来,基本上Dubbo的所有东西都是在扩展点的基础上实现的。Dubbo扩展点的核心是Extension Loader,这个类有点类似ClassLoader,ExtensionLoader是加载Dubbo的扩展点的。ExtensionLoader是Dubbo中一个非常重要的类,刚接触Dubbo源码的人看这个类的时候也多少会有点困惑,这个类非常重要,它就像是厨房里的“大厨”,按照用户的随时需要把各种“食材”烹调出来。
ExtensionLoader的属性结构:
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
EXTENSION_LOADERS常量用来加载dubbo的所有扩展点的ExtensionLoader,在Dubbo中,每种类型的扩展点都会有一个与其对应的ExtensionLoader,类似jvm中每个Class都会有一个ClassLoader,每个ExtensionLoader会包含多个该扩展点的实现,类似一个ClassLoader可以加载多个具体的类,但是不同的ExtensionLoader之间是隔离的,这点也和ClassLoader类似。
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
EXTENSION_INSTANCES是一个具体扩展类的实体,用于缓存,防止扩展点比较重,导致浪费没必要的资源,所以在实现扩展点的时候,一定要确保扩展点可单例化,否则可能会出现问题。
private final Class<?> type;
type一般是接口,用于制定扩展点的类型,因为dubbo的扩展点申明是SPI的方式,所以某一个类型扩展点,就需要申明一个扩展点接口。
我们结合具体代码详细说一下ExtensionLoader的实现,下面是ServiceConfig类里的一行代码:
private static final Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
上面代码的程序流程图如下所示(假定是第一次执行这行代码):


在这个过程中最重要的两个方法是getExtensionClasses和createAdaptiveExtensionClass(图中红色部分),下面详细对这两个方法进行分析:
1)getExtensionClasses
这个方法主要读取META-INF/services/目录下对应文件内容,在本示例代码中,是读取META-INF/services/com.alibaba.dubbo.rpc.Protocol文件中的内容,具体内容如下:
com.alibaba.dubbo.registry.support.RegistryProtocol
com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
它分析该文件中的每一行(每一行对应一个类),分析这些类,如果发现有哪个类的Annotation是@Adaptive,则找到对应的AdaptiveClass了,但由于Protocol文件里没有哪个类的Annotation是@Adaptive,所以在这个例子中该方法没找到对应的AdaptiveClass。
2)createAdaptiveExtensionClass
该方法是在getExtensionClasses方法找不到AdaptiveClass的情况下被调用,
该方法主要是通过字节码的方式在内存中新生成一个类
,它具有AdaptiveClass的功能,Protocol就是通过这种方式获得AdaptiveClass类的。
AdaptiveClass类的作用是能在运行时动态判断具体是要调用哪个类的方法,更多关于AdaptiveClass的内容请参考Dubbo官方文档。


扩展点的加载

1.自适应扩展点
ExtensionLoader加载扩展点时,会检查扩展点的属性(通过set方法判断),如该属性是扩展点类型,则会注入扩展点对象。
因为注入时不能确定使用哪个扩展点(在使用时确定),所以注入的是一个自适应扩展(一个代理)。自适应扩展点调用时,选取一个真正的扩展点,并代理到其上完成调用
。Dubbo是根据调用方法参数(上面有调用哪个扩展点的信息)来选取一个真正的扩展点。
在Dubbo给定所有的扩展点上调用都有URL参数(整个扩展点网的上下文信息)。自适应扩展即是从URL确定要调用那个扩展点实现,URL哪个Key的Value用来确定使用哪个扩展点,这个信息通过的@Adaptive注解在方法上说明。

dubbo的扩展点加载机制主要的实现类便是ExtensionLoader,从功能上来看,扩展点分为加载和获取两个部分,这里涉及到了各个扩展点的注入和实例化,以及对所有扩展点的管理,dubbo把扩展点分为了三类:
可自动激活的扩展点(Activate)
自适应的扩展点(Adaptive)
普通的扩展点

其实Activate的扩展点和Adaptive的扩展点就是对普通的扩展点进行了一次包装。dubbo提供了三个函数去获得这三种扩展点,分别是:
getAdaptiveExtension
getActivateExtension
getExtension