Dubbo源码解析之SPI(一):扩展类的加载过程
Dubbo是一款开源的、高性能且轻量级的Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用、智能容错和负载均衡,以及服务自动注册和发现。
Dubbo最早是阿里公司内部的RPC框架,于 2011 年开源,之后迅速成为国内该类开源项目的佼佼者,2018年2月,通过投票正式成为 Apache基金会孵化项目。目前宜信公司内部也有不少项目在使用Dubbo。
本系列文章通过拆解Dubbo源码,帮助大家了解Dubbo,做到知其然,并且知其所以然。
一、JDK SPI
1.1 什么是SPI?
SPI(Service Provider Interface),即服务提供方接口,是JDK内置的一种服务提供机制。在写程序的时候,一般都推荐面向接口编程,这样做的好处是:降低了程序的耦合性,有利于程序的扩展。
SPI也秉承这种理念,提供了统一的服务接口,服务提供商可以各自提供自己的具体实现。大家都熟知的JDBC中用的就是基于这种机制来发现驱动提供商,不管是Oracle也好,MySQL也罢,在编写代码时都一样,只不过引用的jar包不同而已。后来这种理念也被运用于各种架构之中,比如Dubbo、Eleasticsearch。
1.2 JDK SPI的小栗子
SPI 的实现方式是将接口实现类的全限定名配置在文件中,由服务加载器读取配置文件,加载实现类。
了解了概念后,来看一个具体的例子。
1)定义一个接口
public interface Operation {
int operate(int num1, int num2);
}
2)写两个简单的实现
public class DivisionOperation implements Operation {
public int operate(int num1, int num2) {
System.out.println("run division operation");
return num1/num2;
}
}
3)添加一个配置文件
在ClassPath路径下添加一个配置文件,文件名字是接口的全限定类名,内容是实现类的全限定类名,多个实现类用换行符分隔。
目录结构

文件内容
com.api.impl.DivisionOperation
com.api.impl.PlusOperation
4)测试程序
public class JavaSpiTest {
@Test
public void testOperation() throws Exception {
ServiceLoader<Operation> operations = ServiceLoader.load(Operation.class);
operations.forEach(item->System.out.println("result: " + item.operate(, )));
}
}
5)测试结果
run division operation
result:
run plus operation
result:
1.3 JDK SPI的源码分析
例子很简单,实现的话,可以大胆猜测一下,看名字“ServiceLoader”应该就是用类加载器根据接口的类型加上配置文件里的具体实现名字将实现加载了进来。
接下来通过分析源码进一步了解其实现原理。
1.3.1 ServiceLoader类
PREFIX定义了加载路径,reload方法初始化了LazyIterator,LazyIterator是加载的核心,真正实现了加载。加载的模式从名字上就可以看出,是懒加载的模式,只有当真正调用迭代时才会加载。

1.3.2 hasNextService方法
LazyIterator中的hasNextService方法负责加载配置文件和解析具体的实现类名。

1.3.3 nextService方法
LazyIterator中的nextService方法负责用反射加载实现类。

看完了源码,感觉这个代码是有优化空间的,实例化所有实现其实没啥必要,一来比较耗时,二来浪费资源。Dubbo就没有使用Java原生的SPI机制,而是对其进行了增强,使其能够更好地满足需求。
二、Dubbo SPI
2.1 Dubbo SPI的小栗子
老习惯,在拆解源码之前,先来个栗子。此处示例是在前文例子的基础上稍做了些修改。
1)定义一个接口
修改接口,加上了Dubbo的@SPI注解。
@SPI
public interface Operation {
int operate(int num1, int num2);
}
2)写两个简单的实现
沿用之前的两个实现类。
3)添加一个配置文件
新增配置文件放在dubbo目录下。
目录结构

文件内容
division=com.api.impl.DivisionOperation
plus=com.api.impl.PlusOperation
4)测试程序
public class DubboSpiTest {
@Test
public void testOperation() throws Exception {
ExtensionLoader<Operation> loader = ExtensionLoader.getExtensionLoader(Operation.class);
Operation division = loader.getExtension("division");
System.out.println("result: " + division.operate(, ));
}
}
5)测试结果
run division operation
result:
2.2 Dubbo SPI源码
上面的测试例子也很简单,和JDK原生的SPI对比来看,Dubbo的SPI可以根据配置的kv值来获取。在没有拆解源码之前,考虑一下如何实现。
我可能会用双层Map来实现缓存:第一层的key为接口的class对象,value为一个map;第二层的key为扩展名(配置文件中的key),value为实现类的class。实现懒加载的方式,当运行方法的时候创建空map。在真正获取时先从缓存中查找具体实现类的class对象,找得到就直接返回、找不到就根据配置文件加载并缓存。
Dubbo又是如何实现的呢?
2.2.1 getExtensionLoader方法
首先来拆解getExtensionLoader方法。

这是一个静态的工厂方法,要求传入的类型必须为接口并且有SPI的注解,用map做了个缓存,key为接口的class对象,而value是 ExtensionLoader对象。
2.2.2 getExtension方法
再来拆解ExtensionLoader的getExtension方法。

这段代码也不复杂,如果传入的参数为'true',则返回默认的扩展类实例;否则,从缓存中获取实例,如果有就从缓存中获取,没有的话就新建。用map做缓存,缓存了holder对象,而holder对象中存放扩展类。用volatile关键字和双重检查来应对多线程创建问题,这也是单例模式的常用写法。
2.2.3 createExtension方法
重点分析createExtension方法。

这段代码由几部分组成:
- 根据传入的扩展名获取对应的class。
- 根据class去缓存中获取实例,如果没有的话,通过反射创建对象并放入缓存。
- 依赖注入,完成对象实例的初始化。
- 创建wrapper对象。也就是说,此处返回的对象不一定是具体的实现类,可能是包装的对象。
第二个没啥好说的,我们重点来分析一下1、3、4三个部分。
1)getExtensionClasses方法

老套路,从缓存获取,没有的话创建并加入缓存。这里缓存的是一个扩展名和class的关系。这个扩展名就是在配置文件中的key。创建之前,先缓存了一下接口的限定名。加载配置文件的路径是以下这几个。

2)loadDirectory方法

获取配置文件路径,获取classLoader,并使用loadResource方法做进一步处理。
3)loadResource方法


loadResource加载了配置文件,并解析了配置文件中的内容。loadClass 方法操作了不同的缓存。
首先判断是否有Adaptive注解,有的话缓存到cacheAdaptiveClass(缓存结构为class);然后判断是否wrapperclasses,是的话缓存到cacheWrapperClass中(缓存结构为Set);如果以上都不是,这个类就是个普通的类,存储class和名称的映射关系到cacheNames里(缓存结构为Map)。
基本上getExtensionClasses方法就分析完了,可以看出来,其实并不是很复杂。
2.2.4 IOC
1)injectExtension方法

这个方法实现了依赖注入,即IOC。首先通过反射获取到实例的方法;然后遍历,获取setter方法;接着从objectFactory中获取依赖对象;最后通过反射调用setter方法注入依赖。
objectFactory的变量类型为AdaptiveExtensionFactory。
2)AdaptiveExtensionFactory

这个类里面有个ExtensionFactory的列表,用来存储其他类型的 ExtensionFactory。Dubbo提供了两种ExtensionFactory,一种是SpiExtensionFactory, 用于创建自适应的扩展;另一种是SpringExtesionFactory,用于从Spring的IOC容器中获取扩展。配置文件一个在dubbo-common模块,一个在dubbo-config模块。
配置文件


SpiExtensionFactory中的Spi方式前面已经解析过了。

SpringExtesionFactory是从ApplicationContext中获取对应的实例。先根据名称查找,找不到的话,再根据类型查找。

依赖注入的部分也拆解完毕,看看这次拆解的最后一部分代码。
2.2.5 AOP
创建wrapper对象的部分,wrapper对象是从哪里来的呢?还记得之前拆解的第一步么,loadClass方法中有几个缓存,其中wrapperclasses就是缓存这些wrapper的class。

从代码中可以看出,只要构造方法里有且只有唯一参数,同时此参数为当前传入的接口类型,即为wrapper class。

此处循环创建wrapper实例,首先将instance做为构造函数的参数,通过反射来创建wrapper对象,然后再向wrapper中注入依赖。
看到这里,可能会有人有疑问:为什么要创建一个wrapper对象?其实很简单,系统要在真正调用的前后干点别的事呗。这个就有点类似于spring的aop了。
三、总结
本文简单介绍了JDK的SPI和Dubbo的SPI用法,分析了JDK的SPI源码和Dubbo的SPI源码。在拆解的过程中可以看出,Dubbo的源码还是很值得一读的。在实现方面考虑得很周全,不仅有对多线程的处理、多层缓存,也有IOC、AOP的过程。不过,Dubbo的SPI就这么简单么?当然不是,这篇只拆解了扩展类的加载过程,Dubbo的SPI中还有个很复杂的扩展点-自适应机制。欲知后事如何,请听下回分解~~
本文作者:宜信支付结算部支付研发团队Java研发高级工程师郑祥斌
原文首发于「野指针」
Dubbo源码解析之SPI(一):扩展类的加载过程的更多相关文章
- 【Spring源码分析系列】启动component-scan类扫描加载过程
原文地址:http://blog.csdn.net/xieyuooo/article/details/9089441/ 在spring 3.0以上大家都一般会配置一个Servelet,如下所示: &l ...
- dubbo源码分析4——SPI机制_ExtensionFactory类的作用
ExtensionFactory的源码: @SPI public interface ExtensionFactory { /** * Get extension. * * @param type o ...
- spring源码解析之IOC容器(二)------加载和注册
上一篇跟踪了IOC容器对配置文件的定位,现在我们继续跟踪代码,看看IOC容器是怎么加载和注册配置文件中的信息的.开始之前,首先我们先来了解一下IOC容器所使用的数据结构-------BeanDefin ...
- Android 源码解析:单例模式-通过容器实现单例模式-懒加载方式
本文分析了 Android 系统服务通过容器实现单例,确保系统服务的全局唯一. 开发过 Android 的用户肯定都用过这句代码,主要作用是把布局文件 XML 加载到系统中,转换为 Android 的 ...
- Spring源码剖析2:Spring IOC容器的加载过程
spring ioc 容器的加载流程 1.目标:熟练使用spring,并分析其源码,了解其中的思想.这篇主要介绍spring ioc 容器的加载 2.前提条件:会使用debug 3.源码分析方法:In ...
- Spring源码剖析3:Spring IOC容器的加载过程
本文转自五月的仓颉 https://www.cnblogs.com/xrq730 本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https ...
- dubbo源码解析-spi(4)
前言 本篇是spi的第四篇,本篇讲解的是spi中增加的AOP,还是和上一篇一样,我们先从大家熟悉的spring引出AOP. AOP是老生常谈的话题了,思想都不会是一蹴而就的.比如架构设计从All in ...
- dubbo源码解析-spi(一)
前言 虽然标题是dubbo源码解析,但是本篇并不会出现dubbo的源码,本篇和之前的dubbo源码解析-简单原理.与spring融合一样,为dubbo源码解析专题的知识预热篇. 插播面试题 你是否了解 ...
- dubbo源码解析-spi(3)
前言 在上一篇的末尾,我们提到了dubbo的spi中增加了IoC和AOP的功能.那么本篇就讲一下这个增加的IoC,spi部分预计会有四篇,因为这东西实在是太重要了.温故而知新,我们先来回顾一下,我们之 ...
随机推荐
- [ZJOI2019]麻将(DP+有限状态自动机)
首先只需要考虑每种牌出现的张数即可,然后判断一副牌是否能胡,可以DP一下,令f[i][j][k][0/1]表示到了第i位,用j次i-1,i,i+1和k次i,i+1,i+2,是否出现对子然后最大的面子数 ...
- 【网络流】One-Way Roads
[网络流]One-Way Roads 题目描述 In the country of Via, the cities are connected by roads that can be used in ...
- ELK_疑难杂症处理
一.ELK实用知识点总结 1.编码转换问题 这个问题,主要就是中文乱码. input中的codec=>plain转码: codec => plain {charset => &quo ...
- oracle_(第三课)网络服务配置
1.打开Net Manager 2.开始配置 3.检测 4.SQL Developer 检测 新创数据库连接,连接类型选择 TNS ,若在网络别名中能看到MYORCL,则证明你成功了
- A component required a bean named xxx that could not be found. Action: Consider defining
0 环境 系统:win10 1 正文 https://stackoverflow.com/questions/44474367/field-in-com-xxx-required-a-bean-of- ...
- HTML5 Fundamental Syntax
HTML5 Fundamental Syntax */--> HTML5 Fundamental Syntax 1 Adding Document Structure with HTML5's ...
- [LC] 13. Roman to Integer
Roman numerals are represented by seven different symbols: I, V, X, L, C, Dand M. Symbol Value I 1 V ...
- curl操作和file_get_contents() 比较
1 . curl需要php开启php_curl开启扩展 $ch = curl_init(); $timeout = 5; curl_setopt ($ch, CURLOPT_URL, 'http:// ...
- highcharts series几种写法
一.数据列 数据列是一组数据集合,例如一条线,一组柱形等.图表中所有点的数据都来自数据列对象,数据列的基本构造是: series : [{ name : '', data : [] }] 提示:数据列 ...
- XML的打包与解析
XML的打包与解析 一.XML语言的特点 1.XML独立于任何编程语言,允许人们按接收者容易解析的方式,对复杂数据进行编码.先来看一个简单的XML格式的文件: [XML] 纯文本查看 复制 ...