Dubbo SPI 简介

引言

前面,我们已经介绍了 Dubbo 设计上的一些思想,本文主要介绍 Dubbo 在 SPI(Service Provider Interface)上的一些改进,其他 Dubbo 相关文章均收录于 <Dubbo系列文章>

SPI

我们知道Dubbo的设计原则是微内核+富扩展,它的内核部分就是将各个模块组装起来,而各个模块都抽象称为接口,这样替换任意模块都非常方便。接下来就让我们一起来看一看Dubbo的扩展点是如何设计的。

来源

Dubbo 的扩展点加载从 JDK 标准的 SPI (Service Provider Interface) 扩展点发现机制加强而来。

Dubbo 改进了 JDK 标准的 SPI 的以下问题:

  • JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。
  • 增加了对扩展点 IoC 和 AOP 的支持,一个扩展点可以直接 setter 注入其它扩展点。

Java SPI示例

首先,我们定义一个接口,名称为 Robot。

1
2
3
public interface Robot {
void sayHello();
}

接下来定义两个实现类,分别为 OptimusPrime 和 Bumblebee。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class OptimusPrime implements Robot {

@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}

public class Bumblebee implements Robot {

@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}

接下来 META-INF/services 文件夹下创建一个文件,名称为 Robot 的全限定名 org.apache.spi.Robot。文件内容为实现类的全限定的类名,如下:

1
2
org.apache.spi.OptimusPrime
org.apache.spi.Bumblebee

做好所需的准备工作,接下来编写代码进行测试。

1
2
3
4
5
6
7
8
9
public class JavaSPITest {

@Test
public void sayHello() throws Exception {
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
System.out.println("Java SPI");
serviceLoader.forEach(Robot::sayHello);
}
}

最后来看一下测试结果,如下:
java-spi-result
从测试结果可以看出,我们的两个实现类被成功的加载,并输出了相应的内容。关于 Java SPI 的演示先到这里,接下来演示 Dubbo SPI。

Dubbo 约定

在扩展类的 jar 包内,放置扩展点配置文件META-INF/dubbo/接口全限定名,内容为:配置名=扩展实现类全限定名,多个实现类用换行符分隔。Dubbo 会全 ClassPath 扫描所有 jar 包内同名的这个文件,然后进行合并。

此外,在Dubbo中一次使用只会实例化指定的实现类,并不会像Java SPI中那样一次性实例化所有的实现类,相比而言Dubbo这种实现在性能上更具优势。

Dubbo SPI示例

Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下。

1
2
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee

与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外,在测试 Dubbo SPI 时,需要在 Robot 接口上标注 @SPI 注解。下面来演示 Dubbo SPI 的用法:

1
2
3
4
5
6
7
8
9
10
11
12
public class DubboSPITest {

@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
}
}

测试结果如下:
dubbo-spi-result
Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性,这些内容我们会后再面一一介绍。

特性

Dubbo的SPI除了上述的根据配置信息使用特定实现类这个核心功能外,还具有四种额外的特性,它们分别是自动包装、自动装配、自动适应、自动激活,接下来我们就分别介绍一下这四大特性。

自动包装

自动包装对应的是扩展点的 Wrapper 类。ExtensionLoader 在加载扩展点时,如果加载到的扩展点有拷贝构造函数,则判定为扩展点 Wrapper 类。所谓,拷贝构造函数是指实现类以自己实现的接口作为构造函数的参数,也就是Wrapper类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.alibaba.xxx;

import org.apache.dubbo.rpc.Protocol;

public class XxxProtocolWrapper implements Protocol {
Protocol impl;

public XxxProtocolWrapper(Protocol protocol) { impl = protocol; }

// 接口方法做一个操作后,再调用extension的方法
public void refer() {
//... 一些操作
impl.refer();
// ... 一些操作
}

// ...
}

Wrapper 类同样实现了扩展点接口,但是 Wrapper 不是扩展点的真正实现。它的用途主要是用于从 ExtensionLoader 返回扩展点时,包装在真正的扩展点实现外。即从 ExtensionLoader 中返回的实际上是 Wrapper 类的实例,Wrapper 持有了实际的扩展点实现类。

扩展点的 Wrapper 类可以有多个,也可以根据需要新增。通过 Wrapper 类可以把所有扩展点公共逻辑移至 Wrapper 中。新加的 Wrapper 在所有的扩展点上添加了逻辑,有些类似 AOP,即 Wrapper 代理了扩展点。

自动装配

加载扩展点时,自动注入依赖的扩展点。加载扩展点时,扩展点实现类的成员如果为其它扩展点类型,ExtensionLoader 在会自动注入依赖的扩展点。ExtensionLoader 通过扫描扩展点实现类的所有 setter 方法来判定其成员。即 ExtensionLoader 会执行扩展点的拼装操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface CarMaker {
Car makeCar();
}

public interface WheelMaker {
Wheel makeWheel();
}

public class RaceCarMaker implements CarMaker {
WheelMaker wheelMaker;

public setWheelMaker(WheelMaker wheelMaker) {
this.wheelMaker = wheelMaker;
}

public Car makeCar() {
// ...
Wheel wheel = wheelMaker.makeWheel();
// ...
return new RaceCar(wheel, ...);
}
}

ExtensionLoader 加载 CarMaker 的扩展点实现 RaceCarMaker 时,setWheelMaker 方法的 WheelMaker 也是扩展点则会注入 WheelMaker 的实现。

这里带来另一个问题,ExtensionLoader 要注入依赖扩展点时,如何决定要注入依赖扩展点的哪个实现。在这个示例中,即是在多个WheelMaker 的实现中要注入哪个。我们知道在Spring中,是通过引用时指定bean name来进行指定的,但是在dubbo中,因为各个模块的实现都是根据配置信息来指定的,接下来要介绍的自动适应特性就是对应了这部分的功能。

自动适应

ExtensionLoader 注入的依赖扩展点是一个 Adaptive 实例,直到扩展点方法执行时才决定调用是一个扩展点实现。

Dubbo 使用 URL 对象(包含了Key-Value)传递配置信息。

扩展点方法调用会有URL参数(或是参数有URL成员)

这样依赖的扩展点也可以从URL拿到配置信息,所有的扩展点自己定好配置的Key后,配置信息从URL上从最外层传入。URL在配置传递上即是一条总线。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public interface CarMaker {
Car makeCar(URL url);
}

public interface WheelMaker {
Wheel makeWheel(URL url);
}

public class RaceCarMaker implements CarMaker {
WheelMaker wheelMaker;

public setWheelMaker(WheelMaker wheelMaker) {
this.wheelMaker = wheelMaker;
}

public Car makeCar(URL url) {
// ...
Wheel wheel = wheelMaker.makeWheel(url);
// ...
return new RaceCar(wheel, ...);
}
}

上面的的代码中我们可以看到makeCar多了一个URL参数,这个URL就是前面所说的配置信息,Dubbo将配置信息转化为URL的形式存储。

注入的 Adaptive 实例可以提取约定 Key 来决定使用哪个 WheelMaker 实现来调用对应实现的真正的 makeWheel 方法。如提取 Wheel.maker, key 即 url.get(“Wheel.maker”) 来决定 WheelMake 实现。Adaptive 实例的逻辑是固定,指定提取的 URL 的 Key,即可以代理真正的实现类上,可以动态生成。

WheelMaker 接口的自适应实现类如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class AdaptiveWheelMaker implements WheelMaker {
public Wheel makeWheel(URL url) {
if (url == null) {
throw new IllegalArgumentException("url == null");
}

// 1.从 URL 中获取 WheelMaker 名称
String wheelMakerName = url.getParameter("Wheel.maker");
if (wheelMakerName == null) {
throw new IllegalArgumentException("wheelMakerName == null");
}

// 2.通过 SPI 加载具体的 WheelMaker
WheelMaker wheelMaker = ExtensionLoader
.getExtensionLoader(WheelMaker.class).getExtension(wheelMakerName);

// 3.调用目标方法
return wheelMaker.makeWheel(url);
}
}

AdaptiveWheelMaker 是一个代理类,与传统的代理逻辑不同,AdaptiveWheelMaker 所代理的对象是在 makeWheel 方法中通过 SPI 加载得到的。makeWheel 方法主要做了三件事情:

  1. 从 URL 中获取 WheelMaker 名称
  2. 通过 SPI 加载具体的 WheelMaker 实现类
  3. 调用目标方法

RaceCarMaker 持有一个 WheelMaker 类型的成员变量,在程序启动时,我们可以将 AdaptiveWheelMaker 通过 setter 方法注入到 RaceCarMaker 中。在运行时,假设有这样一个 url 参数传入:

1
dubbo://192.168.0.101:20880/XxxService?wheel.maker=MichelinWheelMaker

RaceCarMaker 的 makeCar 方法将上面的 url 作为参数传给 AdaptiveWheelMaker 的 makeWheel 方法,makeWheel 方法从 url 中提取 wheel.maker 参数,得到 MichelinWheelMaker。之后再通过 SPI 加载配置名为 MichelinWheelMaker 的实现类,得到具体的 WheelMaker 实例。

在 Dubbo 的 ExtensionLoader 的扩展点类对应的 Adaptive 实现是在加载扩展点里动态生成。指定提取的 URL 的 Key 通过 @Adaptive 注解在接口方法上提供。

1
2
3
4
5
6
7
public interface Transporter {
@Adaptive({"server", "transport"})
Server bind(URL url, ChannelHandler handler) throws RemotingException;

@Adaptive({"client", "transport"})
Client connect(URL url, ChannelHandler handler) throws RemotingException;
}

对于 bind() 方法,Adaptive 实现先查找 server key,如果该 Key 没有值则找 transport key 值,来决定代理到哪个实际扩展点。

自动激活

对于集合类扩展点,比如:Filter, InvokerListener, ExportListener, TelnetHandler, StatusChecker 等,可以同时加载多个实现,此时,可以用自动激活来简化配置,如:

1
2
3
4
5
6
7
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;

@Activate // 无条件自动激活
public class XxxFilter implements Filter {
// ...
}
1
2
3
4
5
6
7
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;

@Activate("xxx") // 当配置了xxx参数,并且参数为有效值时激活,比如配了cache="lru",自动激活CacheFilter。
public class XxxFilter implements Filter {
// ...
}
1
2
3
4
5
6
7
import org.apache.dubbo.common.extension.Activate;
import org.apache.dubbo.rpc.Filter;

@Activate(group = "provider", value = "xxx") // 只对提供方激活,group可选"provider"或"consumer"
public class XxxFilter implements Filter {
// ...
}

Dubbo中可扩展的接口

  • 协议
  • 调用拦截
  • 引用监听
  • 暴露监听
  • 集群
  • 路由
  • 负载均衡
  • 合并结果
  • 注册中心
  • 监控中心
  • 扩展点加载
  • 动态代理
  • 编译器
  • 消息派发
  • 线程池
  • 序列化
  • 网络传输
  • 信息交换
  • 组网
  • Telnet
  • 状态检查
  • 容器
  • 页面
  • 缓存
  • 验证
  • 日志适配
  • 配置中心

参考内容

[1]《深入理解Apache Dubbo与实战》
[2] dubbo 官方文档

贝克街的流浪猫 wechat
您的打赏将鼓励我继续分享!
  • 本文作者: 贝克街的流浪猫
  • 本文链接: https://www.beikejiedeliulangmao.top/middleware/dubbo/spi/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 创作声明: 本文基于上述所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。