dubbo的SPI机制与JDK的SPI机制对比

       dubbo一款阿里一款开源的RPC框架,他本身是一款非常复杂的系统,我们主要针对里边的一些核心点来展开分析,其中duboo里的一种核心机制叫SPI( Service Provider Interface)服务发现机制,他是基于原生jdk的SPI机制演化而来。在分析duboo的ExtensionLoader之前,我们先大致了解一下标准JDK的SPI机制。一个最经典的JDK的SPI机制,就是java数据库驱动JDBC,其实JDK自带的jdbc 驱动并没有实现对数据库的封装,而是开放了一系列的接口供各大厂商调用。
了解完了JDK的SPI基本功能之后,来分析一下dubbo的ExtensionLoader,以下是摘自dubbo的官网。http://dubbo.apache.org/#/docs/dev/SPI.md?lang=zh-cn
Dubbo 改进了 JDK 标准的 SPI 的以下问题:
  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName()获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。
在分析dubbo的扩展点加载机制ExtensionLoader加载机制之前,先给大家看一下duboo的层级构成(源自dubbo官网)
  •        config,配置层,对外配置接口,以ServiceConfig, ReferenceConfig为中心,可以直接new配置类,也可以通过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
我们可以看到,dubbo的源码每个层级,不只引用了一个扩展方式,而是提供了多种扩展方式,如果我们把dubbo的源码层级整合成一个整体来看,把他当作一个平面,那么他上面每个层级的扩展实现方式就好比一个槽,这些槽可以供用户自主选择实现合适的扩展方式。
好了,现在我们话不多说了,开始分析duboo启动的时候加载ExtensionLoader,以这种方式来分析ExtensionLoader加载机制。

getExtensionLoader

首先,ExtensionLoader是dubbo的一个类,dubbo器的时候首先会加载ExtensionLoader里面的静态方法getExtensionLoader。
@SuppressWarnings("unchecked")
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {
//首次进来,初始化的type是默认的扩展点interface com.alibaba.dubbo.container.Container
if (type == null)
throw new IllegalArgumentException("Extension type == null");
if(!type.isInterface()) {
throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");
}
if(!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type(" + type +") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
}
//根据这个type去缓存中获取loader
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
//这里会去new一个ExtensionLoader
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}
这里主要是做了一些初始化的合法性校验操作
判断扩展type是否为null,是就抛出异常
  1. 判断扩展type是否为接口,不是接口抛出异常
  2. 判断扩展type是对应的接口是否带有SPI注解,没有抛出异常
  3. 从缓存中去根据扩展type获取ExtensionLoader,如果没有获取到,会先new一个ExtensionLoader,然后加他push到缓存,否则直接返回
进行完合法性校验操作完之后,会从缓存中去获取扩展机制,没有会new一个扩展机制,然后会到ExtensionLoader的私有构造器中。
private ExtensionLoader(Class<?> type) {
//首次进来这个type等于interface com.alibaba.dubbo.container.Container,所以会执行后面的ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()
//其中ExtensionLoader.getExtensionLoader(ExtensionFactory.class)通过,
//再次进来的时候type变为了interface com.alibaba.dubbo.common.extension.ExtensionFactory,就会直接返回
this.type = type;
objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}
代码逻辑,
  1. 首次进来type为interface com.alibaba.dubbo.container.Container,与ExtensionLoader.class不相等,所以会执行后边的逻辑,再次执行.getExtensionLoader方法,将其加入缓存当中
  2. 调用getAdaptiveExtension方法,获得自适应扩展点,将获取到的实例复制给objectFactory。

getAdaptiveExtension

然后进入getAdaptiveExtension函数,分析该方法是如何获取自适应扩展点的。
@SuppressWarnings("unchecked")
public T getAdaptiveExtension() {
//从缓存中获取实例
Object instance = cachedAdaptiveInstance.get();
//双重检查锁,和单点登陆里的双重检查锁类似
if (instance == null) {
if(createAdaptiveInstanceError == null) {
synchronized (cachedAdaptiveInstance) {
instance = cachedAdaptiveInstance.get();
if (instance == null) {
try {
//创建自适应扩展点,获得的实例复制给instance
instance = createAdaptiveExtension();
//将回去的自适应扩展点实例加入缓存
cachedAdaptiveInstance.set(instance);
} catch (Throwable t) {
createAdaptiveInstanceError = t;
throw new IllegalStateException("fail to create adaptive instance: " + t.toString(), t);
}
}
}
}
else {
throw new IllegalStateException("fail to create adaptive instance: " + createAdaptiveInstanceError.toString(), createAdaptiveInstanceError);
}
}
return (T) instance;
}
处理逻辑
  1. 从缓存中获取实例
  2. 因为该实例的单例模式,采用了双重检查锁模式,对该实例做了两次null的判断
  3. 调用createAdaptiveExtension方法创建自适应扩展点,将获得的实例复制给instance,并加入缓存

createAdaptiveExtension

第3点里的createAdaptiveExtension的代码如下
@SuppressWarnings("unchecked")
private T createAdaptiveExtension() {
try {
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
} catch (Exception e) {
throw new IllegalStateException("Can not create adaptive extenstion " + type + ", cause: " + e.getMessage(), e);
}
}
处理逻辑
  1. 首先调用getAdaptiveExtensionClass方法,然后获取实例
  2. 然后将获取的实例最为参数传入injectExtension方法,将结果返回

getAdaptiveExtensionClass

第1点的getAdaptiveExtensionClass方法代码如下
private Class<?> getAdaptiveExtensionClass() {
getExtensionClasses();
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
根据代码可以发现,我们先不管getExtensionClasses方法里做了什么事情,先看主线,cachedAdaptiveClass不为null的话就直接返回,为null的话就先创建再返回。

createAdaptiveExtensionClass

我们先分析createAdaptiveExtensionClass方法,然后再去分析getExtensionClasses,createAdaptiveExtensionClass方法的代码如下。
//基于动态代理生成一个动态的字节码文件
private Class<?> createAdaptiveExtensionClass() {
//生成字节码代码
String code = createAdaptiveExtensionClassCode();
//获得类加载器
ClassLoader classLoader = findClassLoader();
//再次创建一个自适应扩展点
com.alibaba.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(com.alibaba.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
//动态编译字节码
return compiler.compile(code, classLoader);
}

createAdaptiveExtensionClassCode

该方法主要是通过动态代理,基于javassist方式实现,生成二进制字节码代码文本,然后获得类加载器,再次获取一个compiler自适应扩展点,生成一个编译器,然后动态编译字节码文本,生成动态的类。现在主要分析一下createAdaptiveExtensionClassCode是如何生成字节码代码的,通过dubug模式,获取到code的文本,整理一下的代码如下
 package com.alibaba.dubbo.rpc;

 import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adpative implements com.alibaba.dubbo.rpc.Protocol {
public void destroy() {
throw new UnsupportedOperationException("method public abstract void com.alibaba.dubbo.rpc.
Protocol.destroy() of interfacecom.alibaba.dubbo.rpc.Protocol is not adaptive method!");
} 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!");
} 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");
if (arg0.getUrl() =null)
throw new IllegalArgumentException("com.alibaba.dubbo.rpc.Invoker argument getUrl() == null");
com.alibaba.dubbo.common.URL url = arg0.getUrl();
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null)
throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol)
name from url(" + url.toString() + ") use keys([protocol])");
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);
} public com.alibaba.dubbo.rpc.Invoker refer(java.lang.Class arg0, com.alibaba.dubbo.common.URL arg1) throws
java.lang.Class {
if (arg1 == null)
throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg1;
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() );
if(extName == null)
throw new IllegalStateException("Fail to get extension(com.alibaba.dubbo.rpc.Protocol)
name from url(" + url.toString() + ") use keys([protocol])");
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);
}
}
通过代码可以发现,这里默认的RPC方案就是dubbo的自身方案,包括服务的暴露以及引用
String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() ); 

getExtensionClasses

到这里,一个扩展点的加载到此结束,我们再回过头来分析getExtensionClasses方法具体做了什么事情。
//加载扩展点 的实现类
private Map<String, Class<?>> getExtensionClasses() {
Map<String, Class<?>> classes = cachedClasses.get();
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = loadExtensionClasses();
cachedClasses.set(classes);
}
}
}
return classes;
}

loadExtensionClasses

这里又用到了双重检查锁机制,两次判断classes是否为null,然后再调用loadExtensionClasses加载扩展点实现类
 // 此方法已经getExtensionClasses方法同步过。
private Map<String, Class<?>> loadExtensionClasses() {
final SPI defaultAnnotation = type.getAnnotation(SPI.class);
if(defaultAnnotation != null) {
String value = defaultAnnotation.value();
if(value != null && (value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if(names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()
+ ": " + Arrays.toString(names));
}
if(names.length == 1) cachedDefaultName = names[0];
}
}
Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
loadFile(extensionClasses, DUBBO_DIRECTORY);
loadFile(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}

loadFile

loadFile方法根据DUBBO_INTERNAL_DIRECTORY、DUBBO_DIRECTORY、SERVICES_DIRECTORY的路劲去寻找各自的配置文件, 类似于jdk的SPI中放在META-INF/services目录下的文件。loadFiles方法的代码如下,这个方法代码很长
   private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {
String fileName = dir + type.getName();
try {
Enumeration<java.net.URL> urls;
//获取类加载器
ClassLoader classLoader = findClassLoader();
if (classLoader != null) {
urls = classLoader.getResources(fileName);
} else {
urls = ClassLoader.getSystemResources(fileName);
}
if (urls != null) {
while (urls.hasMoreElements()) {
java.net.URL url = urls.nextElement();
try {
BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "utf-8"));
try {
String line = null;
while ((line = reader.readLine()) != null) {
final int ci = line.indexOf('#');
if (ci >= 0) line = line.substring(0, ci);
line = line.trim();
if (line.length() > 0) {
try {
String name = null;
int i = line.indexOf('=');
if (i > 0) {
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
Class<?> clazz = Class.forName(line, true, classLoader);
if (! type.isAssignableFrom(clazz)) {
throw new IllegalStateException("Error when load extension class(interface: " +
type + ", class line: " + clazz.getName() + "), class "
+ clazz.getName() + "is not subtype of interface.");
}
if (clazz.isAnnotationPresent(Adaptive.class)) {
if(cachedAdaptiveClass == null) {
cachedAdaptiveClass = clazz;
} else if (! cachedAdaptiveClass.equals(clazz)) {
throw new IllegalStateException("More than 1 adaptive class found: "
+ cachedAdaptiveClass.getClass().getName()
+ ", " + clazz.getClass().getName());
}
} else {
try {
clazz.getConstructor(type);
Set<Class<?>> wrappers = cachedWrapperClasses;
if (wrappers == null) {
cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();
wrappers = cachedWrapperClasses;
}
wrappers.add(clazz);
} catch (NoSuchMethodException e) {
clazz.getConstructor();
if (name == null || name.length() == 0) {
name = findAnnotationName(clazz);
if (name == null || name.length() == 0) {
if (clazz.getSimpleName().length() > type.getSimpleName().length()
&& clazz.getSimpleName().endsWith(type.getSimpleName())) {
name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() - type.getSimpleName().length()).toLowerCase();
} else {
throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + url);
}
}
}
String[] names = NAME_SEPARATOR.split(name);
if (names != null && names.length > 0) {
Activate activate = clazz.getAnnotation(Activate.class);
if (activate != null) {
cachedActivates.put(names[0], activate);
}
for (String n : names) {
if (! cachedNames.containsKey(clazz)) {
cachedNames.put(clazz, n);
}
Class<?> c = extensionClasses.get(n);
if (c == null) {
extensionClasses.put(n, clazz);
} else if (c != clazz) {
throw new IllegalStateException("Duplicate extension " + type.getName() + " name " + n + " on " + c.getName() + " and " + clazz.getName());
}
}
}
}
}
}
} catch (Throwable t) {
IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + url + ", cause: " + t.getMessage(), t);
exceptions.put(line, e);
}
}
} // end of while read lines
} finally {
reader.close();
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", class file: " + url + ") in " + url, t);
}
} // end of while urls
}
} catch (Throwable t) {
logger.error("Exception when load extension class(interface: " +
type + ", description file: " + fileName + ").", t);
}
}

injectExtension

该方法没有什么很好讲的,里面就是一些if-else,主要是将扩展点的配置文件加载出来或者放入缓存当中。
到此,我们将目光再往前退一下,我们之前是不是调用了getAdaptiveExtensionClass方法,到此为止,我们的仅仅是调用完成了getAdaptiveExtensionClass方法,然后将getAdaptiveExtensionClass的实例当作参数,传入injectExtension方法,我们再看一下injectExtension的代码
 private T injectExtension(T instance) {
try {
//getExtensionLoader的时候赋值的
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
//判断是否以set开头,以set方式动态注入
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
//获取set方法的参数类型
Class<?> pt = method.getParameterTypes()[0];
try {
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).
toLowerCase() + method.getName().substring(4) : "";
//根据类型名称获得对应的扩展点
Object object = objectFactory.getExtension(pt, property);
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("fail to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
这里可以看到,扩展点自动注入的一句就是根据setter方法对应的参数类型和property名称从ExtensionFactory中查询,如果有返回扩展点实例,那么就进行注入操作。到这里getAdaptiveExtension方法就分析完毕了。

总结

ExtensionLoader到此为止大概的走了一遍,一步一步的往里走然后再出来。整个过程还是算比较清晰的。
最后粘贴一张我简单画的时序图

欢迎扫码关注我的微信公众号,我会定期的更新一些个人技术文章

Dubbo死磕之扩展点加载ExetnsionLoader的更多相关文章

  1. Dubbo源码分析之ExtensionLoader加载过程解析

    ExtensionLoader加载机制阅读: Dubbo的类加载机制是模仿jdk的spi加载机制:  Jdk的SPI扩展加载机制:约定是当服务的提供者每增加一个接口的实现类时,需要在jar包的META ...

  2. 设置php在apache下加载ini配置文件路径,~和curl扩展无法加载的问题

    php以模块的方式加载到apache的时候,php配置文件目录为C:windows.这不合理,应该选择php本身目录的配置文件加载,可以在apache的httpd.conf配置文件里设置PHPIniD ...

  3. Dubbo源码分析系列---扩展点加载

    扩展点配置: 约定: 在扩展类的jar包内,放置扩展点配置文件:META-INF/dubbo/接口全限定名,内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔.(摘自dubbo文档) 示例: ...

  4. Dubbo 扩展点加载机制:从 Java SPI 到 Dubbo SPI

    SPI 全称为 Service Provider Interface,是一种服务发现机制.当程序运行调用接口时,会根据配置文件或默认规则信息加载对应的实现类.所以在程序中并没有直接指定使用接口的哪个实 ...

  5. Dubbo实践(六)Spring加载Bean流程

    根据上一小节对于spring扩展schema的介绍,大概可以猜到dubbo中相关的内容是如何实现的. 再来回顾Dubbo实践(一)中定义的dubbo-provider.xml: <?xml ve ...

  6. 《ArcGIS Runtime SDK for Android开发笔记》——(13)、图层扩展方式加载Google地图

    1.前言 http://mt2.google.cn/vt/lyrs=m@225000000&hl=zh-CN&gl=cn&x=420&y=193&z=9& ...

  7. Dubbo扩展点加载机制

    关于ExtensionLoader,http://wely.iteye.com/blog/2304718  这篇文章讲的相当不错:备份一下:)

  8. 解决PHP Redis扩展无法加载的问题(zend_new_interned_string in Unknown on line 0)

    出错代码如下 PHP Warning: PHP Startup: Unable to load 最近在工作中需要使用PHP访问Redis,从https://github.com/phpredis/ph ...

  9. laravel中的自定义函数的加载和第三方扩展库加载

    l 1. 创建文件 app/Helpers/functions.php <?php // 示例函数 function foo() { return "foo"; } 2. 修 ...

随机推荐

  1. powershell脚本,命令行参数传值,并绑定变量的例子

    这是小技巧文章,所以文章不长.但原创唯一,非常重要.我搜了下,还真没有人发 powershell怎样 [命令行 参数 绑定],所以我决定写成博客. 搜索关键字如下: powershell 命令行 参数 ...

  2. myeclipse连接oracle步骤

    1.加载ojdbc.jar驱动(路径:E:\myoracle\oracle\product\11.2.0\dbhome_1\jdbc\lib) 2.String url = "jdbc:or ...

  3. C/C++ 宏中的 #、#@、##的作用

    宏中的# 功能是将其后面的宏参数进行字符串化操作(Stringizing operator), 简单说就是在它引用的宏变量的左右各加上一个双引号. #define STRING(x) #x 下面二条语 ...

  4. Linux shell脚本编程基础之练习篇

    shell脚本编程基础之练习篇. 1.编写一个脚本使我们在写一个脚本时自动生成”#!/bin/bash”这一行和注释信息. #!/bin/bash ] then echo "请输入一个参数& ...

  5. Js制作点击输入框时默认文字消失的效果

    (从已经死了一次又一次终于挂掉的百度空间人工抢救出来的,发表日期 2014-02-17) 为了提高用户体验和易用度,一些设计师会对网页中用户经常用的东西进行优化,比如输入框.一般的输入框是怎样优化的呢 ...

  6. MySQL跨表更新字段 工作记录

    工作中遇到两表查询,从user表中获取用户唯一id字段 写入到另外一张qiuzu表中的uid字段中; 二者可以关联起来的只有用户的手机号码tel字段; 了解需求后数据量稍多,不可能一个一个的手动修改 ...

  7. 基于for循环的呼吸灯

    #include "stm32f10x.h" #include "stm32f10x_gpio.h" //#include "led.h" ...

  8. Hadoop之mapreduce

    doc Hadoop初探之Stream Hadoop Stream 用python + hadoop streaming 编写分布式程序(一) -- 原理介绍,样例程序与本地调试 用python + ...

  9. apache HTML5 History 模式 配置

    说明 使用的  Apache/2.4.6 版本. 文档这么写的: 但是 没说  IfModule 放在哪里 . httpd.conf  里面有大量的  IfModule . 这样的.但是实测 无效.无 ...

  10. SpringBoot整合cxf发布webService

    1. 看看项目结构图 2. cxf的pom依赖 1 <dependency>2 <groupId>org.apache.cxf</groupId>3 <art ...