参考:

敖丙《Dubbo系列》-Dubbo SPI机制

 

基本组成:

 

Client (客户端):服务调用方。
Server(服务端):服务提供方。
Client Stub(客户端存根):存放服务端的消息,负责将客户端的请求参数打包成网络消息,然后通过网络发送给服务提供方。
Server Stub(服务端存根):接收客户端发送的消息,再将客户端请求参数打包成网络消息,然后通过网络远程发送给服务方。

其它:

 

  • 序列化协议:
  • 网络协议:TCP, HTTP 等(TCP 更快(Dubbo),因为 HTTP header 内容过多(SpringCloud))
  • 注册中心:Zookeeper CP 保证一致性,Eureka AP 保证可用性

 

Dubbo

zookeeper 作为注册中心:

  • 提供者节点: /GLOBAL_REGISTRY/tuling.dubbo.server.UserService/providers
    • 存放的内容是一个所有注册的提供者的 URL List  [dubbo://127.0.0.1:12345/tuling.dubbo.server.UserService?application=myapp1&dubbo=2.0.2&protocal=dubbo&..., dubbo://..... ]
    • 在提供者中进行负载均衡
  • 消费者节点:/GLOBAL_REGISTRY/tuling.dubbo.server.UserService/consumers
    • 存放的内容是一个所有注册的提供者的 URL List  [consumer://127.0.0.1:12345/tuling.dubbo.server.UserService?application=myapp2&dubbo=2.0.2&protocal=dubbo&..., consumer://..... ]
  • 提供者动态配置节点:/GLOBAL_REGISTRY/tuling.dubbo.server.UserService/configurators
  • 消费者路由策略节点:/GLOBAL_REGISTRY/tuling.dubbo.server.UserService/routers

网络协议是 TCP

序列化协议是 Hessian

Dubbo序列化支持java、compactedjava、nativejava、fastjson、dubbo、fst、hessian2、kryo,其中默认hessian2。其中java、compactedjava、nativejava属于原生java的序列化。

  • dubbo 序列化:阿里尚未开发成熟的高效java序列化实现,阿里不建议在生产环境使用它。
  • hessian2 序列化:hessian是一种跨语言的高效二进制序列化方式。但这里实际不是原生的hessian2序列化,而是阿里修改过的,它是dubbo RPC默认启用的序列化方式。
  • json 序列化:目前有两种实现,一种是采用的阿里的fastjson库,另一种是采用dubbo中自己实现的简单json库,但其实现都不是特别成熟,而且 json这种文本序列化性能一般不如上面两种二进制序列化。
  • java 序列化:主要是采用JDK自带的Java序列化实现,性能很不理想。

JDK Serializable中通过serialVersionUID控制序列化类的版本,如果序列化与反序列化版本不一致,则会抛出java.io.InvalidClassException异常信息,提示序列化与反序列化SUID不一致。

dubbo序列化主要由Serialization(序列化策略)、DataInput(反序列化,二进制->对象)、DataOutput(序列化,对象->二进制流) 来进行数据的序列化与反序列化。

 

SPI 机制

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类这样可以在运行时,动态为接口替换实现类。

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。

 Dubbo 对配置文件目录的约定,不同于 Java SPI ,Dubbo 分为了三类目录。

  • META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。

  • META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。

  • META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。

Java SPI

就是约定一个目录,根据接口名去那个目录找到文件,文件解析得到实现类的全限定名,然后循环加载实现类和创建其实例。

想一下 Java SPI 哪里不好

相信大家一眼就能看出来,Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。

所以说 Java SPI 无法按需加载实现类。

Dubbo SPI 示例

robot 接口及它的两个实例

需要在 Robot 接口上标注 @SPI 注解。

@SPI
public interface Robot {
    void sayHello();
}

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

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.");
    }
}

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

# robot 接口实现类1
optimusPrime = org.apache.spi.OptimusPrime
# robot 接口实现类2 bumblebee
= org.apache.spi.Bumblebee

下面来演示 Dubbo SPI 的用法:

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

测试结果如下:

源码分析:

createExtension 方法的逻辑稍复杂一下,包含了如下的步骤:

  1. 通过 getExtensionClasses 获取所有的拓展类名称
  2. 通过反射创建拓展对象
  3. 向拓展对象中注入依赖
  4. 将拓展对象包裹在相应的 Wrapper 对象中