写在前面:相信每个程序员都有一颗想看源码的心,但是经常被源码的复杂所吓倒,很多人都停留在想的层面。最近刚好没有什么事情,就打算自己看一下源码。选择dubbo来阅读的原因在于一直对Dubbo有好感,毕竟阿里出品必是精品,虽然Dubbo也曾经历过停更的风波,但好在后面又更新啦,文章使用的版本是2.5.4。GitHub链接https://github.com/apache/dubbo。选择tags的2.5.4。
导入idea中,可以参考https://blog.csdn.net/fenfenguai/article/details/80611648,https://blog.csdn.net/hpchenqi_16/article/details/80955546步骤不完全一样
下面结合自己看的一些博客和自己的理解和大家分享一下Dubbo中无处不在的SPI(Service Provider Interface),SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。为什么要先介绍SPI呢?因为在看 dubbo 源代码时如果没有了解扩展点机制,那么看到代码就是一片凌乱。源码中大量运用了SPI。写的不好的地方也请大家包容。声明:内容存在引用一些博主的博客如下(当然还有参考一些别的细节知识点,如设计模式等知识,这里不再列出)。
https://juejin.im/post/5c0cd73b5188256dd7744858
https://www.jianshu.com/p/99f568df0f05
一、SPI 示例
1.1 Java SPI示例
首先定义一个接口,再定义两个实现类
public interface IService {
public String sayHello();
public String getScheme();
}
public class HBaseServiceImpl implements IService {
@Override
public String sayHello() {
return "Hello HBase!!";
}
@Override
public String getScheme() {
return "HBase";
}
}
public class HDFSServiceImpl implements IService {
@Override
public String sayHello() {
return "Hello HDFS!!";
}
@Override
public String getScheme() {
return "hdfs";
}
}
public class ServiceLoaderTest {
public static void main(String[] args) {
ServiceLoader<IService> serviceLoader = ServiceLoader.load(IService.class);
for (IService iService : serviceLoader) {
System.out.println(iService.getScheme()+" = "+iService.sayHello());
}
}
这里做些解释:java.util.ServiceLoader这个类来从配置文件中加载子类或者接口的实现类。
主要是从META-INF/services这个目录下的配置文件加载给定接口或者基类的实现,
ServiceLoader会根据给定的类的full name来在META-INF/services下面找对应的文件,
在这个文件中定义了所有这个类的子类或者接口的实现类,返回一个实例。
尝试一下,用#注释和不注释运行结果的区别。
//可以看到ServiceLoader可以根据IService把定义的两个实现类找出来,返回一个ServiceLoader的实现,
//而ServiceLoader实现了Iterable接口,所以可以通过ServiceLoader来遍历所有在配置文件中定义的类的实例。
//从使用层面来说,就是运行时,动态给接口添加实现类.其实这有有点像IoC的思想,将装配的控制权移到程序之外.通过改变配置文件,我们就能动态的改变一个接口的实现类.
//SPI 就是这样一种基于接口编程+策略模式+配置文件,同时可供使用者根据自己的实际需要启用/替换模块具体实现的方案。
//那么很容易想到一个问题,比如我想新增一个接口的实现类XXXServiceImpl,这样的话光改配置文件也还是不行,还要预先包里面就有这个实现类才行啊.
//这就要用到javassist,也就是动态字节码技术.这样可以在运行时动态生成Java类,就不存在要预先把接口的实现类先在包里放好
//事实上我就算不用spi,我用spring的ioc也能通过配置文件或者注解动态的注入不同的实现类啊
//在dubbo的设计中,就不想强依赖Spring的IoC容器,但是自已造一个小的IoC容器,也觉得有点过度设计.另外dubbo是不需要依赖任何第三方库的,引用官方文档原话如下
//理论上 Dubbo 可以只依赖 JDK,不依赖于任何三方库运行,只需配置使用 JDK 相关实现策略
1.2 Dubbo SPI示例
首先定义一个接口,再定义两个实现类。注:这里我是在源码中的dubbo-test包下写的。新建工程写可能报错。
package com.alibaba.dubbo.examples.spi;
import com.alibaba.dubbo.common.extension.SPI;
@SPI
public interface Robot {
void sayHello();
}
这里可以看到有@SPI注解,可以自己尝试去掉会有什么现象,这里我先给出来
package com.alibaba.dubbo.examples.spi.impl;
import com.alibaba.dubbo.examples.spi.Robot;
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}
package com.alibaba.dubbo.examples.spi.impl;
import com.alibaba.dubbo.examples.spi.Robot;
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}
package com.alibaba.dubbo.examples.spi;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class DubboSPITest {
public static void main(String[] args) {
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}
Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下。
optimusPrime = com.alibaba.dubbo.examples.spi.impl.OptimusPrime bumblebee = com.alibaba.dubbo.examples.spi.impl.Bumblebee
代码结构及结果截图如下.
Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性,这些特性将会在接下来的源码分析章节中一一进行介绍。
Dubbo SPI 的改进点
以下内容摘录自 https://dubbo.gitbooks.io/dubbo-dev-book/SPI.html
Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。 JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点
上面这段话,暂时看不懂也没关系,我们先放在这,到时候回过头来看也是可以的。
在 Dubbo 中,如果某个 interface 接口标记了 @SPI 注解,那么我们认为它是 Dubbo 中的一个扩展点。扩展点是 Dubbo SPI 的核心。
Dubbo SPI 机制详解
Dubbo 扩展点的加载
上文说到Java SPI有 /META-INF/services 这样一个目录。在这个目录下有一个以接口命名的文件,文件的内容为接口具体实现类的全限定名。在 Dubbo 中我们也能找到类似的设计。
- META-INF/services/(兼容JAVA SPI)
- META-INF/dubbo/(自定义扩展点实现)
- META-INF/dubbo/internal/(Dubbo内部扩展点实现)
非常好~我们现在已经知道了从哪里加载扩展点了,再回忆一下,JAVA SPI是如何加载的。
ServiceLoader<DubboService> spiLoader = ServiceLoader.load(XXX.class);
类似的,在 Dubbo 中也有这样一个用于加载扩展点的类 ExtensionLoader,这个是不是跟 SPI 机制非常像,只是 java spi 机制是 java 后台帮你实现读取文件并对接具体的实现类,而 dubbo 是自己去读文件。我们先来看一段简短的代码。
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
ProxyFactory proxyFactory = ExtensionLoader.getExtensionLoader(ProxyFactory.class).getAdaptiveExtension();
MonitorFactory monitorFactory = ExtensionLoader.getExtensionLoader(MonitorFactory.class).getAdaptiveExtension();
RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension();
getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。在 Dubbo 的实现里面用到了大量类似的代码片段,我们只需要提供一个 type ,即可获取该 type 的自适应(关于自适应的理解在后文会提到)扩展类。在获取对应自适应扩展类时,我们首先获取该类型的 ExtensionLoader。看到这里我们应该下意识的感觉到对于每个 type 来说,都应该有一个对应的 ExtensionLoader 对象。我们先来看看 ExtensionLoader 是如何获取的。很自然想到应该是getXXX方法。
getExtensionLoader()
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("xxx")注解标识
if (!withExtensionAnnotation(type)) {//需要添加spi注解,否则抛异常
throw new IllegalArgumentException("Extension type(" + type +
") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");