前言
Dubbo中的SPI机制,全称为 Service Provider Interface,是一种服务发现机制。是提供给扩展者使用的一种机制,比方说可以载入Dubbo中的各种可配置组件,比如:动态代理方式(ProxyFactory)、负载均衡策略(LoadBalance)、RCP协议(Protocol)、拦截器(Filter)、容器类型(Container)、集群方式(Cluster)和注册中心类型(RegistryFactory)等,增强了JDK 的SPI,使得其在最大程度的解耦。比较类似于Spring中IoC的思想。关于Spring的IoC的具体分析留到后面学习Spring源码的时候再做分析,这边主要看的是Dubbo中SPI机制的实现。另外,我们知道jdk本身就有SPI,那么Dubbo中对SPI是如何实现的。另外补充一点,SPI是一种破坏双亲委派机制的做法,关于双亲委派机制可以看本篇第四节的部分,所以说一种机制的出现肯定有好也有坏,关键看的是在何种情况下的使用。
1、两种SPI的简单用法
先来简单的看一下jdk的spi与dubbo中的spi如何使用,这边引用官方的例子。
jdk SPI
首先是一个接口和俩个实现类
1 | public interface Robot { |
1 | public class OptimusPrime implements Robot { |
接下来 META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 org.apache.spi.Robot。文件内容为实现类的全限定的类名,如下:
1 | org.apache.spi.OptimusPrime |
测试
1 | public class JavaSPITest { |
Java SPI
Hello, I am Optimus Prime.
Hello, I am Bumblebee.
Dubbo SPI
同样的,Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下,是通过一种键值对的方式进行配置的。另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。
1 | optimusPrime = org.apache.spi.OptimusPrime |
测试
1 | public class DubboSPITest { |
Dubbo SPI
Hello, I am Optimus Prime.
Hello, I am Bumblebee.
对比两种SPI,给出一个比较官方的对比,Dubbo比jdk的SPI优越之处在于
JDK标准的SPI会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK标准的ScriptEngine,通过getName();获取脚本类型的名称,但如果RubyScriptEngine因为所依赖的jruby.jar不存在,导致RubyScriptEngine类加载失败,这个失败原因被吃掉了,和ruby对应不起来,当用户执行ruby脚本时,会报不支持ruby,而不是真正失败的原因。
增加了对扩展点IoC和AOP的支持,一个扩展点可以直接setter注入其它扩展点。
总结一下就是两点:提高了效率和性能节约资源、增加了功能。
2、从源码层面进行分析
jdk SPI
java原生的SPI的类存在于java.util.ServiceLoader 目录下,从ServiceLoader.load(Robot.class);进入进行分析。
在此之前,要先知道一个ServiceLoader类下关于位置的静态变量PREFIX,这边可以看到已经是写死的一个位置,也就是只能加载该目录下的文件。
1 | private static final String PREFIX = "META-INF/services/"; |
1 | // 调用load方法 |
1 | private ServiceLoader(Class<S> svc, ClassLoader cl) { |
1 | private class LazyIterator |
总结一下,jdk的spi虽然也使用了延时加载,将服务加载的这个动作延迟到使用服务的时候,但是由于LazyIterator类实现了Iterator接口,所以在使用的时候只能通过遍历来全部获取,也就是接口的实现类全部加载并实例化一遍(实例化是在nextService()函数中出现的)。如果有些实现类你是不需要的,但是仍然会被实例化,这就会造成浪费。而且迭代器并不能直接通过某个参数来直接获取对应的实现类。这便是jdk的缺点所在。
Dubbo SPI
ExtensionLoader的源码位于 com.alibaba.dubbo.common.extension 包下,详细分析以上测试代码的几句话在源码层面到底干了啥。
首先,获取ExtensionLoader实例:ExtensionLoader.getExtensionLoader(Robot.class);
1 | private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>(); |
ExtensionLoader在这边类似于工厂模式,提供了私有的构造器,其入参type为扩展接口类型。Dubbo通过SPI注解定义了可扩展的接口,如Filter、Transporter等。每个类型的扩展对应一个ExtensionLoader。SPI的value参数决定了默认的扩展实现。
比如,以负载均衡为例。

文件下配置了所有负载均衡的方式,也就是之前博客中提到的几种负载均衡机制。
1 | random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance |
加载方式也就是 ExtensionLoader.getExtensionLoader(LoadBalance.class)
接下去,可以通过extensionLoader.getExtension("optimusPrime");获取到你需要的类。
1 | public T getExtension(String name) { |
这边判断是否有实例instance的时候,用到了double check,既保证效率也能保证线程的安全性,是一个亮点。
getExtensionClasses()方法
1 | private Map<String, Class<?>> getExtensionClasses() { |
loadDirectory()函数
1 | private void loadDirectory(Map<String, Class<?>> extensionClasses, String dir) { |
整理一下整个流程
0、首先初始化extensionLoader,也就是ExtensionLoader.getExtensionLoader(Robot.class);这步做的事情。
1、想要获取到对应的名称的类,调用extensionLoader.getExtension("optimusPrime")方法。
具体步骤
1.1 先从缓存cachedInstances中取相应的扩展实现类实例,如果没有则new一个,并放入缓存中。
1.2 用double check的方法判断是否已经获得了实例,这边保证了线程的安全性。如果没有则创建相应的扩展实现类实例:
instance = createExtension(name)。
2、进入到createExtension(name)函数中。
具体步骤
2.1 获得文件中对应的类路径下的名称,具体的方法是
Class<?> clazz = getExtensionClasses().get(name)2.2 从EXTENSION_INSTANCES中取得对应的instance并返回,如果没有,则通过反射newInstance()方法生成一个对象,当然这个对象就是指定name的对象。然后保存在EXTENSION_INSTANCES这个map中,方便下次使用,而不用重新创建了。
3、再进入到getExtensionClasses().get(name)中看如何根据name获得到对应的配置文件。(如name是某个具体负载均衡方法的名字)
具体步骤
3.1 判断缓存cachedClasses中有没有加载过。这里同样的也是用了double check。
3.2 如果没有加载过,也就是缓存中不存在,则调用
loadExtensionClasses()方法加载文件。
4、进入到loadExtensionClasses()方法中。
具体步骤
4.1 里面主要是加载了三个位置的文件,并返回。其中加载文件的方法是
loadDirectory(Map<String, Class<?>> extensionClasses, String dir)
5、进入到loadDirectory()中。
具体步骤
5.1 主要是通过
loadResource(extensionClasses, classLoader, resourceURL)方法,该方法中主要是通过Class.forName方法进行类加载。
6、类加载的时候,有以下几步
6.1 处理Adaptive注解,若存在则将该实现类保存至cachedAdaptiveClass属性
6.2 尝试获取参数类型为当前扩展类型的构造器方法,若成功则表明存在该扩展的封装类型,将封装类型存入wrappers集合
6.3 处理active注解,将扩展名对应active注解存入cachedActivates
至此,就很容易解释为什么dubbo能通过这种key、value的形式获取一个指定实现类的对象,而不像jdk spi那样一次性加载所有类的对象。主要的原因就是它是将所有类加载保存在一个map中,通过指定的key去获取到对应的class,在获取到之后才会对这个key的class进行反射,创建出一个对象,而不是和jdk spi一样,一下子将所有对象都创建出来。
3、仿Dubbo SPI机制
具体的流程我仿照dubbo spi写了个demo,有利于加深理解,可以访问该地址参考。
参考:
http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html