【Dubbo】带着问题看源码:什么是SPI机制?Dubbo是如何实现的?
什么是SPI?
在Java中,SPI全称为 Service Provider Interface,是一种典型的面向接口编程机制。定义通用接口,然后具体实现可以动态替换,和 IoC 有异曲同工之妙。
Java SPI 实现DEMO
定义一个接口
public interface Human {
String sayHello();
}
定义两个实现类
public class American implements Human {
@Override
public String sayHello() {
return "Hello,I'm American";
}
} public class Chinese implements Human {
@Override
public String sayHello() {
return "你好,我是中国人";
}
}
新建 接口全名文件,例如本例: com.xx.spi.Human,放到META-INF/services路径下,文件内容为实现类的全名,不同实现类之前换行。
com.xx.spi.American
com.xx.spi.Chinese
编码测试
public class SpiApplication { public static void main(String[] args) { ServiceLoader<Human> humans = ServiceLoader.load(Human.class); for (Human human : humans) {
System.out.println(human.sayHello());
}
}
}
Java SPI 源码解析
ServiceLoader
提供了静态方法 load(Class<S> service)
,本质上还是要new一个新的实例,调用私有构造方法 ServiceLoader(Class<S> svc, ClassLoader cl)
.
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 此时的 cl 为 Thread.currentThread().getContextClassLoader();
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
初始化 内部 LazyIterator
public void reload() {
// Cached providers, in instantiation order
//private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
//先清空缓存
providers.clear();
//重新初始化 lazyIterator ,此时对象还没有被创建
lookupIterator = new LazyIterator(service, loader);
}
ServiceLoader 本身实现了 Iterable 接口。遍历时会获取 iterator
.
public Iterator<S> iterator() {
return new Iterator<S>() {
//已经缓存的列表
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext() {
//先从缓存中找
if (knownProviders.hasNext())
return true;
//缓存 MISS ,调用内部 LazyIterator.hasNext()
return lookupIterator.hasNext();
}
public S next() {
//缓存中存在,直接返回
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//缓存 MISS,调用内部 LazyIterator.next()
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
LazyIterator#next()
public S next() {
if (acc == null) {
//查找下一个 Service
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
LazyIterator#hasNextService()
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//查找文件 META-INF/services/com.xx.spi.Human
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
//没有更多元素,遍历结束
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
//给下一个名称赋值
nextName = pending.next();
return true;
}
LazyIterator#nextService()
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
//调用完 hasNextService 方法之后,nextName 更新
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//获取 Class
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
//判断类是否继承指定接口
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//反射创建对象
S p = service.cast(c.newInstance());
//加入缓存,下次访问直接访问缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
小结
可以看出在Java SPI
的实现中,核心代码就是反射创建对象实例。经典的实际应用中可以参考 JDBC的Driver实现。这里不在赘述。
在 Dubbo相关官方博客 介绍了它的缺点:
- 需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要的实现。
- 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
- 扩展如果依赖其他的扩展,做不到自动注入和装配
- 不提供类似于Spring的IOC和AOP功能
- 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持
Dubbo SPI 实现DEMO
定义一个接口,添加
@SPI
注解@SPI("human")
public interface Human {
@Adaptive
String sayHello(URL url);
}
定义两个实现类
public class American implements Human {
@Override
public String sayHello(URL url) {
return "Hello,I'm American";
}
} public class Chinese implements Human {
@Override
public String sayHello(URL url) {
return "你好,我是中国人";
}
}
新建 接口全名文件,例如本例: com.xx.spi.Human,放到META-INF/dubbo/internal路径下,文件内容为key=value 形式。
//默认实现
human=com.xx.spi.Chinese
//中国
cn=com.xx.spi.Chinese
//美国
en=com.xx.spi.American
编码测试
public class DubboSpiApplication { public static void main(String[] args) {
//根据 human 参数动态获取实现类
URL url = URL.valueOf("test://localhost/test?human=en");
Human humanDefault = ExtensionLoader.getExtensionLoader(Human.class).getDefaultExtension();
Human humanAdaptive = ExtensionLoader.getExtensionLoader(Human.class).getAdaptiveExtension(); //default
System.out.println(humanDefault.sayHello(url));
//adaptive
System.out.println(humanAdaptive.sayHello(url));
}
}
Dubbo SPI 源码解析
ExtensionLoader
ExtensionLoader
内置了各种不同的缓存,以提高性能,所以在后续的代码中会将缓存判断的地方去掉,只保留核心代码
//缓存 ExtensionLoader 实例,每个 Class<?> 对应一个,静态方法 getExtensionLoader 即从该缓存中获取
private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
//缓存 配置文件中的 key value 对象.
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
private final Map<String, Object> cachedActivates = new ConcurrentHashMap<>();
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
private final Holder<Object> cachedAdaptiveInstance = new Holder<>();
private volatile Class<?> cachedAdaptiveClass = null;
ExtensionLoader# getDefaultExtension()
public T getDefaultExtension() {
//做准备工作,扫描配置文件,提取类信息等
getExtensionClasses();
//获取类实例
return getExtension(cachedDefaultName);
}
ExtensionLoader# getExtensionClasses()
private Map<String, Class<?>> getExtensionClasses() {
//省略其他缓存判断和加锁代码,其实就是调用此方法,然后放入 cachedClasses 中
return loadExtensionClasses();
}
ExtensionLoader#loadExtensionClasses()
private Map<String, Class<?>> loadExtensionClasses() {
//解析默认名称 例如 @SPI("human") 中的human
cacheDefaultExtensionName();
Map<String, Class<?>> extensionClasses = new HashMap<>();
//从 META-INF/dubbo/internal/ 目录下加载
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName());
//从 META-INF/dubbo/internal/ 目录下加载 内置的类,例如ExtensionFactory
loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
//从 META-INF/dubbo/ 目录下加载
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName());
loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
//从 META-INF/services/ 目录下加载,兼容Java SPI 方式
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName());
loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba"));
return extensionClasses;
}
ExtensionLoader#loadResource()
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) {
try {
//读取文件
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) {
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) {
//解析出 key value ,key为别名 value 为类的全名
name = line.substring(0, i).trim();
line = line.substring(i + 1).trim();
}
if (line.length() > 0) {
loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name);
}
}
//...其他代码
}
此时,cachedClasses 已经存放若干类信息。
ExtensionLoader#getExtension()
public T getExtension(String name) {
//忽略缓存部分代码,核心就是调用此方法进行类的实例化
return (T)createExtension(name);
}
ExtensionLoader#createExtension()
private T createExtension(String name) {
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw findException(name);
}
try {
//从缓存获取实例对象
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
//缓存MISS,调用 newInstance() 方法实例化,存入缓存
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
}
//做后续的注入工作(下文解析)
injectExtension(instance);
//包装类构造函数注入(下文解析)
Set<Class<?>> wrapperClasses = cachedWrapperClasses;
if (CollectionUtils.isNotEmpty(wrapperClasses)) {
for (Class<?> wrapperClass : wrapperClasses) {
instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
return instance;
} catch (Throwable t) {
}
}
所以到了这一步,就很明了了,其实核心代码还是通过反射的方式newInstance
创建对象实例,然后存入缓存。但是后续又做了一些注入工作。功能性要比 JAVA SPI
丰富一些。
ExtensionLoader#injectExtension()
private T injectExtension(T instance) {
try {
if (objectFactory != null) {
//遍历所有的方法
for (Method method : instance.getClass().getMethods()) {
//如果是set方法
if (isSetter(method)) {
//方法存在 DisableInject 注解,不注入
if (method.getAnnotation(DisableInject.class) != null) {
continue;
}
//获取参数类型
Class<?> pt = method.getParameterTypes()[0];
//如果是基本类型的参数,跳过
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
//获取属性名称
String property = getSetterProperty(method);
//通过 Factory 获取 扩展实例。
Object object = objectFactory.getExtension(pt, property);
//获取到了就执行 set 方法
if (object != null) {
method.invoke(instance, object);
}
} catch (Exception e) {
logger.error("Failed to inject via method " + method.getName()
+ " of interface " + type.getName() + ": " + e.getMessage(), e);
}
}
}
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
return instance;
}
ExtensionLoader#getAdaptiveExtension()
public T getAdaptiveExtension() {
//除去其他判断代码,核心方法
return (T)createAdaptiveExtension();
}
ExtensionLoader#createAdaptiveExtension()
private T createAdaptiveExtension() {
//忽略 injectExtension方法,主要是看AdaptiveExtensionClass如何获取
return injectExtension((T) getAdaptiveExtensionClass().newInstance());
}
ExtensionLoader#getAdaptiveExtensionClass()
private Class<?> getAdaptiveExtensionClass() {
//同样,先加载类信息
getExtensionClasses();
//读取缓存
if (cachedAdaptiveClass != null) {
return cachedAdaptiveClass;
}
// 创建
return cachedAdaptiveClass = createAdaptiveExtensionClass();
}
ExtensionLoader#createAdaptiveExtensionClass()
这个方法比较有意思,会动态创建一个新的类,类名 Class$Adaptive.
private Class<?> createAdaptiveExtensionClass() {
//生成类的字符串代码
String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate();
ClassLoader classLoader = findClassLoader();
//获取内置的 Compiler,它也是通过 ExtensionLoader 生成的实例,可以通过修改配置 <dubbo:application compiler="jdk"/> 选择使用哪一种。Dubbo支持 JDK,Javassist,AdaptiveCompiler
org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension();
//编译生成类
return compiler.compile(code, classLoader);
}
默认使用 javassist
@SPI("javassist")
public interface Compiler {
/**
* Compile java source code.
*
* @param code Java source code
* @param classLoader classloader
* @return Compiled class
*/
Class<?> compile(String code, ClassLoader classLoader);
}
类结构图如下:
JavassistCompiler
和 JdkCompiler
是真正做事的,而AdaptiveCompiler
则是为了实现动态选择编译器而生的。
@Override
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) {
//根据 name 获取编译器
compiler = loader.getExtension(name);
} else {
//获取默认编译器,即 @SPI 注解标明的编译器,默认是 javassist
compiler = loader.getDefaultExtension();
}
return compiler.compile(code, classLoader);
}
那么,编译器编译的代码是什么呢?我将代码格式稍微整理了一下
//包名
package com.xx.spi;
//导入
import org.apache.dubbo.common.extension.ExtensionLoader;
//生成的类名 ClassName+ $ + Adaptive
public class Human$Adaptive implements com.fanpan26.jls.spi.Human {
public java.lang.String sayHello(org.apache.dubbo.common.URL arg0) {
//参数校验
if (arg0 == null) throw new IllegalArgumentException("url == null");
org.apache.dubbo.common.URL url = arg0;
//获取参数
String extName = url.getParameter("human", "human");
//如果没有获取到,说明配置文件里没有,抛出异常
if (extName == null) {
throw new IllegalStateException("Failed to get extension (com.fanpan26.jls.spi.Human) name from url (" + url.toString() + ") use keys([human])");
}
//最终还是调用了 getExtension(String name); 方法获取实例
com.fanpan26.jls.spi.Human extension = ExtensionLoader.getExtensionLoader(com.fanpan26.jls.spi.Human.class).getExtension(extName);
//最后调用该实例的方法
return extension.sayHello(arg0);
}
}
所以,到此为止, getExtension(String name) 是个核心方法,很多地方都会用到他。即便是 Adaptive
类,最终生成的类方法中也是调用了该方法。
ExtensionLoader#getActivateExtension()
此方法也是大同小异,不在赘述。
总结
简单的做了一下 JAVA SPI 和Dubbo SPI 的代码解析,也有很多细节没有分析。相比于JAVA SPI,Dubbo SPI提供了更加丰富的功能和更高的灵活性。Dubbo SPI 在Dubbo框架中的应用到处可见,所以它的重要性不言而喻。通过代码的编写与调试,让我收获良多。分析不正确的地方还请多多谅解和批评指正。
【Dubbo】带着问题看源码:什么是SPI机制?Dubbo是如何实现的?的更多相关文章
- dubbo源码分析5——SPI机制_AdaptiveExtension的原理和作用
private T createAdaptiveExtension() { try { return injectExtension((T) getAdaptiveExtensionClass().n ...
- dubbo源码分析4——SPI机制_ExtensionFactory类的作用
ExtensionFactory的源码: @SPI public interface ExtensionFactory { /** * Get extension. * * @param type o ...
- dubbo源码分析1——SPI机制的概要介绍
插件机制是Dubbo用于可插拔地扩展底层的一些实现而定制的一套机制,比如dubbo底层的RPC协议.注册中心的注册方式等等.具体的实现方式是参照了JDK的SPI思想,由于JDK的SPI的机制比较简单, ...
- dubbo源码分析2——SPI机制中的SPI实现类的读取和预处理
SPI机制中的SPI实现类的读取和预处理是由ExtensionLoader类的loadFile方法来完成的 loadFile方法的作用是读取dubbo的某个SPI接口的spi描述文件,然后进行缓存,缓 ...
- 基于dubbo源码包通过Maven构建dubbo的详细步骤
通过Maven构建dubbo 既然可以下载得到源码以及发布包,那么为什么要去构建dubbo呢?,我们先来看下dubbo的主要模块: 我们不仅要使用dubbo的核心框架,还要使用它的一些服务,比如管理控 ...
- 专治不会看源码的毛病--spring源码解析AOP篇
昨天有个大牛说我啰嗦,眼光比较细碎,看不到重点.太他爷爷的有道理了!要说看人品,还是女孩子强一些.原来记得看到一个男孩子的抱怨,说怎么两人刚刚开始在一起,女孩子在心里就已经和他过完了一辈子.哥哥们,不 ...
- Spring AOP源码解析——专治你不会看源码的坏毛病!
昨天有个大牛说我啰嗦,眼光比较细碎,看不到重点.太他爷爷的有道理了!要说看人品,还是女孩子强一些. 原来记得看到一个男孩子的抱怨,说怎么两人刚刚开始在一起,女孩子在心里就已经和他过完了一辈子.哥哥们, ...
- 没必要看源码。。把文档学通就已经牛逼了(我们大多还是在应用层,还达不到研究的程度。附class与examples大全链接)
[学霸]深圳-鑫 2017/7/11 13:54:07只是学习怎么用QT的话,不用看源码.看帮助文档就很好要学习编码风格与思路,就看看源码 [学神]武汉-朝菌 2017/7/11 13:54:39没必 ...
- Dubbo源码分析之 SPI(一)
一.概述 dubbo SPI 在dubbo的作用是基础性的,要想分析研究dubbo的实现原理.dubbo源码,都绕不过 dubbo SPI,掌握dubbo SPI 是征服dubbo的必经之路. 本篇文 ...
随机推荐
- 下载安装Tomcat教程
注:由于我的笔记不知怎么滴不能复制粘贴我就直接贴图了
- JavaWeb第三天--JavaScript
JavaScript 1. JavaScript概述 1.1 JavaScript是什么?有什么作用? HTML:就是用来写网页的.(人的身体) CSS:就是用来美化页面的.(人的衣服) JavaSc ...
- 下拉框等需要显示上下箭头切换的CSS样式
下拉框等需要显示上下箭头切换的CSS样式 .icon-right, .icon-down, .icon-up { display: inline-block; padding-right: 13r ...
- 英语hawkbillturtle玳瑁
玳瑁(hawkbillturtle):属爬行纲,海龟科的海洋动物.一般长约0.6米,大者可达1.6米.头顶有两对前额鳞,吻部侧扁,上颚前端钩曲呈鹰嘴状:前额鳞2对:背甲盾片呈覆瓦状排列:背面的角质板覆 ...
- ApplicationContext的名称解释
如果说BeanFactory是Spring的心脏,那么Application就是完整的身躯.ApplicationContext就是由BeanFactory派生出来的. 1.ApplicationCo ...
- CDA数据分析【第二章:数据收集与导入】
一.概述 数据是对我们所研究现象的属性和特征的具体描述,在分析数据前必须要做的工作就是收集数据.按照存储形式可以将数据划分为结构化数据.非结构化数据和半结构化数据. 1.结构化数据 能够用数据或统一的 ...
- linux shell的输出效果
在linux系统命令行界面默认目录颜色是蓝色,在黑色底色上无法看清 原来效果图: 最终效果图: 大致步骤:1.复制配置文件到个人用户的根目录下2.修改配置文件中字体颜色的设置3.重新启动窗口,输入ls ...
- Odoo销售模块
转载请注明原文地址:https://www.cnblogs.com/ygj0930/p/10825988.html 一:销售模块 销售模块的用途: 1)管理销售团队.销售人员:维护销售产品: 2)管理 ...
- 树莓派3b+更改静态IP
ubuntu系统修改静态IP的方法是在修改/etc/network/interfaces文件,而树莓派此文件下有说明: # interfaces() ) and ifdown() # Please n ...
- 【解决】Error: ENOSPC: no space left on device, watch
发现问题: 启动 node 项目ReactNative时候出现报错Error: ENOSPC: no space left on device, watch [root@iz2zeihk6kfcls5 ...