Dubbo源码分析-SPI机制流程分析及核心架构图解析
从本篇开始就进去关于dubbo 的一系列分析文章,dubbo 本身的一些基础概念和用法在这个系列中不会去讲到,这些通过dubbo 的官网可以了解,平时工作时的使用方法官网也有详细描述。dubbo 系列的文章主要从dubbo 的核心架构 -> 核心机制SPI机制 -> provider 端的启动 -> consumer 端的启动 -> 服务调用全流程,这一套流程来详细分析。
本次就是主要简单说下dubbo 的核心架构还有详细走一遍spi 机制的实现原理,SPI 是dubbo 的核心,没有SPI 就相当于Spirng 没有IOC 和aop。
Dubbo的核心架构
dubbo 的相关我之前在CSDN 上面也写过一次,这次算是面试前的复习,温故而知新嘛。dubbo 相对于来说spring 要难一点,相对于myBatis 来说要难亿点,不过问题不大,下面正式发车。
说到dubbo 就一定要知道它的核心机制SPI,因为dubbo 整体设计是微内核架构,这个很重要。微内核架构的特点就是只有一套主流程,同时提供了一套插件查询机制。在主流程的每一个流程点上根据配置查找对应的插件来完成具体的工作,这样在每一个流程点上都可以完成自定制,为开发者提供了很大的便利,在dubbo 中插件也叫做扩展点。
同时一定要记住一幅图,dubbo 的核心架构图。这个是我从官网拉的,后面写了一次CSDN 的文章就有了水印,在官网这个图一定能找到,也一定要记住,面试就是问这个图。除了service 层一共是九层,分别是:
- config:配置信息层,对外配置接口,以
ServiceConfig
,ReferenceConfig
为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类; - proxy:服务代理层,服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以
ServiceProxy
为中心,扩展接口为ProxyFactory
; - registry:注册中心层,封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为
RegistryFactory
,Registry
,RegistryService
; - cluster:路由层,封装多个提供者的路由及负载均衡,并桥接注册中心,以
Invoker
为中心,扩展接口为Cluster
,Directory
,Router
,LoadBalance
; - monitor:监控层,RPC 调用次数和调用时间监控,以
Statistics
为中心,扩展接口为MonitorFactory
,Monitor
,MonitorService
; - protocol:远程调用层,封装 RPC 调用,以
Invocation
,Result
为中心,扩展接口为Protocol
,Invoker
,Exporter
; - exchange:信息交换层,封装请求响应模式,同步转异步,以
Request
,Response
为中心,扩展接口为Exchanger
,ExchangeChannel
,ExchangeClient
,ExchangeServer
; - transport:网络传输层,抽象 mina 和 netty 为统一接口,以
Message
为中心,扩展接口为Channel
,Transporter
,Client
,Server
,Codec
; - serialize:数据序列化层,可复用的一些工具,扩展接口为
Serialization
,ObjectInput
,ObjectOutput
,ThreadPool
。
SPI机制
关于SPI,java 中也是有SPI 机制的,Java SPI 实际上是:“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。其最大的弊端就是不能按需获取实现,如果获取想要的实现类只能全部迭代一遍然后判断。
dubbo 的SPI 是基础Java 的SPI 实现的,那么dubbo 这种微内核设计肯定不能是跟Java 的SPI 一样每次获取不同的实现都要循环迭代和判断,所以dubbo 的SPI 的写法加入了键值对的形式,通过键找不同的实现。下面看下dubbo具体是如何实现的。
首先我们写一个测试类、一个robot 接口,三个实现类都是实现robot 接口的。
1 | public class DubboSpiTest { |
extensionLoader对于dubbo的spi机制封装对象
然后我们首先要看的是ExtensionLoader.getExtensionLoader。ExtensionLoader 对象就是对于dubbo 的spi 机制的封装。
跟进代码。
首先就是判断入参接口不能为空,入参必须是接口类型的,入参接口必须被@spi 注解修饰。
1 | /** |
接着就是获取入参接口对应的extensionLoader 对象。
这里需要注意要先在缓存中判断是否已经存在接口对应的extensionLoader 对象,没有则去创建一个并放入缓存,有则直接获取返回。
1 | // 已加载的 Extension |
上面这一段简单来说就是为对应的接口生成一个extensionLoader 对象,并放入缓存,没有对这个对象进行进一步的填充属性。也就是相当于spring 中bean 对象的实例化,至于初始化并不在一起。
接着看就是getExtension 方法。跟进代码。
首先是通过getOrCreateHolder 方法获取一个Holder 对象,该对象就是对于接口实现类的一个封装,然后如果没有获取到对应对象,就会调用createExtension 方法进行创建,并再为Holder 对象进行一个赋值,这里有加双重锁,目的是就是为了保证对象只会被创建一次。下面我们再对Holder 对象、getOrCreateHolder 方法、createExtension 方法一个一个分析
1 | /** |
实现类对象的封装对象-Holder对象
代码中的Holder 对象其实就是对接口实现类进行了一层封装,并用volatile 关键字修饰,同时在下面的判断中加锁,保证对象不会重复创建,volatile + synchronized 就是为了加一个双重锁形成单例模式。这里提一句volatile 关键字视为了对象被创建时不会被重排序,具体的可以再找博客看下,这个比较简单。
1 | public class Holder<T> { |
从缓存中获取holder对象-getOrCreateHolder方法
再说getOrCreateHolder 方法,先跟进代码。
这段内容相对比较简单,目的就是从缓存中获取到具体的holder 对象,如果没有就实例化一个,并将其放入缓存。注意啊,到这里已经出现两个缓存了,都是ConcurrentHashMap 集合。
1 | // 缓存 所有实例key及对应的Holder |
第一步:构建解析具体实现类对象-createExtension方法
再说createExtension 方法,直接跟进代码。
首先这里会调用getExtensionClasses 方法获取缓存,并使用入参获取到对应的实现类对象。如果没有获取到对象表示入参没有对应的class 对象,直接抛出异常。
1 | // 根据key获取对应实例的Class |
从缓存中获取所有的实现类对象-getExtensionClasses方法
我们先跟进getExtensionClasses 方法,这里会出现第三个缓存,用于缓存所有的实现类对象,这不再是ConcurrentHashMap,而是holder 对象,并且这个对象封装的是一个map 集合。(注意:我们目前的所有操作都是在extensionLoader 对象里面,所有的缓存都是extensionLoader 的属性,也就意味着一个接口就是一个extensionLoader 对象,单例对象,接口对应的实现类就存放在这个extensionLoader 对象中的属性里面)
如果缓存中没有,那么就调用loadExtensionClasses 方法创建一个,注意这里还是双重锁,意味着这里同时这能被一个线程调用,也就是不会重复创建。
1 | // 缓存该接口type下的所有实例key及 实例对应的Class |
解析所有的实现类对象-loadExtensionClasses方法
接着看loadExtensionClasses 方法,跟进代码。
这里就是从不同的路径下获取所有于当前接口的相关文件,比如:DUBBO_DIRECTORY 常量就是META-INF/dubbo/ 文件目录下,至于type 就是在最开始构建extensionLoader 对象的时候进行的赋值。
1 | private static final String DUBBO_DIRECTORY = "META-INF/dubbo/"; |
上面这段内容其实没有必要深究,其目的就是为了解析所有目录下对象接口的文件,并将对应实现类的class 对象存入集合,比如我们目前解析的就是META-INF/dubbo/目录下的com.spi.dubbo.robot文件,并将其接口的三个实现类分别用key 值出入class 对象。
但是loadDirectory 方法里面会调用一个loadResource 方法,然后继续会调用loadClass 方法。这个方法就有必要看下。
真正构建接口对应实现类class对象的方法-loadClass方法
这里分三个内容,一是被Adaptive 修饰过,也就意味着这个地方要生成代理对象;二是:对象是wrapper 类型,也就是对象中存在接口类型的构造方法;最后一种就是普通的扩展点extensionClasses,这种就不用深究,只要知道返回的是正常的class 对象即可。
1 | if (clazz.isAnnotationPresent(Adaptive.class)) { |
我们现在一个一个看,先看Adaptive 注解修饰过的。跟进代码。
这里一目了然,就是为cachedAdaptiveClass 属性赋值,同时这个属性也被volatile 关键字修饰,看到这个关键字马上联想的应该就是双重锁,单例对象,而且一个接口只能有一个实现类被Adaptive 注解修饰,多了就会报错。至于这个有什么用,看后面的初始化填充属性的时候就能知道了。
1 | private volatile Class<?> cachedAdaptiveClass = null; |
接下来看cacheWrapperClass 方法,跟进代码。
依然的逻辑清晰,为cachedWrapperClasses 集合属性赋值,集合使用的是ConcurrentHashSet,set 的底层是map,其真正使用的就是map 的key 值,所以这里就存在唯一性,不可重复添加。至于这个集合是在createExtension 方法的最后一步使用,可以理解为spring 的aop 切点解析。
1 | // 缓存WrapperClasses |
第二步:实例出具体的实现类对象
然后这里回到createExtension 方法。继续跟代码。
这里出现了第四个缓存EXTENSION_INSTANCES,这里面存放是class 对应的实例对象,如果实例对象没有,那就直接通过反射构建一个。
1 | // 缓存所有的实例Class及对应的实例对象 |
第三步:向实例中注入其依赖的实例-injectExtension方法
这一步就是调用injectExtension 方法,其作用就是为实例对象添加属性,可以理解为IOC 的初始化过程中的添加属性操作。
首先第一步就是循环当前实现类对象的所有方法,判断当前方式是否是set 方法,并且set 方法只能有一个入参,然后过滤基本类型,因为这里只支持被@spi 注解修饰的接口注入。
1 | for (Method method : instance.getClass().getMethods()) { |
第二步获取set 方法的属性名称,也就是接口名称,然后调用objectFactory.getExtension,objectFactory 对象是在extensionLoade 对象构建的时候进行的赋值,本质是一个extensionFactory,相当于BeanFactroy一样。
1 | try { |
我们这里关注getExtension 方法,注意不是上面的getExtension 方法,这个是重载方法,上面的入参只有一个就是实现类对象key值,但是这里的入参是两个,前者是接口的class 对象,后者是接口名称。
还有一点extensionFactory 本身也是一个扩展点,被@spi 注解修饰,同时获取构建该对象的时候用的是getAdaptiveExtension 方法,也就是自适应扩展点,当前我们是spi 内容中调用的,所以最后上面的getExtension 方法会定位到SpiExtensionFactory 对象中。
1 |
|
继续跟进getExtension 方法的代码。
这里就是判断当前接口是否是扩展点,是否被@spi 注解修饰,然后还是调用getExtensionLoader 方法,这里是单个入参也就是文章最开始的调用,这里会获取到一个ExtensionLoader 对象,如果存在的话直接取我们上面说的第一个缓存EXTENSION_LOADERS 缓存,如果没有就创建一个。
然后接着是getSupportedExtensions 方法获取该扩展点的所有实现类,这里会去取我们说的第三个缓存cachedClasses 缓存,这里面存放是所有的实现类,如果该缓存中没有值,这里又会走到我们目前的步骤。如果存在的话,直接调用getAdaptiveExtension 方法获取一个自适应的对象返回。
1 |
|
生成自适应接口实现类的方法-getAdaptiveExtension方法
上面代码中我们要关注的是getAdaptiveExtension 方法,其余的都在上面讲过。getAdaptiveExtension 方法在后面dubbo 服务引用和服务导出的流程很常用,这个一定要记清楚。跟进代码。
我们一步一步看。首先是获取cachedAdaptiveInstance 属性中保存的自适应对象,也就是我们上面loadClass 方法解析封装,同时这也是我们自己编写的自适应对象。
1 | Object instance = cachedAdaptiveInstance.get(); |
但是如果我们没有自己的自适应对象呢,这里继续就是双重锁,保证对象不被重复创建。
1 | if (instance == null) { |
接着就是如果不存在自己的自适应对象,那么就调用createAdaptiveExtension 方法生成一个,同时还会存入extensionLoader 对象的属性中。
1 | try { |
继续跟进createAdaptiveExtension 方法代码。
这里就是继续调用getAdaptiveExtensionClass 方法得到class 对象,然后直接调用newInstance 方法得到对应的object,同时还会再次调用injectExtension 方法,也就是我们上面第三步使用的方法,避免自适应对象中存在扩展点的set 注入。
1 | // getAdaptiveExtensionClass()是核心 |
继续跟进getAdaptiveExtensionClass 方法。
这里又会获取一次当前扩展点所有实现,并判断是否自己实现了自适应实现对象,如果实现了就直接返回,否则继续调用createAdaptiveExtensionClass 方法,生成默认的自适应实现对象。
1 | getExtensionClasses(); |
真正生成代理对象的方法-createAdaptiveExtensionClass方法
先说这个方法的目的吧,就是使用javassist 生成一个class 文件,然后得到class 对象,具体的不用管那么多,只要知道一下这段代码是怎么跟到的就行。
1 | private Class<?> createAdaptiveExtensionClass() { |
第四步:装配wrapper
再次回到createExtension 方法,我们走到了最后一步,装配wrapper,wrapper 其实就是相当于spring 中的aop,本身就是对接口的增强。在上面的loadClass 方法中,我们已经将wrapper 对象,也就是对象存在对接口的构造方法的对象,存入了cachedWrapperClasses 集合中,这里要获取所有的wrapper 对象的话,也是循环这个集合。
下面我们看下具体代码。
首先就是获取到所有wrapper 对象,然后一个一个的封装,最好的理解就是套娃,将当前对象封装到下一个wrapper 对象中,通过构造注入即可。
最后再将当前实现类返回,然后一层一层的存入相应的缓存。
1 | /** |
小结
到这里dubbo 的spi 就已经结束了,总结一下:
- 先是获取一个extensionLoader 对象,并存入第一个缓存,
- 然后使用getExtension 方法获取对应key 值的实现类对象,这里就有了第二个缓存,
- 如果没有实现类在缓存中,就会走到createExtension 方法去创建对应的实现类,注意这里是解析接口所有的实现类,
- 这方法中第一步是获取到解析配置信息,获取所有对象的class 对象,同时会存入就有了第三个缓存,
- 然后将key 值对应的class 对象进行实例,这里会有第四个缓存,
- 然后对实例对象的set 方法进行注入,这里只注入扩展点也就是被@spi 修饰的接口,注入的内容都是自适应对象,如果自己编写了自适应对象,也就是有实现类被@Adaptive 注解修饰,那么会注入该对象,反之会注入由javassist 生成的自适应对象。
- 最后这个实现类对象,如果接口存在wrapper 对象的话,还会被wrapper 对象用构造注入的方式封装,最后的实现类返回。
spi 中的四个缓存明细:
- 第一个缓存中存放的就是接口对应该的extensionLoader 对象;
- 第二个缓存中存放的是接口的不同key 对应实现类对象的封装holder 对象;
- 第三个缓存中存放的是key 值对应的class 对象,注意这里不是实例对象,而是class 对象,还有这里不再直接存入集合,而是由holder 对象封装了map 集合;
- 第四个缓存中存放的是key 值对应的实例对象。
总结
本次真正要说的其实就是spi,上面的小结也能完整的把spi 的流程详细的说出来,至于dubbo 的那幅核心架构图,说实话我看了不知道多少遍,后面的所有dubbo 系列的文章我都会在开头贴出来,如果说spi 是dubbo 的基础,那么核心架构图就是基础中的基础,它是dubbo 的主流程,本次只要记住架构图中的九层分别是什么就行,后面会一个一个解析。本次就到这,后面继续。
附录Dubbo 源码分析系列文章
时间 | 文章 |
---|---|
2022-02-20 | Dubbo源码分析-SPI机制流程分析及核心架构图解析 |