【Dubbo 源码解析】02_Dubbo SPI
Dubbo SPI:(version:2.6.*)
Dubbo 微内核 + 插件 模式,得益于 Dubbo SPI 。其中 ExtentionLoader是 Dubbo SPI 最核心的类,它负责扩展点的加载和生命周期管理。
ExtensionLoader
ExtensionLoader 类似于 Java SPI 的 ServiceLoader,负责扩展的加载和生命周期维护,它是 Dubbo SPI 最核心的类。
使用最频繁的 API 有如下几个:
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> type)
-- 获取 type 类对应的 ExtensionLoader
public T getAdaptiveExtension()
-- 获取扩展自适应实例(扩展适配器)。扩展点适配器的实现中,一般会调用下面的 API 来获取指定的扩展点实例
warn: dubbo 中大多数的扩展实例,都是通过扩展点匹配器 AdaptiveExtension 来获取的
public T getExtension(String name)
-- 根据 name 获取指定的扩展实例
public T getDefaultExtension()
-- 获取 @SPI value 中指定的默认扩展点
public List<T> getActivateExtension(URL url, String key)
-- 获取被激活的扩展点集合
举例:
Protocol refprotocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(); -- 获取扩展点适配器。扩展点适配器实现类中会调用下面的代码来获取指定的扩展点实例
Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName); -- 获取指定的扩展点
Protocol extension = (Protocol)ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension(); -- 获取 @SPI value 中指定的默认扩展点
ExtensionLoader.getExtensionLoader(ExporterListener.class).getActivateExtension(invoker.getUrl(), Constants.EXPORTER_LISTENER_KEY)) -- 获取被激活的扩展点集合
Dubbo 扩展点机制基本概念
扩展点(Extension Point)
是一个Java的接口。
扩展(Extension)
扩展点的实现类。
扩展实例(Extension Instance)
扩展点实现类的实例。
扩展自适应实例(Extension Adaptive Instance)
扩展适配器。扩展适配器实例就是一个 Extension 的代理,它实现了扩展点接口。在调用扩展点的接口方法时,会根据实际的参数来决定要使用哪个扩展。
@SPI
@SPI 注解作用于扩展点的接口上,表明该接口是一个扩展点,可以被 Dubbo 的ExtentionLoader 加载。如果没有此注解的话, ExtensionLoader.getExtensionLoader(type) 调用会异常。
@Adaptive
@Adaptive 注解使用在类上,表示这个类是一个扩展适配器。当调用 ExtensionLoader.getExtensionLoader(type).getAdaptiveExtension() 会获取到该扩展适配器的实例。
@Adaptive 注解使用在方法上,表示该方法是一个适配(自适应)方法。Dubbo 在为扩展点生成扩展适配器时,如果方法上有 @Adaptive 注解,会为该方法生成对应的实现。实现方法内部会根据方法的参数,来决定使用哪个扩展。
举例: 以 com.alibaba.dubbo.rpc.Protocol 为例
@SPI("dubbo")
public interface Protocol {
int getDefaultPort(); @Adaptive
<T> Exporter<T> export(Invoker<T> invoker) throws RpcException; @Adaptive
<T> Invoker<T> refer(Class<T> type, URL url) throws RpcException; void destroy();
}
Protocol 类上有 @SPI("dubbo"), 含有 4 个方法,其中有 2 个有 @Adaptive 注解。
@SPI("dubbo")
@SPI 表示 Protocol 是一个扩展点。如果 Protocol 类上没有 @SPI 注解的话,试图去加载扩展时,会抛出异常。
"dubbo" 表示 Protocol 的默认扩展点的名称是 "dubbo"
@Adaptive
这里 @Adaptive 用在方法上,表示 export() 与 refer() 方法都是适配方法,Dubbo 会自动为该方法生成适配实现。
而 getDefaultPort() 和 destroy() 方法上没有 @Adaptive 注解,则 Dubbo 在自动生成扩展适配器时,会让这类方法抛出异常。
说明:
Dubbo 自动生成扩展实现的代码参考
ExtensionLoader#createAdaptiveExtensionClassCode()
方法通过阅读源码,我们可以发现,Dubbo 是通过扩展点适配器在运行时动态选择扩展点实现的,而动态选择的策略是通过 URL 中的参数来决定的。
Duubo 为 Protocol 生成的扩展点适配器(Protocol$Adaptive.class)的代码如下:
package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Protocol$Adaptive 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) throws com.alibaba.dubbo.rpc.RpcException {
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 com.alibaba.dubbo.rpc.RpcException {
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);
}
}
源码分析
根据代码 ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension()
来分析 SPI 的加载过程:
#getExtensionLoader(Protocol.class)
new 出一个 ExtensionLoader,并将 Protocol.class 赋值给成员变量 type
#getAdaptiveExtension() 获取 SPI 扩展点适配器
#getExtensionClasses() 首先加载 type 类相关的所有扩展点实例
获取 Protocol.class 上的 @SPI 注解,如果 value() 有且只有一个值,就将值存放到 cachedDefaultName 中;如果没有值,就跳过。 cachedDefaultName 就是 @SPI 默认实现的扩展点 name
按目录顺序加载 SPI 扩展文件,扩展文件名为:com.alibaba.dubbo.rpc.Protocol
目录顺序为:
META-INF/dubbo/internal/
META-INF/dubbo/
META-INF/services/依次读取文件中的内容,内容格式有两种:kv格式 和 v 格式
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
k: 扩展点的 name (name 有多个值的话,用逗号","隔开)
v: 扩展点对应的 class
a. 如果 v 类上有 @Adaptive 注解,就将 v 赋值给 cachedAdaptiveClass
b. 如果 v 类是装饰类,那么就将 v 添加到 cachedWrapperClasses 中 装饰类:v 类含有一个参数的构造函数,且这个参数为 SPI 接口 type,这里为 Protocol.class clazz.getConstructor(type);
c. 如果前端的情况都不满足,且 v 有默认的构造函数 则检查扩展点的 name 是否有值,没有值的话就,取 v 上的 @Extension 注解的 value();没有 @Extension 注解,就取 v 类的简称的小写(simpleName - type.simpleName)
获取 v 类上的 @Activate 注解,有值的话就存放到 cachedActivates 里面(cachedActivates.put(names[0], activate),@Activate 是用来激活指定的扩展点的)
将 v 缓存到 cachedNames 中 (cachedNames.put(clazz, name))
将 v 缓存到 cachedClasses 中 (extensionClasses.put(name, clazz))
#createAdaptiveExtensionClass()
经过上面的加载过程,如果 cachedAdaptiveClass 有值,就返回。没有的话,就使用字节码技术动态创建一个扩展点匹配器 AdaptiveExtensionClass 返回。(绝大部分的 SPI 扩展点匹配器 AdaptiveExtension 的创建方式)
以 Protocol.class 为例,动态创建出的扩展点适配器的类名为 Protocol$Adaptive将获取到的扩展点匹配器实例 instance 存放到 cachedAdaptiveInstance 中并返回
#getExtension(name) 按名称 name 去获取扩展点
先按照上面 2-4 的步骤加载 SPI 扩展文件
从 cachedClasses 中获取扩展类 v (getExtensionClasses().get(name))
通过反射创建扩展点实例 instance (clazz.newInstance()),并将实例放到缓存 EXTENSION_INSTANCES 中 (EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()))
injectExtension() 注入扩展点实例 instance 依赖的扩展点 【IoC】
a. 拿到 instance 类的 set 方法。set 方法需要是 public 且只有一个入参
b. 通过 set 方法的名称反推出扩展点的名字 name
c. 通过 ExtensionFactory 获取到 type + name 的扩展点(objectFactory.getExtension(pt, property),pt 为 set 方法的入参 class 类型,property 为扩展点的 name)
d. 通过反射注入依赖的扩展点(method.invoke(instance, object))将所有的 cachedWrapperClasses 包装在 instance 上,并按照 4 的流程注入 wrapperClass 实例依赖的扩展点 【AOP】
附:
获取 SPI 扩展点有两种方式:
先获取扩展点适配器,然后在方法调用时,扩展点适配器会动态路由到指定的扩展点去调用
通过 getExtension(name) 或者 getDefaultExtension() 方法直接获取 以 Protocol.class 为例,dubbo 在暴露一个服务的时候,会调用 Protocol#export(Invoker<T> invoker) 方法。
具体它是先获取扩展点适配器 Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension(),然后再通过扩展点适配器去调用 export(Invoker<T> invoker) 方法。
而扩展点适配器 Protocol$Adaptive.class 的 export(Invoker<T> invoker) 方法则是根据 invoker 中指定的扩展点名称 extName 去调用指定扩展点的 export(Invoker<T> invoker) 方法
#getActivateExtension(URL url, String key, String group) 获取激活的扩展点列表
public List<T> getActivateExtension(URL url, String[] values, String group) {
List<T> exts = new ArrayList<T>();
List<String> names = values == null ? new ArrayList<String>(0) : Arrays.asList(values);
// 添加框架默认激活的扩展点
if (!names.contains(Constants.REMOVE_VALUE_PREFIX + Constants.DEFAULT_KEY)) {
getExtensionClasses();
for (Map.Entry<String, Activate> entry : cachedActivates.entrySet()) {
String name = entry.getKey();
Activate activate = entry.getValue();
if (isMatchGroup(group, activate.group())) {
T ext = getExtension(name);
if (!names.contains(name) /* 添加默认激活的扩展点,需要排除指定激活的 */
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)
&& isActive(activate, url)) {
exts.add(ext);
}
}
}
Collections.sort(exts, ActivateComparator.COMPARATOR);
} // 添加 url 中指定激活的扩展点
List<T> usrs = new ArrayList<T>();
for (int i = 0; i < names.size(); i++) {
String name = names.get(i);
if (!name.startsWith(Constants.REMOVE_VALUE_PREFIX)
&& !names.contains(Constants.REMOVE_VALUE_PREFIX + name)) {
if (Constants.DEFAULT_KEY.equals(name)) {
if (!usrs.isEmpty()) {
exts.addAll(0, usrs);
usrs.clear();
}
} else {
T ext = getExtension(name);
usrs.add(ext);
}
}
}
if (!usrs.isEmpty()) {
exts.addAll(usrs);
}
return exts;
}
参考:
@see: http://dubbo.apache.org/zh-cn/blog/introduction-to-dubbo-spi.html
@see: http://dubbo.apache.org/zh-cn/blog/introduction-to-dubbo-spi-2.html
如果想了解更多Dubbo源码的知识,请移步 Dubbo源码解读——通向高手之路 的视频讲解:
http://edu.51cto.com/sd/2e565
【Dubbo 源码解析】02_Dubbo SPI的更多相关文章
- Dubbo源码解析之SPI(一):扩展类的加载过程
Dubbo是一款开源的.高性能且轻量级的Java RPC框架,它提供了三大核心能力:面向接口的远程方法调用.智能容错和负载均衡,以及服务自动注册和发现. Dubbo最早是阿里公司内部的RPC框架,于 ...
- dubbo源码解析-spi(4)
前言 本篇是spi的第四篇,本篇讲解的是spi中增加的AOP,还是和上一篇一样,我们先从大家熟悉的spring引出AOP. AOP是老生常谈的话题了,思想都不会是一蹴而就的.比如架构设计从All in ...
- dubbo源码解析-spi(3)
前言 在上一篇的末尾,我们提到了dubbo的spi中增加了IoC和AOP的功能.那么本篇就讲一下这个增加的IoC,spi部分预计会有四篇,因为这东西实在是太重要了.温故而知新,我们先来回顾一下,我们之 ...
- dubbo源码解析-spi(一)
前言 虽然标题是dubbo源码解析,但是本篇并不会出现dubbo的源码,本篇和之前的dubbo源码解析-简单原理.与spring融合一样,为dubbo源码解析专题的知识预热篇. 插播面试题 你是否了解 ...
- dubbo源码解析五 --- 集群容错架构设计与原理分析
欢迎来我的 Star Followers 后期后继续更新Dubbo别的文章 Dubbo 源码分析系列之一环境搭建 博客园 Dubbo 入门之二 --- 项目结构解析 博客园 Dubbo 源码分析系列之 ...
- Dubbo 源码解析四 —— 负载均衡LoadBalance
欢迎来我的 Star Followers 后期后继续更新Dubbo别的文章 Dubbo 源码分析系列之一环境搭建 Dubbo 入门之二 --- 项目结构解析 Dubbo 源码分析系列之三 -- 架构原 ...
- dubbo源码解析-zookeeper创建节点
前言 在之前dubbo源码解析-本地暴露中的前言部分提到了两道高频的面试题,其中一道dubbo中zookeeper做注册中心,如果注册中心集群都挂掉,那发布者和订阅者还能通信吗?在上周的dubbo源码 ...
- 【Dubbo 源码解析】05_Dubbo 服务发现&引用
Dubbo 服务发现&引用 Dubbo 引用的服务消费者最终会构造成一个 Spring 的 Bean,具体是通过 ReferenceBean 来实现的.它是一个 FactoryBean,所有的 ...
- 【Dubbo 源码解析】04_Dubbo 服务注册&暴露
Dubbo 服务注册&暴露 Dubbo 服务暴露过程是通过 com.alibaba.dubbo.config.spring.ServiceBean 来实现的.Spring 容器 refresh ...
随机推荐
- java-----任意进制间的转换
public class Main { public static void main(String[] args) { // TODO Auto-generated method stub Sy ...
- 深入理解JVM(8)——类加载的时机
一.类的生命周期 一个类从加载进内存到卸载出内存一共要经历7个阶段:加载—>验证—>准备-->解析—>初始化—>使用—>卸载. 类加载包括五部分:加载—>验证 ...
- 常用的sort打乱数组方法真的有用?
JavaScript 开发中有时会遇到要将一个数组随机排序(shuffle)的需求,一个常见的写法是这样: function shuffle(arr) { arr.sort(function () { ...
- JSP(1)—基础知识
JSP(1)-基本知识 起源 在很多动态网页中绝大多数网页都是固定不变的只有局部内容需要动态产生和改变,如果使用Servlet程序来输出只有局部内容需要动态改变的网页,其中所有的的静态内容,也需要程序 ...
- Java知识回顾 (10) 线程
再次声明,正如(1)中所描述的,本资料来自于runoob,略有修改. 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务. Java 给多线程编程提供了内 ...
- Mysql中DDL, DML, DCL, 和TCL是什么?
在一些公司中提交给测试团队的SQL脚本会划分为DDL.DML等,但这些概念到底是如何定义的呢? SQL(Structure Query Language)是数据库操作的的核心语言,接下来我们通过一张图 ...
- 00、Word Count
1.开发环境 1.eclipse-jee-neon-3 2.scala-ide:http://download.scala-ide.org/sdk/lithium/e46/scala212/stabl ...
- commit 冲突
git status or repo sync . 时看到如下的提示: error: packages/apps/app/: branch alpha is published (but not me ...
- Hadoop Ls命令添加显示条数限制參数
前言 在hadoop的FsShell命令中,预计非常多人比較经常使用的就是hadoop fs -ls,-lsr,-cat等等这种与Linux系统中差点儿一致的文件系统相关的命令.可是细致想想,这里还是 ...
- SpringBoot2.0针对请求参数@RequestBody验证统一拦截
title: "SpringBoot2.0针对请求参数@RequestBody验证的统一拦截"categories: SpringBoot2.0 Shirotags: Spring ...