前言

  阅读本文需要具备java spi的基础,本文不讲java spi,please google it.

一.Dubbo SPI 简介

  SPI(Service Provider Interface)是服务发现机制,Dubbo没有使用jdk SPI而对其增强和扩展:

  1. jdk SPI仅通过接口类名获取所有实现,但是Duboo SPI可以根据接口类名和key值获取具体一个实现
  2. 可以对扩展类实例的属性进行依赖注入,即IOC
  3. 可以采用装饰器模式实现AOP功能

  你可以发现Dubbo的源码中有很多地方都用到了@SPI注解,例如:Protocol(通信协议),LoadBalance(负载均衡)等。基于Dubbo SPI,我们可以非常容易的进行拓展。ExtensionLoader是扩展点核心类,用于载入Dubbo中各种可配置的组件,比如刚刚说的Protocol和LoadBalance等。那么接下来我们看一下Dubbo SPI的示例

二.Dubbo SPI 示例

  比如现在我们要拓展Protocol这个组件,新建一个DefineProtocol类并修改默认端口为8888:

 /**
* @author GrimMjx
*/
public class DefineProtocol implements Protocol {
@Override
public int getDefaultPort() {
return 8888;
} @Override
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
return null;
} @Override
public <T> Invoker<T> refer(Class<T> aClass, URL url) throws RpcException {
return null;
} @Override
public void destroy() { }
}

  配置文件的文件名字是接口的全限定名,那么在这个例子中就是:com.alibaba.dubbo.rpc.Protocol

  Dubbo SPI所需的配置文件要放在以下3个目录任意一个中:

  META-INF/services/

  META-INF/dubbo/

  META-INF/dubbo/internal/

  同时需要将服务提供者配置文件设计成KV键值对的形式,Key是拓展类的name,Value是扩展的全限定名实现类。比如:

myProtocol=com.grimmjx.edu.DefineProtocol

  然后测试一下:

ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("dubbo-client.xml");
Protocol myProtocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");
System.out.println(myProtocol.getDefaultPort());

  结果如下:

三.源码解析

  那我们就从上面的方法看起,重要方法红色标注:

ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");

1.getExtensionLoader方法,入参是一个可拓展的借口,返回ExtensionLoader实体类,然后可以通过name(key)来获取具体的扩展:

 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 an interface!");
}
// 必须有@SPI注解
if (!withExtensionAnnotation(type)) {
throw new IllegalArgumentException("Extension type (" + type +
") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!");
}
// 每个扩展只会被加载一次
ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
if (loader == null) {
// 初始化扩展
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
}
return loader;
}

2.getExtension方法,首先检查缓存,如果没有则用双检锁方式创建实例:

 public T getExtension(String name) {
if (StringUtils.isEmpty(name)) {
throw new IllegalArgumentException("Extension name == null");
}
if ("true".equals(name)) {
// 默认拓展实现类
return getDefaultExtension();
}
// 获取持有目标对象
Holder<Object> holder = getOrCreateHolder(name);
Object instance = holder.get();
// 双检锁
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {
// 创建实例
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}

3.createExtension方法,这个方法比较核心。做了有4件事情,第3件和第4件分别为上面介绍Dubbo SPI中对jdk SPI扩展的第二和第三点(红字已标注)。请看代码注释:

 private T createExtension(String name) {
// 1.加载配置文件所有拓展类,得到配置名-拓展类的map,从map中获取到拓展类
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
// 2.通过反射创建实例
// EXTENSION_INSTANCES这个map是配置名-拓展类实例的map
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
// 3.注入依赖,即IOC
injectExtension(instance);
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
// 4.循环创建Wrapper实例
for (Class<?> wrapperClass : wrapperClasses) {
// 通过反射创建Wrapper实例
// 向Wrapper实例注入依赖,最后赋值给instance
// 自动包装实现类似aop功能
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
throw new IllegalStateException("Extension instance (name: " + name + ", class: " +
type + ") couldn't be instantiated: " + t.getMessage(), t);
}
}

4.getExtensionClasses方法,这里就是找出所有拓展类,返回一个配置名-拓展类的map。

 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;
}

5.loadExtensionClasses方法,主要就是解析SPI注解,然后加载指定目录的配置文件,也不是很难

 private Map<String, Class<?>> loadExtensionClasses() {
// 获取SPI注解,检查合法等
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<?>>();
// META-INF/dubbo/internal/目录
loadFile(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
// META-INF/dubbo/目录
loadFile(extensionClasses, DUBBO_DIRECTORY);
// META-INF/services/目录
loadFile(extensionClasses, SERVICES_DIRECTORY);
return extensionClasses;
}

  这里返回的extensionClasses的map就肯定包含了"myProtocol"->"com.grimmjx.edu.DefineProtocol"。同时也可以看到,dubbo支持有很多协议:

  接下来不用多说了吧,再从map里get出"myProtocol",得到的就是我们自定义的协议类。

  上面3.createExtension方法,这个方法里注释的3和4很关键,里面实现了依赖注入和AOP的功能,那么接下来我们主要看看Dubbo的IOC和AOP

Dubbo IOC

   Dubbo IOC是通过setter方法注入依赖的,首先遍历方法是否有setter方法特征,如果有则通过objectFactory获取依赖对象进行注入。Dubbo注入的可能是Dubbo的扩展,也有可能是一个Spring bean!

  上面的3方法中有这一行代码,实现了Dubbo SPI的IOC

injectExtension(instance);

  1.injectExtension方法。我们主要看这个方法,自动装配的功能都在这个方法中:

 private T injectExtension(T instance) {
try {
if (objectFactory != null) {
for (Method method : instance.getClass().getMethods()) {
// 如果方法以set开头 && 只有一个参数 && 方法是public级别的
if (method.getName().startsWith("set")
&& method.getParameterTypes().length == 1
&& Modifier.isPublic(method.getModifiers())) {
// 获取setter方法参数类型
Class<?> pt = method.getParameterTypes()[0];
try {
String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : ""; // 关键,从objectFactory里拿出依赖对象
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;
}

  2.objectFactory,究竟这里的objectFactory是什么呢?它是ExtensionFactory类型的,自身也是一个扩展点。我先告诉你这里的objectFactory是AdaptiveExtensionFactory。后面会有解释。

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

  3.getExtension方法,这里比较简单,为了直观,用debug模式看一下,factories有两个,分别是SpiExtensionFactory和SpringExtensionFactory

  对于SpringExtensionFactory就是从工厂里根据beanName拿到Spring bean来注入,对于SpiExtensionFactory就是根据传入Class获取自适应拓展类,那么我们写一段代码,来试试获取一个Spring Bean玩玩,先定义一个bean:

 /**
* @author GrimMjx
*/
public class Worker { private int age = 24; public int getAge() {
return age;
} public void setAge(int age) {
this.age = age;
}
}

  然后再配置文件里配置这个Spring Bean:

  最后简单写个Main方法,可以看出SpringExtensionFactory可以加载Spring Bean:

Dubbo AOP

  Dubbo中也支持Spring AOP类似功能,通过装饰者模式,使用包装类包装原始的扩展点实例。在扩展点实现前后插入其他逻辑,实现AOP功能。说这很绕口啊,那什么是包装类呢?举个例子你就知道了:

 class A{
private A a;
public A(A a){
this.a = a;
} public void do(){
// 插入扩展逻辑
a.do();
}
}

  这里的插入扩展逻辑,是不是就是实现了AOP功能呢?比如说Protocol类,有2个Wrapper,分别是ProtocolFilterWrapper和ProtocolListenerWrapper,我们可以在dubbo-rpc/dubbo-rpc-api/src/main/resources/META-INF/dubbo/internal/com.alibaba.dubbo.rpc.Protocol看到:

filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper

  源码的话createExtension方法里的注释已经写的很清楚了,这里可以自行研究。所以我们在最开始的Dubbo SPI的例子中,我们打个断点就很明显了,得到的myProtocol对象其实是这样的:

  如果你调用export方法的话,会先经历ProtocolFilterWrapper的export方法,再经历ProtocolListenerWrapper的export方法,这样是不是就实现了Spring AOP的功能呢?

Dubbo SPI 自适应

  这里getAdaptiveExtension到底获取的是什么呢,这里涉及到SPI 自适应扩展,十分重要,涉及到@Adaptive注解。

  如果注解加在类上,比如说com.alibaba.dubbo.common.extension.factory.AdaptiveExtensionFactory(自行验证):直接加载当前的适配器

  如果注解加载方法上,比如说com.alibaba.dubbo.rpc.Protocol:动态创建一个自适应的适配器,就像是执行如下代码,返回的是一个动态生成的代理类

Protocol adaptiveExtension = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
 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 interface com.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) {
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) {
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);
}
}

  为什么getDefaultPort和destroy方法都是直接抛出异常呢?因为Protocol接口只有export和refer方法使用了@Adaptive注解,Dubbo会自动生成自适应实例,其他方法都是抛异常。

  为什么还要要动态生成呢?有时候拓展不像在框架启动的时候被加载,而是希望在扩展方法被调用的时候,根据运行时参数进行加载。

  最后看看上面2个标红的代码,是不是就是本文开始的源码分析,是不是很简单了?

参考

http://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html(官方文档,稳稳的)

https://blog.51cto.com/13679539/2125211

搞懂Dubbo SPI可拓展机制的更多相关文章

  1. Java进阶专题(二十六) 将近2万字的Dubbo原理解析,彻底搞懂dubbo

    前言 ​ 前面我们研究了RPC的原理,市面上有很多基于RPC思想实现的框架,比如有Dubbo.今天就从Dubbo的SPI机制.服务注册与发现源码及网络通信过程去深入剖析下Dubbo. Dubbo架构 ...

  2. 搞懂Dubbo服务发布与服务注册

    一.前言 本文讲服务发布与服务注册,服务提供者本地发布服务,然后向注册中心注册服务,将服务实现类以服务接口的形式提供出去,以便服务消费者从注册中心查阅并调用服务. 本文源码分析基于org.apache ...

  3. Java进阶专题(二十七) 将近2万字的Dubbo原理解析,彻底搞懂dubbo (下)

    ...接上文 服务发现 服务发现流程 整体duubo的服务消费原理 Dubbo 框架做服务消费也分为两大部分 , 第一步通过持有远程服务实例生成Invoker,这个Invoker 在客户端是核心的远程 ...

  4. 探究Dubbo的拓展机制: 下

    承接上篇, 本篇博文的主题就是认认真真捋一捋, 看一下 Dubbo是如何实现他的IOC / AOP / 以及Dubbo SPI这个拓展点的 总览: 本篇的话总体上分成两部分进行展开 第一点就是 Dub ...

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

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

  6. Dubbo SPI源码解析①

    目录 0.Java SPI示例 1.Dubbo SPI示例 2.Dubbo SPI源码分析 ​ SPI英文全称为Service Provider Interface.它的作用就是将接口实现类的全限定名 ...

  7. 一文搞懂Java/Spring/Dubbo框架中的SPI机制

    几天前和一位前辈聊起了Spring技术,大佬突然说了SPI,作为一个熟练使用Spring的民工,心中一紧,咱也不敢说不懂,而是在聊完之后赶紧打开了浏览器,开始的学习之路,所以也就有了这篇文章.废话不多 ...

  8. 探究Dubbo的拓展机制: 上

    这篇博文是我决心深度学习Dubbo框架时记录的笔记, 主题是Dubbo的拓展点, 下面的几个部分相对来说比较零散, 貌似是不和主题挂钩的 , 并且是一些很冷门的知识点 , 但是它们确实是深入学习Dub ...

  9. Dubbo SPI 机制源码分析(基于2.7.7)

    Dubbo SPI 机制涉及到 @SPI.@Adaptive.@Activate 三个注解,ExtensionLoader 作为 Dubbo SPI 机制的核心负责加载和管理扩展点及其实现.本文以 E ...

随机推荐

  1. 软件开发中什么是CI/CD

    持续集成(Continuous integration)是一种软件开发实践,每次集成都通过自动化的构建(包括编译,发布,自动化测试)来验证,从而尽早地发现集成错误. 持续部署(continuous d ...

  2. Java 多线程:什么是线程安全性

    线程安全性 什么是线程安全性 <Java Concurrency In Practice>一书的作者 Brian Goetz 是这样描述"线程安全"的:"当多 ...

  3. Spring AOP:Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException

    1 报错 Exception encountered during context initialization - cancelling refresh attempt: org.springfra ...

  4. 梯度提升树GBDT总结

    提升树的学习优化过程中,损失函数平方损失和指数损失时候,每一步优化相对简单,但对于一般损失函数优化的问题,Freidman提出了Gradient Boosting算法,其利用了损失函数的负梯度在当前模 ...

  5. Python3+RobotFramewok 循环判断以及Evaluate用法(三)

    本章主要介绍RF的循环,判断以及关键字Evaluate. 1. for循环 在RF中通过 :FOR 编写循环 :FOR ${i} in range 10 log ${i} @{list} create ...

  6. docker网络(3)

    docker网络介绍 大量的互联网应用服务需要多个服务组件,这往往需要多个容器之间通过网络通信进行相互配合. docker 网络从覆盖范围可分为单个 host 上的容器网络和跨多个 host 的网络. ...

  7. Linux Shell 自动备份脚本

    写一个使用shell脚本增量备份系统文件,顺便复习一下shell脚本相关的命令,这个脚本可以根据自己的需求来备份不同的文件或者文件夹,进行完整备份和增量备份. 参考的网址:http://blog.51 ...

  8. Java泛型(8):自限定&参数协变

    自限定 自限定将强制泛型当做自己的边界参数来使用.自限定所做的,就是要求在继承关系中,像下面这样使用这个类: class A extends SelfBounded<A> {} 它的意义是 ...

  9. 转:微服务框架之微软Service Fabric

    常见的微服务架构用到的软件&组件: docker(成熟应用) spring boot % spring cloud(技术趋势) Service Fabric(属于后起之秀 背后是微软云的驱动) ...

  10. Mac下启动iOS模拟器

    终端列出你安装的所有可用的设备xcrun instruments -s 或 xcrun simctl list 启动方式一: 先启动模拟器:open -a Simulator,这时会以默认的iOS系统 ...