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(2, 2)));
- }
- }
5) 测试结果
- run division operation
- result:1
- run plus operation
- result:4
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(1, 2));
- }
- }
5) 测试结果
- run division operation
- result:0
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 中还有个很复杂的扩展点 - 自适应机制. 欲知后事如何, 请听下回分解~~
来源: https://www.cnblogs.com/yixinjishu/p/12420696.html