这一篇我们先不讲Dubbo中的具体业务逻辑,我们来打基础,聊一聊Dubbo中的SPI机制。

Dubbo SPI是干啥的

了解一个技术,得先知道它是为了解决什么问题而产生的。那么Dubbo SPI是干什么的呢?

按照官网的描述,Dubbo是一款高性能的RPC框架,是为了解决应用间的服务治理问题而诞生的。
服务治理会涉及到很多方面的内容,如网络连接、集群容错、服务路由、负载均衡等。这么多的内容,每个都有不同的解决方案。如网络连接,可以由netty实现,也可以由mina实现。

Dubbo并没有局限于某一种解决方案,而是博采众长。它通过一种机制,让用户可以自行扩展加载想要的功能实现。这种机制就是Dubbo SPI。

Dubbo SPI与Java SPI的渊源

你可能了解过Java SPI机制(如果不了解,建议了解一下),Dubbo SPI是在Java SPI的基础之上,又进行了一层功能的扩展得到的。相较于Java SPI方式,Dubbo SPI具有以下几个方面的优势:

  • 按需加载
    Java SPI无论你是否需要这个扩展类,都会将其加载到内存中。这样可能会造成资源浪费。Dubbo则不然,采用了一种按需加载扩展的方式,避免不必要的资源浪费
  • 扩展间的IOC和AOP
    Dubbo SPI机制还实现了一种扩展间的注入与切入功能。简单来说,一个扩展类可以注入另一个扩展类中,外层包装的扩展可以做更多的事,如:流量统计、监控等,好处不言而喻。

Dubbo SPI使用

Dubbo SPI的使用和Java SPI非常相像,也并不复杂,这里就不再赘述。如果你不太了解,建议去官网看一下。

一个普通扩展类是如何加载的

在Dubbo中,标注了@SPI的接口,即被认为是Dubbo SPI扩展类.。

接下来我们聊聊一个普通Dubbo SPI扩展类是如何加载的。首先从Dubbo中最长见到的一种扩展调用方式开始:

ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(DubboProtocol.NAME);

这行代码顾名思义,就是先查找Protocol这个类的加载器,然后利用加载器获取 Dubbo协议扩展类。
前半段的加载器获取逻辑我们暂时忽略,先从getExtension来说起。

不看源码实现,先想一想,如果要你实现一个getExtension方法,你会做哪些事情?

  • 扩展类加载完了之后,你是不是要将它缓存起来,方便下次直接获取?
  • 扩展类是从哪里获取的?对,配置文件。那么是不是要先读取文件,知道有哪些扩展类?
  • 上面说到扩展类支持IOC和AOP,那么在实例化扩展类的时候,是否应该有对扩展类中进行注入的逻辑?

带着上面几个猜测,我们来看一下getExtension的源码,看看一个普通扩展类是如何加载进来的。

public T getExtension(String name) {// $-- 空校验if (name == null || name.length() == 0)throw new IllegalArgumentException("Extension name == null");// $-- 特殊处理,true则进行默认扩展类加载if ("true".equals(name)) {return getDefaultExtension();}// $-- 尝试根据name从缓存中获取类实例,没有则创建Holder<Object> holder = cachedInstances.get(name);if (holder == null) {cachedInstances.putIfAbsent(name, new Holder<Object>());holder = cachedInstances.get(name);}Object instance = holder.get();// $-- double checkif (instance == null) {synchronized (holder) {instance = holder.get();if (instance == null) {// $-- 创建扩展类,并进行缓存instance = createExtension(name);holder.set(instance);}}}return (T) instance;
}

在getExtension方法里,Dubbo主要进行了空校验,并对一些特殊逻辑进行判断处理。接着就是一套“查缓存,缓存不存在创建并缓存”的套路了。在Dubbo的源码里存在大量这样的缓存套路使用,这样的缓存使用对提升效率是非常明显的。
注意:这里缓存的是扩展类的实例

接下来看一下createExtension创建扩展类的逻辑。

private T createExtension(String name) {// $-- 先获取该name对应的Class类Class<?> clazz = getExtensionClasses().get(name);if (clazz == null) {throw findException(name);}try {// $-- 通过扩展点类名从缓存中获取该类的实例,如果没有,则进行创建T instance = (T) EXTENSION_INSTANCES.get(clazz);if (instance == null) {EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());instance = (T) EXTENSION_INSTANCES.get(clazz);}// $-- 扩展类实例的依赖注入(setter)injectExtension(instance);// $-- 对包装扩展类进行依赖注入(constructor)Set<Class<?>> wrapperClasses = cachedWrapperClasses;if (wrapperClasses != null && !wrapperClasses.isEmpty()) {for (Class<?> wrapperClass : wrapperClasses) {instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));}}return instance;} catch (Throwable t) {throw new IllegalStateException("Extension instance(name: " + name + ", class: " +type + ")  could not be instantiated: " + t.getMessage(), t);}
}

createExtension先要根据name获取对应的扩展类,如果系统中都没有这个扩展类,那么生成扩展类实例就无从谈起了。
然后又是一套缓存的套路,通过反射创建了该扩展类的实例,放入缓存中。
接下来就是对扩展类进行IOC注入的逻辑了,主要对扩展类实例内部的setter方法进行注入。以上面DubboProtocol的加载为例,这里就是找DubboProtocol类中的setter方法,如果有扩展类可以注入,就进行注入。
随后是对包装扩展类进行依赖注入,使用的是构造器注入。这里与上述setter方法是不同的。同样以DubboProtocol为例,这里是对系统中以Protocol为构造函数的扩展类进行注入,注入的就是当前的DubboProtocol类。相当于给DubboProtocol外面装饰了一下再返回(装饰器模式)。举个例子,如:ProtocolListenerWrapper类

从文件中加载扩展类

关于扩展类注入的逻辑我们稍后再聊,先看一下Dubbo从文件中加载扩展类Class的实现逻辑。
方法getExtensionClasses会加载当前系统中所有的扩展点类

private Map<String, Class<?>> getExtensionClasses() {// $-- 缓存老套路Map<String, Class<?>> classes = cachedClasses.get();if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {// $-- 加载所有扩展类,生成包含所有扩展类Class的一个Mapclasses = loadExtensionClasses();cachedClasses.set(classes);}}}return classes;
}

这里没有啥逻辑,依然是一个缓存的老套路。不同的是这里缓存的是扩展类Class,而不是实例。
加载的逻辑还要往下看

private Map<String, Class<?>> loadExtensionClasses() {final SPI defaultAnnotation = type.getAnnotation(SPI.class);if (defaultAnnotation != null) {String value = defaultAnnotation.value();if ((value = value.trim()).length() > 0) {String[] names = NAME_SEPARATOR.split(value);// $-- @SPI注解只允许指定一个默认值if (names.length > 1) {throw new IllegalStateException("more than 1 default extension name on extension " + type.getName()+ ": " + Arrays.toString(names));}// $-- @SPI指定了一个默认值,缓存起来if (names.length == 1) cachedDefaultName = names[0];    }}Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();// $-- 加载扩展类资源目录loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);loadDirectory(extensionClasses, DUBBO_DIRECTORY);loadDirectory(extensionClasses, SERVICES_DIRECTORY);return extensionClasses;
}

这里首先对扩展类@SPI注解的使用进行了一下校验。如果@SPI注解中配置了两个默认值,则抛异常。方法中的type就是我们要加载的Protocol接口,Protocol接口上使用了@SPI(“dubbo”)进行修饰,代表默认使用dubbo扩展类。
随后就是文件的读取与扩展类的加载了。这里可以看到,Dubbo默认的扩展类目录有以下三个:

  • META-INF/services/
  • META-INF/dubbo/
  • META-INF/dubbo/internal/

那么它是如何读取文件,加载扩展类资源的呢,且看loadDirectory方法

private void loadDirectory(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 resourceURL = urls.nextElement();// $-- 加载资源文件中的扩展类loadResource(extensionClasses, classLoader, resourceURL);}}} catch (Throwable t) {logger.error("Exception when load extension class(interface: " +type + ", description file: " + fileName + ").", t);}
}

这里的主要逻辑实际上是获取类加载器,然后将文件的读取交给loadResource方法

private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {try {BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), "utf-8"));try {String line;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为i扩展类的key,如 @SPI("dubbo")中的dubbo,line为类名name = line.substring(0, i).trim();line = line.substring(i + 1).trim();}if (line.length() > 0) {// $-- 反射创建class并加载loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);}} catch (Throwable t) {IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t);exceptions.put(line, e);}}}} finally {reader.close();}} catch (Throwable t) {logger.error("Exception when load extension class(interface: " +type + ", class file: " + resourceURL + ") in " + resourceURL, t);}
}

到这里,我们终于看到熟悉的文件读取操作了。
Dubbo扩展类文件的格式一般都是 “key=value” 样式的,并且支持 “#” 作为注释符的。

具体的加载类操作依然要往下看,loadClass方法是真正的加载类方法。

private void loadClass(Map<String, Class<?>> extensionClasses, java.net.URL resourceURL, Class<?> clazz, String name) throws NoSuchMethodException {// $-- 校验扩展类与接口是否类型匹配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.");}// $-- 该类上是否使用了Adaptive注解,有则进行缓存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 if (isWrapperClass(clazz)) {// $-- 是否为包装扩展类(构造函数注入),是则加入set缓存Set<Class<?>> wrappers = cachedWrapperClasses;if (wrappers == null) {cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();wrappers = cachedWrapperClasses;}wrappers.add(clazz);} else {clazz.getConstructor();// $-- 此处应该是兼容旧方法(@Extension注解)的处理逻辑if (name == null || name.length() == 0) {name = findAnnotationName(clazz);if (name.length() == 0) {throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL);}}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());}}}}
}

这里首先校验了扩展类类型。
随后对@Adaptive注解进行了判断处理。如果类上标注了@Adaptive,则代表其为默认实现,会进行缓存。关于@Adaptive注解,我们这里暂时不说,后续再聊。

接着判断了该扩展类是否存在包装扩展类,如果有的话,就加入到缓存。包装扩展类的逻辑,我们在注入的时候再统一讲,这里先知道有这么一层处理逻辑。

如果上述特殊场景都不满足,那么我们就直接进入默认的加载处理逻辑:首先判断了扩展类的name的取值,此处兼容了对旧的@Extension注解的处理逻辑。随后就自激活扩展类(@Activate注解修饰)和普通扩展类分别加入到相应的缓存中。

这样将三个目录都读取完成之后,扩展类就被直接加载到缓存中了。(o゜▽゜)o☆

扩展类注入

现在让我们将注意力转回到createExtension中。获取到扩展类后,直接进行实例化,然后就是重头戏注入了。
注入分为两种场景,分别是扩展类setter方法注入和包装扩展类构造器注入。

扩展类setter注入

setter方法注入由方法injectExtension实现

private T injectExtension(T instance) {try {if (objectFactory != null) {for (Method method : instance.getClass().getMethods()) {// $-- set方法注入if (method.getName().startsWith("set")&& method.getParameterTypes().length == 1&& Modifier.isPublic(method.getModifiers())) {// $-- 方法上存在@DisableInject注解的,此处不进行注入if (method.getAnnotation(DisableInject.class) != null) {continue;}// $-- 获取要注入的类型Class<?> pt = method.getParameterTypes()[0];try {// $-- 获取要注入的扩展类名,如 setProtcol ==> protocolString property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";// $-- 获取注入类实例Object object = objectFactory.getExtension(pt, property);if (object != null) {// $-- 调用set方法进行注入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;
}

objectFactory为扩展加载器工厂,它的主要作用是生成扩展加载器,下面会详细介绍,这里先忽略。
整个setter注入的逻辑还是非常清晰的,配合代码中的注释,相应应该不用我来解释什么了。

包装扩展类构造器注入

Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (wrapperClasses != null && !wrapperClasses.isEmpty()) {for (Class<?> wrapperClass : wrapperClasses) {instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));}
}

首先判断该扩展类是否有包装,这里是通过cachedWrapperClasses缓存来判断的。
对于包装扩展类,随后进行包装扩展类的实例化,以及该包装扩展类的setter注入。

整个逻辑还是非常简单的,唯一的问题在于cachedWrapperClass是如何赋值的呢?
回想读取文件加载类时,会进行该类是否有包装的判断。如果有包装,则加入到cachedWrapperClasses缓存中。

else if (isWrapperClass(clazz)) {// $-- 是否为包装扩展类(构造函数注入),是则加入set缓存Set<Class<?>> wrappers = cachedWrapperClasses;if (wrappers == null) {cachedWrapperClasses = new ConcurrentHashSet<Class<?>>();wrappers = cachedWrapperClasses;}wrappers.add(clazz);
}

isWrapperClass是其判断代码,主要是判断要加载的类是否有当前接口的构造函数

private boolean isWrapperClass(Class<?> clazz) {try {// $-- 判断Clazz是否有type类型的构造函数,没有则抛异常clazz.getConstructor(type);return true;} catch (NoSuchMethodException e) {return false;}
}

这里你可能有一点懵,我们举Protocol的例子来说明一下吧。

当进行文件读取,加载扩展类读取时,会加载到类ProtocolListenerWrapper。这个类里存在以Protocol为参数的构造方法,说明ProtocolListenerWrapper这个类可以进行包装扩展,因此就会在cachedWrapperClasses中记录下来。

当我们通过createExtension来创建dubbo类型的Protocol时,判断到其包装扩展类缓存cachedWrapperClasses中存在ProtocolListenerWrapper类型的包装类时,就会对DubboProtocol进行装饰,返回的是ProtocolListenerWrapper类。(DubboProtocol的包装类不只ProtocolListenerWrapper,这里仅仅是作为一个例子进行理解)

另外需要注意的是,代码中并没有对包装的顺序进行定义,所以理论上,文件读取时,加载到cachedWrapperClasses缓存中的顺序会直接影响到包装的结果。

ExtensionLoader的获取

上述就是整个普通扩展类加载的流程了,不知道你是否了然于胸了呢?

结合我个人的经历,当我初读这段代码时,对type这个字段很迷惑,特别是在包装扩展类缓存判断这一块儿。那么这个type到底是啥?

别急,让我们将视线转换回一切的起点

ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(DubboProtocol.NAME);

你可能已经猜到了,这里的Protocol.class就是type!
现在是时候分析一下这段代码的前半段了:如何获取某个扩展类的ExtensionLoader?

public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type) {// $-- 空校验if (type == null)throw new IllegalArgumentException("Extension type == null");// $-- 是否为接口校验if (!type.isInterface()) {throw new IllegalArgumentException("Extension type(" + type + ") is not interface!");}// $-- 是否有@SPI注解校验if (!withExtensionAnnotation(type)) {throw new IllegalArgumentException("Extension type(" + type +") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");}// $-- 缓存套路ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);if (loader == null) {// $-- 这里创建type类型的ExtensionLoaderEXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);}return loader;
}

获取扩展类加载器的这段代码里,主要先对要加载的类做了一些校验,然后是缓存的老套路。我们主要来看一下创建ExtensionLoader的方法,也就是ExtensionLoader的构造方法。

private ExtensionLoader(Class<?> type) {this.type = type;objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());
}

到这里可以看到,我们一直疑惑的type原来是在构造方法里被赋值的。type代表的实际上是我们将要获取的扩展类的类型定义。
objectFactory是扩展类加载器工厂,用来生成扩展类加载器ExtensionLoader的。这里先判断了一下我们要加载的类是否就是扩展类加载器工厂本身,如果是本身的话,则返回null,不用加载。否则,就再通过扩展类加载机制获取ExtensionLoader的扩展类。

ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension());

像不像一个“套娃”?

Dubbo SPI机制(上):一个普通的扩展类是如何加载的相关推荐

  1. Dubbo SPI机制学习总结(持续更新...)

    参考文章:Dubbo的SPI机制分析 首先来看看 Java SPI 的机制 Java SPI 起初是提供给厂商做插件开发用的,例如数据库驱动java.sql.Driver,市面上各种各样的数据库,不同 ...

  2. Dubbo SPI机制和原理解析

    简介 SPI(service provider interface)是一种服务发现机制,通过加载指定路径下配置文件中的实现类,达到运行时用实现动态替换接口的目的.SPI常常用于扩展应用的功能,Dubb ...

  3. Java-Day11 面向对象遍程的入门 (类属性的默认值、构造方法、类的成员、static关键字、类的成员加载顺序、Java(权限)访问修饰符)

    目录 1. 类的属性的默认值问题 2. 构造方法(Constructor) 3. 类的成员 3.1 类的成员之一:属性 3.2 UML类图 4. static关键字 5. 类的成员加载(运行)顺序 6 ...

  4. java类是如何加载的?不知道classLoader和双亲委派,不是一个合格的程序员

    目录 详细图送上 类加载器子系统 类的加载过程 加载(loading)阶段 链接(linking) 验证(Verify) 准备(Prepare) 解析(Resolve) 初始化(Initializat ...

  5. java 类编译_Java类编译、加载、和执行机制

    Java类编译.加载.和执行机制 标签: java 类加载 类编译 类执行 机制 0.前言 个人认为,对于JVM的理解,主要是两大方面内容: Java类的编译.加载和执行. JVM的内存管理和垃圾回收 ...

  6. JavaWeb --MYSql(MySql基础,MySql高级,JDBC,从类路径下加载输入流对象)

    SQL分类 DDL(Data Definition Language)数据库定义语言,用来定义数据库对象:数据库,表,列等(操作数据库,表等) DML(Data Manipulation Langua ...

  7. java编写hot_类的热加载(Hot Deployment)的简单例子

    应用服务器一般都支持热部署(Hot Deployment),更新代码时把新编译的确类 替换旧的就行,后面的程序就执行新类中的代码.这也是由各种应用服务器的独 有的类加载器层次实现的.那如何在我们的程序 ...

  8. Java类的热加载原理与实现

    1 类加载原理 Java类的加载过程主要分为三个步骤,加载.链接.初始化,其中将类加载到JVM中的工作由类加载器完成.在加载阶段,类加载器可以从不同的数据源(jar文件.class文件.网络文件)读取 ...

  9. flash AS3 Loader加载外部文件类 及队列加载方法

    从2011年开始使用这段代码为了应对各种加载修修改改了这么多年,很基础的功能,没啥特别的,重点在于加载子swf获取它的类,还有就是卸载子swf时要清理内存,否则内存占用会节节窜高,内存溢出,所以加载新 ...

最新文章

  1. ISME:多组学揭示低氧环境下的汞甲基化细菌
  2. r语言中调用c 程序,如何在R程序包中调用C函数
  3. 批量备SAP中CBO ABAP 程序代码为TXT文件备份
  4. boost::log相关用法的测试程序
  5. oracle表参数,Oracle 表的创建 及相关参数
  6. 什么是工业微型计算机,2008年(下)全国自考工业用微型计算机试卷02241
  7. 蓝桥杯 基础练习 回文数
  8. 数字信号处理matlab心得,数字信号处理学习心得体会.doc
  9. 我的管理实践---《人件》读后感
  10. 自定义加载等待动画,仿金山词霸
  11. Android仿人人客户端(转)
  12. 如何保障短网址的安全性?
  13. python testng_自动化测试框架TestNG
  14. C6能比C8快多少(Altera的FPGA速度等级)
  15. 下次约会时,让人工智能做你的僚机!
  16. 关于杂质过滤的一点研究
  17. 三维坐标系介绍与转换
  18. selenium爬取评论
  19. win10无法识别linux硬盘,win10硬盘不能识别怎么办_win10硬盘不能识别解决办法_飞翔教程...
  20. jieba分词的源码解析,并从零实现自己的分词器

热门文章

  1. jquery的each循环return语法有点坑
  2. 学计算机转业哪个学校好,大学报考计算机专业哪个学校比较好
  3. aspen为什么不能用_我是如何学习Aspen Plus软件的---入门必看
  4. 小满nestjs(第十九章 nestjs 管道验证DTO)
  5. 修改idea字体大小
  6. 新应用——养老院管理应用,信息化的多功能管理应用
  7. 优雅的时钟翻页效果,让你的网页时钟与众不同!
  8. PowerMock详解
  9. 快速云:五分钟了解几款磁盘测试与IO查看的工具
  10. django安装指定版本