您的位置:首页 > 其它

服务治理中间件 Dubbo 原理解析(Dubbo内核实现)读书笔记

2018-08-23 16:05 465 查看

 

[code]首先感谢一个人,斩秋,很幸运能看到他整理的《服务治理中间件Dubbo原理分析》一文,接下来尝试对这本书的阅读;

一.SPI(service provider interface)传统机制,斩秋典型案例举例如下:
JDK 实现 spi 服务查找: ServiceLoader
ServiceLoader 会遍历所有 jar 查找 META-INF/services/com.example.Spi 文件
public interface Spi {
boolean isSupport(String name);
String sayHello();
}

在A 厂商提供的 jar 包中的 META-INF/services/com.example.Spi 文件内容为:
com.a.example.SpiAImpl #厂商 A 的 spi 实现全路径类名

public class SpiAImpl implements Spi {
public boolean isSupport(String name) {
return "SPIA".equalsIgnoreCase(name.trim());
}
public String syaHello() {
return “hello 我是厂商 A”;
}
}
在B 厂商提供的 jar 包中的 META-INF/services/com.example.Spi 文件内容为:
com.b.example.SpiBImpl #厂商 B 的 spi 实现全路径类名

public class SpiBImpl implements Spi {
public boolean isSupport(String name) {return"SPIB".equalsIgnoreCase(name.trim());}
public String syaHello() {
return “hello 我是厂商 B”;
}
}

一个接口多种实现,就如策略模式一样提供了策略的实现,但是没有提供策略的选择, 使用方可以根据
isSupport 方法根据业务传入厂商名来选择具体的厂商。
public class SpiFactory {
//读取配置获取所有实现
private static ServiceLoader spiLoader =ServiceLoader.load(Spi.class);
//根据名字选取对应实现
public static Spi getSpi(String name) {
for (Spi spi : spiLoader) {
if (spi.isSupport(name) ) {
return spi;
}
}
return null;
}
}

二:基于 SPI 思想 Dubbo 内核实现
1.dubbo中应用举例
public @interface SPI {
String value() default ""; //指定默认的扩展点
}
会依次从这几个文件中读取扩展点
META-INF/dubbo/internal/ //dubbo 内部实现的各种扩展都放在了这个目录
META-INF/dubbo/;META-INF/services/

@SPI("dubbo") 接口上打上 SPI 注解,默认扩展点名字为 dubbo
public interface Protocol {
}
dubbo 中 内 置 实 现 了 各 种 协 议 如 : DubboProtocol InjvmProtocol
HessianProtocol WebServiceProtocol 等等
2.具体Dubbo中spi加载机制
先熟悉这个类 ExtensionLoader 类
1.ExtensionLoader.getExtensionLoader(Protocol.class)每个定义的 spi 的接口
都会构建一个ExtensionLoader实例,存储在ConcurrentMap<Class<?>, ExtensionLoader<?>>
EXTENSION_LOADERS 这个map 对象中

2. loadExtensionClasses 读取扩展点中的实现类
a) 先读取 SPI 注解的 value 值,有值作为默认扩展实现的 key
b) 依次读取路径的文件
META-INF/dubbo/internal/ com.alibaba.dubbo.rpc.Protocol
META-INF/dubbo/ com.alibaba.dubbo.rpc.Protocol
META-INF/services/ com.alibaba.dubbo.rpc.Protocol
3. loadFile 逐行读取 com.alibaba.dubbo.rpc.Protocol 文件中的内容,每行
内容以 key/value 形式存储的。
a) 判断类实现(如: DubboProtocol)上有木有打上@Adaptive 注解,如果
打上了注解,将此类作为 Protocol协议的设配类缓存起来,读取下一行;
否则适配类通过 javasisit 修改字节码生成,关于设配类功能作用后续介绍
b) 如果类实现没有打上@Adaptive, 判断实现类是否存在入参为接口的构
造器(就是 DubbboProtocol 类是否还有入参为 Protocol 的构造器),有
的话作为包装类缓存到此 ExtensionLoader 的 Set<Class<?>>集合中,
这个其实是个装饰模式
c) 如果即不是设配对象也不是 wrapped 的对象,那就是扩展点的具体实现
对象,查找实现类上有没有打上 @Activate 注解 ,有缓存到变量
cachedActivates 的 map 中将实现类缓存到 cachedClasses 中,以便于使用时获取
4. 获取或者创建设配对象 getAdaptiveExtension
a) 如 果 cachedAdaptiveClass有值,说明有且仅有一个实现类打了
@Adaptive, 实例化这个对象返回
b) 如果 cachedAdaptiveClass 为空, 创建设配类字节码。
为什么要创建设配类,一个接口多种实现, SPI 机制也是如此,这是策
略模式,但是我们在代码执行过程中选择哪种具体的策略呢。 Dubbo采
用统一数据模式 com.alibaba.dubbo.common.URL( 它是 dubbo定义的数
据模型不是 jdk 的类),它会穿插于系统的整个执行过程,URL中定义的
协议类型字段protocol , 会根据具体业务设置不同的协议 。
url.getProtocol()值可以是 dubbo 也是可以 webservice, 可以是zookeeper 也可以是redis.
设配类的作用是根据url.getProtocol() 的值extName
去ExtensionLoader.getExtension(extName)选取具体的扩展点实现所以能够利用 javasist 生
成设配类的条件
1)接口方法中必须至少有一个方法打上了@Adaptive 注解
2)打上了@Adaptive 注解的方法参数必须有URL类型参数或者有参数中存在 getURL()方法
三、下面给出 createAdaptiveExtensionClassCode()方法生成 javasist 用来生成 Protocol 适配类后的代码
public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
// 没有打上@Adaptive 的方法如果被调到抛异常
public void destroy() {
throw new UnsupportedOperationException(
"method public abstract void com.alibaba.dubbo.rpc.Protocol.destroy() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!")
}

// 没有打上@Adaptive 的方法如果被调到抛异常
public int getDefaultPort() {
throw new UnsupportedOperationException(
"method public abstract int com.alibaba.dubbo.rpc.Protocol.getDefaultPort() of interface com.alibaba.dubbo.rpc.Protocol is not adaptive method!");
}

// 接口中 export 方法打上@Adaptive 注册
public com.alibaba.dubbo.rpc.Exporter export(com.alibaba.dubbo.rpc.Invoker arg0)
throws com.alibaba.dubbo.rpc.Invoker {
if (arg0 == null)
throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument == null");
//参数类中要有 URL 属性
if (arg0.getUrl() == null)throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
//从入参获取统一数据模型 URL
com.alibaba.dubbo.common.URL url = arg0.getUrl();
String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
//从统一数据模型 URL 获取协议,协议名就是 spi 扩展点实现类的 key
if (extName == null) throw new IllegalStateException(
"Fail to get extension(com.alibaba.dubbo.rpc.Protocol) name from url(" + url.toString() + ") use keys([protocol])");
//利用 dubbo 服务查找机制根据名称找到具体的扩展点实现
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)
ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class).getExtension(extName);
//调具体扩展点的方法
return extension.export(arg0);
}

// 接口中 refer 方法打上@Adaptive 注册
public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0,
com.alibaba.dubbo.common.URL arg1) throws {
//统一数据模型 URL 不能为空
if (arg1 == null)
throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg1;
//从统一数据模型 URL 获取协议,协议名就是 spi 扩展点实现类的 key
String extName = (url.getProtocol() == null ? "dubbo" :url.getProtocol());
if (extName == null)
throw new IllegalStateException("Failtogetextension(com.a libaba.dubbo.rpc.Protocol) name from url("+ url.toString() + ") use keys([protocol])");
//利用 dubbo 服务查找机制根据名称找到具体的扩展点实现
com.alibaba.dubbo.rpc.Protocol extension = (com.alibaba.dubbo.rpc.Protocol)
ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.rpc.Protocol.class)
.getExtension(extName);
//调具体扩展点的方法
return extension.refer(arg0, arg1);
}
}

5. 通过 createAdaptiveExtensionClassCode()生成如上的 java 源码代码,要
被 java 虚拟机加载执行必须得编译成字节码, dubbo 提供两种方式去执行代
码的编译
1)利用 JDK 工具类编译 2)利用 javassit 根据源代码生成字节码。
1) 生成 Adaptive 代码 code
2) 利用 dubbo 的 spi 扩展机制获取 compiler 的设配类
3) 编译生成的 adaptive 代码
在此顺便介绍下@Adaptive 注解打在实现类上跟打在接口方法上的区别
1) 如果有打在接口方法上,调 ExtensionLoader.getAdaptiveExtension()
获取设配类,会先通过前面的过程生成java的源代码,在通过编译器编
译成class加载 。但是Compiler的实现策略选择也是通过
ExtensionLoader.getAdaptiveExtension(),如果也通过编译器编译成
class 文件那岂不是要死循环下去了吗?
2) ExtensionLoader.getAdaptiveExtension(),对于有实现类上去打了注解
@Adaptive 的 dubbo spi 扩展机制,它获取设配类不在通过前面过程生成设配类 java 源代
码,而是在读取扩展文件的时候遇到实现类打了注解@Adaptive 就把这个类作为设配类缓存在
ExtensionLoader 中,调用是直接返回
6. 自动 Wrap 上扩展点的 Wrap 类
这是一种装饰模式的实现,在 jdk 的输入输出流实现中有很多这种设计,在
于增强扩展点功能。这里我们拿对于 Protocol 接口的扩展点实现作为实例讲
解:Protocol 继承关系 ProtocolFilterWrapper,ProtocolListenerWrapper 这个两个类是装饰对象用来增强其他扩展点实现的功能。 ProtocolFilterWrapper 功能主要是在 refer 引用远程服务的中透
明的设置一系列的过滤器链用来记录日志,处理超时,权限控制等等功能;ProtocolListenerWrapper 在 provider 的 exporter,unporter 服 务 和consumer 的 refer 服务, destory 调用时添加监听器, dubbo 提供了扩展但是没有默认实现哪些监听器。

Dubbo 是如何自动的给扩展点 wrap 上装饰对象的呢?
1) 在 ExtensionLoader.loadFile 加载扩展点配置文件的时候,对扩展点类有接口类型为参数的构造器就是包转对象,缓存到集合中去
2) 在调ExtensionLoader的createExtension(name)根据扩展点 key 创建扩展的时候,先实例化扩展点的实现, 在判断时候有此扩展时候有包装类缓存,有的话利用包转器增强这个扩展点实现的功能。

7. IOC大家所熟知的ioc是spring的三大基础功能之一,dubbo 的ExtensionLoader 在加载扩展实现的时候内部实现了个简单的 ioc 机制来实现对扩展实现所依赖的参数的注入, dubbo对扩展实现中公有的set方法且入参个数为一个的方法,尝试从对象工厂ObjectFactory获取值注入到扩展点实现中去

下面我们来看看 ObjectFactory 是如何根据类型和名字来获取对象的, ObjectFactory 也是基于dubbo的spi扩展机制的它跟Compiler接口一样设配类注解@Adaptive是打在类AdaptiveExtensionFactory 上的不是通过 javassist 编译生成的。AdaptiveExtensionFactory 持有所有 ExtensionFactory 对象的集合, dubbo内部默认实现的对象工厂是 SpiExtensionFactory 和SpringExtensionFactory,他们经过 TreeMap 排好序的查找顺序是优先先从SpiExtensionFactory 获取,如果返回空在从 SpringExtensionFactory 获取
1) SpiExtensionFactory 工厂获取要被注入的对象,就是要获取dubbo spi
扩展的实现,所以传入的参数类型必须是接口类型并且接口上打上了
@SPI 注解,返回的是一个设配类对象。
2) SpringExtensionFactory, Dubbo 利用 spring 的扩展机制跟 spring 做了
很好的融合。在发布或者去引用一个服务的时候,会把 spring 的容器添
加到 SpringExtensionFactory 工厂集合中去, 当 SpiExtensionFactory
没有获取到对象的时候会遍历 SpringExtensionFactory 中的 spring 容
器来获取要注入的对象

三: 动态编译
我们运行的 java 代码,一般都是编译之后的字节码。 Dubbo 为了实现基于 spi
思想的扩展特性,特别是能够灵活添加额外功能,对于扩展或者说是策略的选择
这个叫做控制类也好设配类也好的类要能够动态生成。当然对应已知需求如
Protocol, ProxyFactory 他们的策略选择的设配类代码 dubbo 直接提供也无妨,
但是 dubbo 作为一个高扩展性的框架,使得用户能够添加自己的需求,根据配置
动态生成自己的设配类代码,这样就需要在运行的时候去编译加载这个设配类的
代码。下面我们就是来了解下 Dubbo 的动态编译。

编译接口定义
@SPI("javassist")
public interface Compiler {
Class<?> compile(String code, ClassLoader classLoader);
}

SPI 注解表示如果没有配置,dubbo 默认选用 javassist 编译源代码
接口方法 compile 第一个入参 code,就是 java 的源代码
接口方法 compile 第二个入参 classLoader,按理是类加载器用来加载编译后的
字节码,其实没用到,都是根据当前线程或者调用方的 classLoader加载的
@Adaptive
public class AdaptiveCompiler implements Compiler {
private static volatile String DEFAULT_COMPILER;public Class<?> compile(String code, ClassLoader classLoader) {
Compiler compiler;
ExtensionLoader<Compiler> loader =
ExtensionLoader.getExtensionLoader(Compiler.class);
String name = DEFAULT_COMPILER; // copy reference
if (name != null && name.length() > 0) {
compiler = loader.getExtension(name);
} else {
compiler = loader.getDefaultExtension();
}
return compiler.compile(code, classLoader);
}
}
AdaptiveCompiler 是 Compiler 的设配类, 它有类注解@Adaptive 表示这个
Compile r 的设配类不是动态编译生成的。 AdaptiveCompiler 作用就是策略的选
择,根据条件选择何种编译策略来编译动态生成的源代码。
AbstractCompiler 为编译的抽象类,抽象出公用逻辑,这里它主要是利用正则
匹配出源代码中的包名和类名后先在 jvm 中 Class.forName 看下是否存在,如果
存在反回,不存在在执行编译与加载。
关于 JavassistCompiler 和 JdkCompiler 执行 doCompile 的过程都是利用
Javassit 和 Jdk 提供的相关 api 或者扩展接口实现的。

如果系统配置中没有给 AdaptiveCompiler 设置哪种 compiler, 那么获取默认
compiler, 默认策略根据打在接口 Compiler 上的@SPI 值为 javassist, 所以
dubbo 默认利用 javassist 生成 SPI 机制的设配用来根据统一数据模型 URL 中获
取协议来选择使用何种策略

 

 

 

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: