Dubbo 源码分析
1. Dubbo 的扩展机制
在 Dubbo 的官网上, Dubbo 描述自己是一个高性能的 RPC 框架. 今天我想聊聊 Dubbo 的另一个很棒的特性, 就是它的可扩展性. 如同罗马不是一天建成的, 任何系统都一定是从小系统不断发展成为大系统的, 想要从一开始就把系统设计的足够完善是不可能的, 相反的, 我们应该关注当下的需求, 然后再不断地对系统进行迭代. 在代码层面, 要求我们适当的对关注点进行抽象和隔离, 在软件不断添加功能和特性时, 依然能保持良好的结构和可维护性, 同时允许第三方开发者对其功能进行扩展. 在某些时候, 软件设计者对扩展性的追求甚至超过了性能.
在谈到软件设计时, 可扩展性一直被谈起, 那到底什么才是可扩展性, 什么样的框架才算有良好的可扩展性呢? 它必须要做到以下两点:
作为框架的维护者, 在添加一个新功能时, 只需要添加一些新代码, 而不用大量的修改现有的代码, 即符合开闭原则.
作为框架的使用者, 在添加一个新功能时, 不需要去修改框架的源码, 在自己的工程中添加代码即可. Dubbo 很好的做到了上面两点. 这要得益于 Dubbo 的微内核 + 插件的机制. 接下来的章节中我们会慢慢揭开 Dubbo 扩展机制的神秘面纱.
2. 可扩展的几种解决方案
通常可扩展的实现有下面几种:
Factory 模式
IoC 容器
OSGI 容器 Dubbo 作为一个框架, 不希望强依赖其他的 IoC 容器, 比如 Spring,Guice.OSGI 也是一个很重的实现, 不适合 Dubbo. 最终 Dubbo 的实现参考了 Java 原生的 SPI 机制, 但对其进行了一些扩展, 以满足 Dubbo 的需求.
3.Java SPI 机制
既然 Dubbo 的扩展机制是基于 Java 原生的 SPI 机制, 那么我们就先来了解下 Java SPI 吧. 了解了 Java 的 SPI, 也就是对 Dubbo 的扩展机制有一个基本的了解. 如果对 Java SPI 比较了解的同学, 可以跳过.
Java SPI(Service Provider Interface) 是 JDK 内置的一种动态加载扩展点的实现. 在 ClassPath 的 META-INF/services 目录下放置一个与接口同名的文本文件, 文件的内容为接口的实现类, 多个实现类用换行符分隔. JDK 中使用 java.util.ServiceLoader 来加载具体的实现. 让我们通过一个简单的例子, 来看看 Java SPI 是如何工作的.
1. 定义一个接口 IRepository 用于实现数据储存
public interface IRepository { void save(String data); }
2. 提供 IRepository 的实现 IRepository 有两个实现. MysqlRepository 和 MongoRepository.
- public class MysqlRepository implements IRepository {
- public void save(String data) {
- System.out.println("Save" + data + "to Mysql");
- }
- }
- public class MongoRepository implements IRepository {
- public void save(String data) {
- System.out.println("Save" + data + "to Mongo");
- }
- }
3. 添加配置文件 在 META-INF/services 目录添加一个文件, 文件名和接口全名称相同, 所以文件是 META-INF/services/com.demo.IRepository. 文件内容为:
com.demo.MongoRepository com.demo.MysqlRepository
4. 通过 ServiceLoader 加载 IRepository 实现
- ServiceLoader<IRepository> serviceLoader = ServiceLoader.load(IRepository.class);
- Iterator<IRepository> it = serviceLoader.iterator();
- while (it != null && it.hasNext()){
- IRepository demoService = it.next();
- System.out.println("class:" + demoService.getClass().getName());
- demoService.save("tom");
- }
在上面的例子中, 我们定义了一个扩展点和它的两个实现. 在 ClassPath 中添加了扩展的配置文件, 最后使用 ServiceLoader 来加载所有的扩展点. 最终的输出结果为:
- class:testDubbo.MongoRepository Save tom to Mongo
- class:testDubbo.MysqlRepository Save tom to MySQL
4.Dubbo 的 SPI 机制
Java SPI 的使用很简单. 也做到了基本的加载扩展点的功能. 但 Java SPI 有以下的不足:
需要遍历所有的实现, 并实例化, 然后我们在循环中才能找到我们需要的实现.
配置文件中只是简单的列出了所有的扩展实现, 而没有给他们命名. 导致在程序中很难去准确的引用它们.
扩展如果依赖其他的扩展, 做不到自动注入和装配
不提供类似于 Spring 的 IoC 和 AOP 功能
扩展很难和其他的框架集成, 比如扩展里面依赖了一个 Spring bean, 原生的 Java SPI 不支持
所以 Java SPI 应付一些简单的场景是可以的, 但对于 Dubbo, 它的功能还是比较弱的. Dubbo 对原生 SPI 机制进行了一些扩展. 接下来, 我们就更深入地了解下 Dubbo 的 SPI 机制.
5.Dubbo 扩展点机制基本概念
在深入学习 Dubbo 的扩展机制之前, 我们先明确 Dubbo SPI 中的一些基本概念. 在接下来的内容中, 我们会多次用到这些术语.
5.1 扩展点 (Extension Point)
是一个 Java 的接口.
5.2 扩展 (Extension)
扩展点的实现类.
5.3 扩展实例 (Extension Instance)
扩展点实现类的实例.
5.4 扩展自适应实例 (Extension Adaptive Instance)
第一次接触这个概念时, 可能不太好理解 (我第一次也是这样的...). 如果称它为扩展代理类, 可能更好理解些. 扩展的自适应实例其实就是一个 Extension 的代理, 它实现了扩展点接口. 在调用扩展点的接口方法时, 会根据实际的参数来决定要使用哪个扩展. 比如一个 IRepository 的扩展点, 有一个 save 方法. 有两个实现 MysqlRepository 和 MongoRepository.IRepository 的自适应实例在调用接口方法的时候, 会根据 save 方法中的参数, 来决定要调用哪个 IRepository 的实现. 如果方法参数中有 repository=MySQL, 那么就调用 MysqlRepository 的 save 方法. 如果 repository=mongo, 就调用 MongoRepository 的 save 方法. 和面向对象的延迟绑定很类似. 为什么 Dubbo 会引入扩展自适应实例的概念呢?
Dubbo 中的配置有两种, 一种是固定的系统级别的配置, 在 Dubbo 启动之后就不会再改了. 还有一种是运行时的配置, 可能对于每一次的 RPC, 这些配置都不同. 比如在 xml 文件中配置了超时时间是 10 秒钟, 这个配置在 Dubbo 启动之后, 就不会改变了. 但针对某一次的 RPC 调用, 可以设置它的超时时间是 30 秒钟, 以覆盖系统级别的配置. 对于 Dubbo 而言, 每一次的 RPC 调用的参数都是未知的. 只有在运行时, 根据这些参数才能做出正确的决定.
很多时候, 我们的类都是一个单例的, 比如 Spring 的 bean, 在 Spring bean 都实例化时, 如果它依赖某个扩展点, 但是在 bean 实例化时, 是不知道究竟该使用哪个具体的扩展实现的. 这时候就需要一个代理模式了, 它实现了扩展点接口, 方法内部可以根据运行时参数, 动态的选择合适的扩展实现. 而这个代理就是自适应实例. 自适应扩展实例在 Dubbo 中的使用非常广泛, Dubbo 中, 每一个扩展都会有一个自适应类, 如果我们没有提供, Dubbo 会使用字节码工具为我们自动生成一个. 所以我们基本感觉不到自适应类的存在. 后面会有例子说明自适应类是怎么工作的.
5.5 @SPI
@SPI 注解作用于扩展点的接口上, 表明该接口是一个扩展点. 可以被 Dubbo 的 ExtentionLoader 加载. 如果没有此 ExtensionLoader 调用会异常.
5.6 @Adaptive
@Adaptive 注解用在扩展接口的方法上. 表示该方法是一个自适应方法. Dubbo 在为扩展点生成自适应实例时, 如果方法有 @Adaptive 注解, 会为该方法生成对应的代码. 方法内部会根据方法的参数, 来决定使用哪个扩展. @Adaptive 注解用在类上代表实现一个装饰类, 类似于设计模式中的装饰模式, 它主要作用是返回指定类, 目前在整个系统中 AdaptiveCompiler,AdaptiveExtensionFactory 这两个类拥有该注解.
5.7 ExtentionLoader
类似于 Java SPI 的 ServiceLoader, 负责扩展的加载和生命周期维护.
5.8 扩展别名
和 Java SPI 不同, Dubbo 中的扩展都有一个别名, 用于在应用中引用它们. 比如
- random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
- roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
5.9 一些路径
和 Java SPI 从 / META-INF/services 目录加载扩展配置类似, Dubbo 也会从以下路径去加载扩展配置文件:
- META-INF/dubbo/internal
- META-INF/dubbo
- META-INF/services
6.Dubbo 的 LoadBalance 扩展点解读
在了解了 Dubbo 的一些基本概念后, 让我们一起来看一个 Dubbo 中实际的扩展点, 对这些概念有一个更直观的认识.
我们选择的是 Dubbo 中的 LoadBalance 扩展点. Dubbo 中的一个服务, 通常有多个 Provider,consumer 调用服务时, 需要在多个 Provider 中选择一个. 这就是一个 LoadBalance. 我们一起来看看在 Dubbo 中, LoadBalance 是如何成为一个扩展点的.
6.1 LoadBalance 接口
- @SPI(RandomLoadBalance.NAME)
- public interface LoadBalance {
- @Adaptive("loadbalance")
- <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) throws RpcException;
- }
LoadBalance 接口只有一个 select 方法. select 方法从多个 invoker 中选择其中一个. 上面代码中和 Dubbo SPI 相关的元素有:
@SPI( http://RandomLoadBalance.NAME ) @SPI 作用于 LoadBalance 接口, 表示接口 LoadBalance 是一个扩展点. 如果没有 @SPI 注解, 试图去加载扩展时, 会抛出异常.@SPI 注解有一个参数, 该参数表示该扩展点的默认实现的别名. 如果没有显示的指定扩展, 就使用默认实现. RandomLoadBalance.NAME 是一个常量, 值是 "random", 是一个随机负载均衡的实现. random 的定义在配置文件 META-INF/dubbo/internal/com.alibaba.dubbo.rpc.cluster.LoadBalance 中:
- random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance
- roundrobin=com.alibaba.dubbo.rpc.cluster.loadbalance.RoundRobinLoadBalance
- leastactive=com.alibaba.dubbo.rpc.cluster.loadbalance.LeastActiveLoadBalance
- consistenthash=com.alibaba.dubbo.rpc.cluster.loadbalance.ConsistentHashLoadBalance
可以看到文件中定义了 4 个 LoadBalance 的扩展实现. 由于负载均衡的实现不是本次的内容, 这里就不过多说明. 只用知道 Dubbo 提供了 4 种负载均衡的实现, 我们可以通过 xml 文件, properties 文件, JVM 参数显式的指定一个实现. 如果没有, 默认使用随机.
@Adaptive("loadbalance") @Adaptive 注解修饰 select 方法, 表明方法 select 方法是一个可自适应的方法. Dubbo 会自动生成该方法对应的代码. 当调用 select 方法时, 会根据具体的方法参数来决定调用哪个扩展实现的 select 方法.@Adaptive 注解的参数 loadbalance 表示方法参数中的 loadbalance 的值作为实际要调用的扩展实例. 但奇怪的是, 我们发现 select 的方法中并没有 loadbalance 参数, 那怎么获取 loadbalance 的值呢? select 方法中还有一个 URL 类型的参数, Dubbo 就是从 URL 中获取 loadbalance 的值的. 这里涉及到 Dubbo 的 URL 总线模式, 简单说, URL 中包含了 RPC 调用中的所有参数. URL 类中有一个 Map<String, String> parameters 字段, parameters 中就包含了 loadbalance.
来源: https://juejin.im/post/5c690e5df265da2ddd4a4c19