Dubbo源码分析

一,为什么要用Dubbo

1.为什么要用现成的框架呢?

如果我们自己去开发一个网络通信,需要考虑到

  • 底层网络通信协议的处理
  • 序列化和反序列化的处理工作

这些工作本身应该是通用的,应该是一个中间件服务。为整个公司提供远程通信的服务。而不应该由业务开发人员来自己去实现,所以才有了这样的 rpc 框架,使得我们调用远程方法时就像调用本地方法那么简单,不需要关心底层的通信逻辑。

2.大规模服务化对于服务治理的要求

当企业开始大规模的服务化以后,远程通信带来的弊端就越来越明显了。

  • 服务链路变长了,如何实现对服务链路的跟踪和监控
  • 服务的大规模集群使得服务之间需要依赖第三方注册中心来解决服务的发现和服务的感知问题
  • 服务通信之间的异常,需要有一种保护机制防止一个节点故障引发大规模的系统故障,所以要有容错机制
  • 服务大规模集群会是的客户端需要引入负载均衡机制实现请求分发

dubbo 主要是一个分布式服务治理解决方案,那么什么是服务治理?服务治理主要是针对大规模服务化以后,服务之间的路由、负载均衡、容错机制、服务降级这些问题的解决方案,而 Dubbo 实现的不仅仅是远程服务通信,并且还解决了服务路由、负载、降级、容错等功能。

二,Dubbo 的基本使用

1.dubbo-common

1)service

/**
 * @author yhd
 * @email yinhuidong1@xiaomi.com
 * @description TODO
 * @since 2021/4/2 0:32
 */
public interface LoginService {
   

    String login(String username,String password);
}

2.dubbo-provider

1)pom

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<dependency>
			<groupId>org.apache.dubbo</groupId>
			<artifactId>dubbo</artifactId>
			<version>2.7.8</version>
		</dependency>
		<dependency>
			<groupId>org.slf4j</groupId>
			<artifactId>slf4j-api</artifactId>
		</dependency>
		<dependency>
			<groupId>ch.qos.logback</groupId>
			<artifactId>logback-classic</artifactId>
		</dependency>
		<dependency>
			<groupId>com.yhd</groupId>
			<artifactId>dubbo-common</artifactId>
			<version>0.0.1-SNAPSHOT</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.apache.zookeeper</groupId>
			<artifactId>zookeeper</artifactId>
			<version>3.6.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.curator</groupId>
			<artifactId>curator-framework</artifactId>
			<version>5.1.0</version>
		</dependency>
		<dependency>
			<groupId>org.apache.curator</groupId>
			<artifactId>curator-recipes</artifactId>
			<version>5.1.0</version>
		</dependency>
		<dependency>
			<groupId>com.101tec</groupId>
			<artifactId>zkclient</artifactId>
			<version>0.10</version>
		</dependency>

2)配置文件

server.port=8081
dubbo.application.name=dubbo-provider
dubbo.registry.address=zookeeper://localhost:2181
dubbo.registry.protocal=zookeeper
dubbo.protocol.name=dubbo
dubbo.protocol.port=20880

3)主启动类

@EnableDubbo
@ComponentScan("com.yhd")
@SpringBootApplication
public class DubboProviderApplication {
   

	public static void main(String[] args) {
   
		SpringApplication.run(DubboProviderApplication.class, args);
	}

}

4)service

@DubboService
public class LoginServiceImpl implements LoginService {
   
    @Override
    public String login(String username, String password) {
   
        return "login success!";
    }
}

3.dubbo-consumer

1)Controller

@RestController
public class LoginController {
   

    @DubboReference
    private LoginService loginService;

    @GetMapping("login/{aaa}/{bbb}")
    public String login(@PathVariable("aaa")String aaa,@PathVariable("bbb") String bbb){
   
        return loginService.login(aaa,bbb);
    }
}

三,dubbo启动原理

Dubbo 提供了几种容器让我们去启动和发布服务

1.容器类型

Spring Container

自动加载 META-INF/spring 目录下的所有 Spring 配置。

logback Container

自动装配 logback 日志

Log4j Container

自动配置 log4j 的配置

Dubbo 提供了一个 Main.main 快速启动相应的容器,默认情况下,只会启动 spring 容器

2.原理分析

默认情况下,spring 容器,本质上,就是加在 spring ioc 容器,然后启动一个 netty 服务实现服务的发布,所以并没有特别多的黑科技,下面是spring 容器启动的代码

public void start() {
   
    String configPath =ConfigUtils.getProperty( "dubbo.spring.config");
    if (StringUtils.isEmpty(configPath)) {
   
        configPath =  "classpath*:META- - INF/spring/*.xml";
    }
    context =  new
	ClassPathXmlApplicationContext(configPath.split( "[,\ \\ \ s]+"),false);
    context.refresh();
    context.start();
}

四,Dubbo对注册中心的支持

Dubbo 能够支持的注册中心有:consul、etcd、nacos、sofa、zookeeper、redis、multicast

1.Dubbo 集成 Zookeeper 的实现原理

在这里插入图片描述

2.dubbo 每次都要连 zookeeper ?

不是每次发起一个请求的时候,都需要访问注册中心,是通过缓存实现。

其他注册中心的实现,核心本质是一样的,都是为了管理服务地址。

3.多注册中心支持

Dubbo 中可以支持多注册中心,有的时候,客户端需要用调用的远程服务不在同一个注册中心上,那么客户端就需要配置多个注册中心来访问。

五,Dubbo仅仅是一个RPC框架?

Dubbo 的核心功能,提供服务注册和服务发现以及基于 Dubbo 协议的远程通信。Dubbo 从另一个方面来看也可以认为是一个服务治理生态。

  • Dubbo 可以支持市面上主流的注册中心
  • Dubbo 提供了 Container 的支持,默认提供了 3 种 container。
  • Dubbo 对于 RPC 通信协议的支持,不仅仅是原生的 Dubbo 协议,它还围绕着 rmi、hessian、http、webservice、thrift、rest

有了多协议的支持,使得其他 rpc 框架的应用程序可以快速的切入到 dubbo生态中。 同时,对于多协议的支持,使得不同应用场景的服务,可以选择合适的协议来发布服务,并不一定要使用 dubbo 提供的长连接方式。

1.Dubbo 监控平台安装

Dubbo-Admin

  • 修 改 dubbo-admin-server/src/main/resources/application.properties中的配置信息
  • mvn clean package 进行构建
  • mvn clean package 进行构建
  • 访问 localhost:8080

2.Dubbo 的终端操作

Dubbo 里面提供了一种基于终端操作的方法来实现服务治理。

使用 telnet localhost 20880 连接到服务对应的端口。

1)常见命令

ls
ls: 显示服务列表
ls -l: 显示服务详细信息列表
ls XxxService: 显示服务的方法列表
ls -l XxxService: 显示服务的方法详细信息列表

ps
ps: 显示服务端口列表
ps -l: 显示服务地址列表
ps 20880: 显示端口上的连接信息
ps -l 20880: 显示端口上的连接详细信息

cd
cd XxxService: 改变缺省服务,当设置了缺省服务,凡是需要输入服务名作
为参数的命令,都可以省略服务参数
cd /: 取消缺省服务

pwd
pwd: 显示当前缺省服务

count
count XxxService: 统计 1 次服务任意方法的调用情况
count XxxService 10: 统计 10 次服务任意方法的调用情况
count XxxService xxxMethod: 统计 1 次服务方法的调用情况
count XxxService xxxMethod 10: 统计 10 次服务方法的调用情况

六,负载均衡

1.负载均衡的背景

当服务端存在多个节点的集群时,zookeeper 上会维护不同集群节点,对于客户端而言,他需要一种负载均衡机制来实现目标服务的请求负载。通过负载均衡,可以让每个服务器节点获得适合自己处理能力的负载。

Dubbo 里面默认就集成了负载均衡的算法和实现,默认提供了 4 种负载均衡实现。

2.Dubbo 中负载均衡的应用

1)启动两台一样的服务

修改配置文件

dubbo.protocol.port=20881

2)代码

@DubboService(loadbalance = "random")

3.Dubbo负载均衡算法

1)RandomLoadBalance

权重随机算法,根据权重值进行随机负载。

它的算法思想很简单。假设我们有一组服务器 servers = [A, B, C],他们对应的权重为weights = [5, 3, 2],权重总和为 10。现在把这些权重值平铺在一维坐标值上,[0, 5) 区间属于服务器 A,[5, 8) 区间属于服务器 B,[8, 10) 区间属于服务器 C。接下来通过随机数生成器生成一个范围在 [0, 10) 之间的随机数,然后计算这个随机数会落到哪个区间上。比如数字 3 会落到服务器 A 对应的区间上,此时返回服务器 A 即可。权重越大的机器,在坐标轴上对应的区间范围就越大,因此随机数生成器生成的数字就会有更大的概率落到此区间内。只要随机数生成器产生的随机数分布性很好,在经过多次选择后,每个服务器被选中的次数比例接近其权重比例。

2)LeastActiveLoadBalance

最少活跃调用数算法,活跃调用数越小,表明该服务提供者效率越高,单位时间内可处理更多的请求这个是比较科学的负载均衡算法。

每个服务提供者对应一个活跃数 active。初始情况下,所有服务提供者活跃数均为 0。每收到一个请求,活跃数加 1,完成请求后则将活跃数减 1。在服务运行一段时间后,性能好的服务提供者处理请求的速度更快,因此活跃数下降的也越快,此时这样的服务提供者能够优先获取到新的服务请求。

3)ConsistentHashLoadBalance

hash 一致性算法,相同参数的请求总是发到同一提供者。

当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。

4)RoundRobinLoadBalance

加权轮询算法

所谓轮询是指将请求轮流分配给每台服务器。举个例子,我们有三台服务器 A、B、C。我们将第一个请求分配给服务器 A,第二个请求分配给服务器 B,第三个请求分配给服务器 C,第四个请求再次分配给服务器 A。这个过程就叫做轮询。轮询是一种无状态负载均衡算法,实现简单,适用于每台服务器性能相近的场景下。但现实情况下,我们并不能保证每台服务器性能均相近。如果我们将等量的请求分配给性能较差的服务器,这显然是不合理的。因此,这个时候我们需要对轮询过程进行加权,以调控每台服务器的负载。经过加权后,每台服务器能够得到的请求数比例,接近或等于他们的权重比。比如服务器 A、B、C 权重比为 5:2:1。那么在 8 次请求中,服务器 A 将收到其中的 5 次请求,服务器 B 会收到其中的 2 次请求,服务器 C 则收到其中的 1次请求。

5)一致性 hash 算法原理

七,集群容错

网络通信会有很多不确定因素,比如网络延迟、网络中断、服务异常等,会造成当前这次请求出现失败。 当服务通信出现这个问题时,需要采取一定的措施应对。而 dubbo 中提供了容错机制来优雅处理这种错误。

在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。

@DubboService(loadbalance = "random",cluster = "failsafe")

1.Failover Cluster

失败自动切换,当出现失败,重试其它服务器。(缺省)。

通常用于读操作,但重试会带来更长延迟。

可通过 retries=“2” 来设置重试次数(不含第一次)。

2.Failfast Cluster

快速失败,只发起一次调用,失败立即报错。

通常用于非幂等性的写操作,比如新增记录。

3.Failsafe Cluster

失败安全,出现异常时,直接忽略。

通常用于写入审计日志等操作。

4.Failback Cluster

失败自动恢复,后台记录失败请求,定时重发。

通常用于消息通知操作。

5.Forking Cluster

并行调用多个服务器,只要一个成功即返回。

通常用于实时性要求较高的读操作,但需要浪费更多服务资源。

可通过 forks=“2” 来设置最大并行数。

6.Broadcast Cluster

广播调用所有提供者,逐个调用,任意一台报错则报错。(2.1.0 开始支持)

通常用于通知所有提供者更新缓存或日志等本地资源信息。

在实际应用中 查询语句容错策略建议使用默认 Failover Cluster ,而增删改 建议使用Failfast Cluster 或者 使用 Failover Cluster(retries=”0”) 策略 防止出现数据 重复添加等等其它问题!建议在设计接口时候把查询接口方法单独做一个接口提供查询。

八,服务降级

1.降级的概念

当某个非关键服务出现错误时,可以通过降级功能来临时屏蔽这个服务。降级可以有几个层面的分类: 自动降级和人工降级; 按照功能可以分为:读服务降级和写服务降级;

  • 对一些非核心服务进行人工降级,在大促之前通过降级开关关闭哪些推荐内容、评价等对主流程没有影响的功能。
  • 故障降级,比如调用的远程服务挂了,网络故障、或者 RPC 服务返回异常。 那么可以直接降级,降级的方案比如设置默认值、采用兜底数据(系统推荐的行为广告挂了,可以提前准备静态页面做返回)等等。
  • 限流降级,在秒杀这种流量比较集中并且流量特别大的情况下,因为突发访问量特别大可能会导致系统支撑不了。这个时候可以采用限流来限制访问量。当达到阀值时,后续的请求被降级,比如进入排队页面,比如跳转到错误页(活动太火爆,稍后重试等)。

那么,Dubbo 中如何实现服务降级呢?Dubbo 中提供了一个 mock 的配置,可以通过mock 来实现当服务提供方出现网络异常或者挂掉以后,客户端不抛出异常,而是通过Mock 数据返回自定义的数据。

2.Dubbo 实现服务降级

dubbo-client 端创建一个 mock 类,当出现服务降级时,会被调用

/**
 * @author yhd
 * @email yinhuidong1@xiaomi.com
 * @description 服务降级兜底类
 * @since 2021/4/2 15:28
 */
public class MockSayHelloService implements LoginService {
   
    @Override
    public String login(String username, String password) {
   
        return "Sorry,  服务端发生异常,被降级啦!";
    }
}

在消费方的主街上配置:

    @DubboReference(mock = "com.yhd.dubboconsumer.mock.MockSayHelloService",
            timeout = 1000, loadbalance = "random", cluster = "failfast",check = false)
    private LoginService loginService;

3.启动时检查

Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring初始化完成,以便上线时,能及早发现问题,默认 check=“true”。

可以通过 check=“false” 关闭检查,比如,测试时,有些服务不关心,或者出现了循环依赖,必须有一方先启动。

registry、reference、consumer 都可以配置 check 这个属性.

    @DubboReference(mock = "com.yhd.dubboconsumer.mock.MockSayHelloService",
            timeout = 1000, loadbalance = "random", cluster = "failfast",check = false)
    private LoginService loginService;

4.多版本支持

当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。

可以按照以下的步骤进行版本迁移:

  • 在低压力时间段,先升级一半提供者为新版本
  • 再将所有消费者升级为新版本
  • 然后将剩下的一半提供者升级为新版本

5.主机绑定

1)默认的主机绑定方式

  • 通过 LocalHost.getLocalHost()获取本机地址。
  • 如果是 127.*等 loopback(环路地址)地址,则扫描各网卡,获取网卡 IP。
    • 如果是 springboot,修改配置:dubbo.protocol.host=””
    • 如果注册地址获取不正确,可以通过在 dubbo.xml 中加入主机地址的配置。
<dubbo:protocol host="205.182.23.201">

2)缺省主机端口

dubbo: 20880
rmi: 1099
http: 80
hessian: 80
webservice: 80
memcached: 11211
redis: 6379

九,Dubbo 新的功能

1.动态配置规则

动态配置是 Dubbo2.7 版本引入的一个新的功能,简单来说,就是把 dubbo.properties中的属性进行集中式存储,存储在其他的服务器上。

那么如果需要用到集中式存储,那么还需要一些配置中心的组件来支撑。目前 Dubbo 能支持的配置中心有:apollo、nacos、zookeeper

从另外一个角度来看,我们之前用 zookeeper 实现服务注册和发现,本质上就是使用 zookeeper 实现了配置中心,这个配置中心只是维护了服务注册和服务感知的功能。在 2.7 版本中,dubbo 对配置中心做了延展,除了服务注册之外,还可以把其他的数据存储在 zookeeper 上,从而更好的进行维护。

1)在 dubboadmin 添加配置

应用名称可以是 global,或者对应当前服务的应用名,如果是 global 表示全局配置,针对所有应用可见。

配置的内容,实际就是 dubbo.properties 中配置的基本信息。只是同意存储在了zookeeper 上。

2)本地的配置文件添加配置中心

application.properties 中添加配置中心的配置项,app-name对应的是上一步创建的配置项中的应用名.

dubbo.config-center.address= zookeeper://192.168.13.106 6 :2181
dubbo.config-center.app- - name= spring-boot-provider
#需要注意的是,存在于配置中心上的配置项,本地仍然需要配置一份。所以下面这些配置一定要加上。否则启动不了。这样做的目的是保证可靠性
dubbo.application.name= spring-boot-provider
dubbo.protocol.port= 20880
dubbo.protocol.name= dubbo
dubbo.registry.address= zookeeper://192.168.13.102:2181?backup=192.168.13.103:2181,192.168.13.104:2181

3)配置的优先级

引入配置中心后,配置的优先级就需要关注了,默认情况下,外部配置的优先级最高,也就是意味着配置中心上的配置会覆盖本地的配置。当然我们也可以调整优先级。

dubbo.config-center.highest-priority=false

4)配置中心的原理

默认所有的配置都存储在/dubbo/config 节点。

namespace,用于不同配置的环境隔离。

config,Dubbo 约定的固定节点,不可更改,所有配置和服务治理规则都存储在此节点下。

dubbo/application,分别用来隔离全局配置、应用级别配置:dubbo 是默认 group 值,

application 对应应用名。

dubbo.properties,此节点的 node value 存储具体配置内容。

在这里插入图片描述

2.元数据中心

Dubbo2.7 的另外一个新的功能,就是增加了元数据的配置。

在 Dubbo2.7 之前,所有的配置信息,比如服务接口名称、重试次数、版本号、负载策略、容错策略等等,所有参数都是基于 url 形式配置在 zookeeper 上的。这种方式会造成一些问题:

  • url 内容过多,导致数据存储空间增大
  • url 需要涉及到网络传输,数据量过大会造成网络传输过慢
  • 网络传输慢,会造成服务地址感知的延迟变大,影响服务的正常响应

服务提供者这边的配置参数有 30 多个,有一半是不需要作为注册中心进行存储和传输地的。而消费者这边可配置的参数有 25 个以上,只有个别是需要传递到注册中心的。所以,在 Dubbo2.7 中对元数据进行了改造,简单来说,就是把属于服务治理的数据发布到注册中心,其他的配置数据统一发布到元数据中心。这样一来大大降低了注册中心的负载。

1)元数据中心配置

元数据中心目前支持 redis 和 zookeeper。官方推荐是采用 redis。毕竟 redis 本身对于非结构化存储的数据读写性能比较高。当然,也可以使用 zookeeper 来实现。将注册中心地址、元数据中心地址等配置集中管理,可以做到统一环境、减少开发侧感知。官网可查询外部化配置,不过描述过于简略。

dubbo.metadata-report.address= zookeeper://192.168.13.106:2181
dubbo.registry.simplified= true 
#注册到注册中心的 URL 是否采用精简模式的(与低版本兼容)

十,Dubbo 中的 SPI 机制

dubbo版本2.7.2。

1.Java SPI

SPI 是 JDK 内置的一种服务提供发现机制。目前市面上有很多框架都是用它来做服务的扩展发现。简单来说,它是一种动态替换发现的机制。举个简单的例子,我们想在运行时动态给它添加实现,你只需要添加一个实现,然后把新的实现描述给 JDK 知道就行了。如 JDBC、日志框架都有用到。

在这里插入图片描述

1)实现SPI需要遵循的标准

  • 需要在 classpath 下创建一个目录,该目录命名必须是:META-INF/service
  • 在该目录下创建一个 properties 文件,该文件需要满足以下几个条件
    • 文件名必须是扩展的接口的全路径名称
    • 文件内部描述的是该扩展接口的所有实现类
    • 文件的编码格式是 UTF-8
  • 通过 java.util.ServiceLoader 的加载机制来发现

2)SPI的实际应用

JDK 本身提供了数据访问的 api。在 java.sql 这个包里面。

java.sql.Driver 的源码,Driver 并没有实现,而是提供了一套标准的 api 接口。

在这里插入图片描述

通过 SPI 机制把 java.sql.Driver 和 mysql 的驱动做了集成,达到了各个数据库厂商自己去实现数据库连接,jdk 本身不关心你怎么实现。

门面模式?适配器模式?

3)SPI的缺点

  • JDK 标准的 SPI 会一次性加载实例化扩展点的所有实现,什么意思呢?就是如果你在 META-INF/service 下的文件里面加了 N个实现类,那么 JDK 启动的时候都会一次性全部加载。那么如果有的扩展点实现初始化很耗时或者如果有些实现类并没有用到,那么会很浪费资源
  • 如果扩展点加载失败,会导致调用方报错,而且这个错误很难定位到是这个原因。

2.Dubbo 优化后的 SPI

1)基于 Dubbo SPI 的实现自己的扩展

Dubbo 的 SPI 扩展机制,有两个规则

  • 需要在 resource 目录下配置 META-INF/dubbo 或者 META-INF/dubbo/internal 或者 META-INF/services,并基于 SPI 接口去创建一个文件。
  • 文件名称和接口名称保持一致,文件内容和 SPI 有差异,内容是 KEY 对应 Value

Dubbo 针对的扩展点非常多,可以针对协议、拦截、集群、路由、负载均衡、序列化、容器… 几乎里面用到的所有功能,都可以实现自己的扩展,这个是 dubbo 比较强大的一点。

在这里插入图片描述

2)扩展协议扩展点

  • 创建如下结构,添加 META-INF.dubbo 文件。类名和 Dubbo 提供的协议扩展点接口保持一致。

在这里插入图片描述

myProtocol=com.yhd.dubboprovider.diy.MyProtocol
  • 创建 MyProtocol 协议类
    • 可以实现自己的协议,我们为了模拟协议产生了作用,修改一个端口
public class MyProtocol implements Protocol {
   
    @Override
    public int getDefaultPort() {
   
        return 8888;
    }

    @Override
    public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException {
   
        return null;
    }

    @Override
    public <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException {
   
        return null;
    }

    @Override
    public void destroy() {
   

    }
}
  • 在调用处执行如下代码
Protocol protocol=ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol"); 
System.out.print(protocol.getDefaultPort)
  • 输出结果,可以看到运行结果,是执行的自定义的协议扩展点。
  • 总结:总的来说,思路和 SPI 是差不多。都是基于约定的路径下制定配置文件。目的,通过配置的方式轻松实现功能的扩展。

一定有一个地方通过读取指定路径下的所有文件进行 load。然后讲对应的结果保存到一个 map 中,key 对应为名称,value 对应为实现类。那么这个实现,一定就在 ExtensionLoader 中了。

3.Dubbo 的扩展点原理实现

Dubbo SPI和JDK SPI配置的不同,在Dubbo SPI中可以通过键值对的方式进行配置,这样就可以按需加载指定的实现类。

Dubbo SPI的相关逻辑都被封装到ExtensionLoader类中,通过ExtensionLoader我们可以加载指定的实现类,一个扩展接口就对应一个ExtensionLoader对象,在这里我们把它称为:扩展点加载器。

1)属性

public class ExtensionLoader<T> {
   
    
    //扩展点配置文件的路径,可以从3个地方加载到扩展点配置文件
    private static final String SERVICES_DIRECTORY = "META-INF/services/";
    private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";
    private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";   
    //扩展点加载器的集合
    private static final ConcurrentMap<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<Class<?>, ExtensionLoader<?>>();
    //扩展点实现的集合
    private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<Class<?>, Object>();
    //扩展点名称和实现的映射缓存
    private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<Class<?>, String>();
    //拓展点实现类集合缓存
    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<Map<String, Class<?>>>();
    //扩展点名称和@Activate的映射缓存
    private final Map<String, Activate> cachedActivates = new ConcurrentHashMap<String, Activate>();
    //扩展点实现的缓存
    private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<String, Holder<Object>>();
}

ExtensionLoader会把不同的扩展点配置和实现都缓存起来。同时,Dubbo在官网上也给了我们提醒:扩展点使用单一实例加载(请确保扩展实现的线程安全性),缓存在 ExtensionLoader中。下面我们看几个重点方法。

2)获取扩展点加载器

我们首先通过ExtensionLoader.getExtensionLoader() 方法获取一个 ExtensionLoader 实例,它就是扩展点加载器。然后再通过 ExtensionLoader 的 getExtension 方法获取拓展类对象。这其中,getExtensionLoader 方法用于从缓存中获取与拓展类对应的 ExtensionLoader,若缓存未命中,则创建一个新的实例。

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!");
    }
    if (!withExtensionAnnotation(type)) {
   
        throw new IllegalArgumentException("Extension type(" + type +
                ") is not extension, because WITHOUT @" + SPI.class.getSimpleName() + " Annotation!");
    }
    ExtensionLoader<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    if (loader == null) {
   
        EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<T>(type));
        loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
    }
    return loader;
}

比如你可以通过下面这样,来获取Protocol接口的ExtensionLoader实例:

ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);

就可以拿到扩展点加载器的对象实例:

com.alibaba.dubbo.common.extension.ExtensionLoader[com.alibaba.dubbo.rpc.Protocol]

3)获取扩展类对象

上一步我们已经拿到加载器,然后可以根据加载器实例,通过扩展点的名称获取扩展类对象。

public T getExtension(String name) {
   
    //校验扩展点名称的合法性
    if (name == null || name.length() == 0)
        throw new IllegalArgumentException("Extension name == null");
    // 获取默认的拓展实现类
    if ("true".equals(name)) {
   
        return getDefaultExtension();
    }
    //用于持有目标对象
    Holder<Object> holder = cachedInstances.get(name);
    if (holder == null) {
   
        cachedInstances.putIfAbsent(name, new Holder<Object>());
        holder = cachedInstances.get(name);
    }
    Object instance = holder.get();
    if (instance == null) {
   
        synchronized (holder) {
   
            instance = holder.get();
            if (instance == null) {
   
                instance = createExtension(name);
                holder.set(instance);
            }
        }
    }
    return (T) instance;
}

它先尝试从缓存中获取,未命中则创建扩展对象。那么它的创建过程是怎样的呢?

private T createExtension(String name) {
   
    //从配置文件中获取所有的扩展类,Map数据结构
    //然后根据名称获取对应的扩展类
    Class<?> clazz = getExtensionClasses().get(name);
    if (clazz == null) {
   
        throw findException(name);
    }
    try {
   
        //通过反射创建实例,然后放入缓存
        T instance = (T) EXTENSION_INSTANCES.get(clazz);
        if (instance == null) {
   
            EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
            instance = (T) EXTENSION_INSTANCES.get(clazz);
        }
        //注入依赖
        injectExtension(instance);
        Set<Class<?>> wrapperClasses = cachedWrapperClasses;
        if (wrapperClasses != null && !wrapperClasses.isEmpty()) {
   
            // 包装为Wrapper实例
            for (Class<?> wrapperClass : wrapperClasses) {
   
                instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance));
            }
        }
        return instance;
    } catch (Throwable t) {
   
        throw new IllegalStateException("Extension instance(name: " + name + ", class: " +
                type + ")  could not be instantiated: " + t.getMessage(), t);
    }
}

这里的重点有两个,依赖注入和Wrapper包装类,它们是Dubbo中IOC 与 AOP 的具体实现。

①依赖注入

向拓展对象中注入依赖,它会获取类的所有方法。判断方法是否以 set 开头,且方法仅有一个参数,且方法访问级别为 public,就通过反射设置属性值。所以说,Dubbo中的IOC仅支持以setter方式注入。

private T injectExtension(T instance) {
   
    try {
   
        if (objectFactory != null) {
   
            for (Method method : instance.getClass().getMethods()) {
   
                if (method.getName().startsWith("set")
                        && method.getParameterTypes().length == 1
                        && Modifier.isPublic(method.getModifiers())) {
   
                    Class<?> pt = method.getParameterTypes()[0];
                    try {
   
                        String property = method.getName().length() > 3 ? method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4) : "";
                        Object object = objectFactory.getExtension(pt, property);
                        if (object != null) {
   
                            method.invoke(instance, object);
                        }
                    } catch (Exception e) {
   
                        logger.error("fail to inject via method " + method.getName()
                                + " of interface " + type.getName() + ": " + e.getMessage(), e);
                    }
                }
            }
        }
    } catch (Exception e) {
   
        logger.error(e.getMessage(), e);
    }
    return instance;
}
②Wrapper

它会将当前 instance 作为参数传给 Wrapper 的构造方法,并通过反射创建 Wrapper 实例。 然后向 Wrapper 实例中注入依赖,最后将 Wrapper 实例再次赋值给 instance 变量。说起来可能比较绕,我们直接看下它最后生成的对象就明白了。

我们以DubboProtocol为例,它包装后的对象为:

在这里插入图片描述

综上所述,如果我们获取一个扩展类对象,最后拿到的就是这个Wrapper类的实例。

就像这样:

ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol extension = extensionLoader.getExtension("dubbo");
System.out.println(extension);

输出为:com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper@4cdf35a9

4)获取所有的扩展类

在我们通过名称获取扩展类对象之前,首先需要根据配置文件解析出所有的扩展类。

它是一个扩展点名称和扩展类的映射表Map<String, Class<?>>

首先,还是从缓存中cachedClasses获取,如果没有就调用loadExtensionClasses从配置文件中加载。配置文件有三个路径:

  • META-INF/services/
  • META-INF/dubbo/
  • META-INF/dubbo/internal/

先尝试从缓存中获取。

private Map<String, Class<?>> getExtensionClasses() {
   
    //从缓存中获取
    Map<String, Class<?>> classes = cachedClasses.get();
    if (classes == null) {
   
        synchronized (cachedClasses) {
   
            classes = cachedClasses.get();
            if (classes == null) {
   
                //加载扩展类
                classes = loadExtensionClasses();
                cachedClasses.set(classes);
            }
        }
    }
    return classes;
}   

如果没有,就调用loadExtensionClasses从配置文件中读取。

private Map<String, Class<?>> loadExtensionClasses() {
   
    //获取 SPI 注解,这里的 type 变量是在调用 getExtensionLoader 方法时传入的
    final SPI defaultAnnotation = type.getAnnotation(SPI.class);
    if (defaultAnnotation != null) {
   
        String value = defaultAnnotation.value();
        if ((value = value.trim()).length() > 0) {
   
            String[] names = NAME_SEPARATOR.split(value);
            if (names.length > 1) {
   
                throw new IllegalStateException("more than 1 default extension 
                    name on extension " + type.getName()+ ": " + Arrays.toString(names));
            }
            //设置默认的扩展名称,参考getDefaultExtension 方法
            //如果名称为true,就是调用默认扩赞类
            if (names.length == 1) cachedDefaultName = names[0];
        }
    }
    //加载指定路径的配置文件
    Map<String, Class<?>> extensionClasses = new HashMap<String, Class<?>>();
    loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY);
    loadDirectory(extensionClasses, DUBBO_DIRECTORY);
    loadDirectory(extensionClasses, SERVICES_DIRECTORY);
    return extensionClasses;
}

以Protocol接口为例,获取到的实现类集合如下,我们就可以根据名称加载具体的扩展类对象。

{
   
    registry=class com.alibaba.dubbo.registry.integration.RegistryProtocol
    injvm=class com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
    thrift=class com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
    mock=class com.alibaba.dubbo.rpc.support.MockProtocol
    dubbo=class com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
    http=class com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
    redis=class com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
    rmi=class com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
}

在这里插入图片描述

4.自适应扩展机制

在Dubbo中,很多拓展都是通过 SPI 机制进行加载的,比如 Protocol、Cluster、LoadBalance 等。这些扩展并非在框架启动阶段就被加载,而是在扩展方法被调用的时候,根据URL对象参数进行加载。

那么,Dubbo就是通过自适应扩展机制来解决这个问题。

自适应拓展机制的实现逻辑是这样的:

首先 Dubbo 会为拓展接口生成具有代理功能的代码。然后通过 javassist 或 jdk 编译这段代码,得到 Class 类。最后再通过反射创建代理类,在代理类中,就可以通过URL对象的参数来确定到底调用哪个实现类。

1)Adaptive注解

在开始之前,我们有必要先看一下与自适应拓展息息相关的一个注解,即 Adaptive 注解。

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
   ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
   
    String[] value() default {
   };
}

从上面的代码中可知,Adaptive 可注解在类或方法上。

  • 标注在类上
    Dubbo 不会为该类生成代理类。
  • 标注在方法上
    Dubbo 则会为该方法生成代理逻辑,表示当前方法需要根据 参数URL 调用对应的扩展点实现。

2)获取自适应拓展类

getAdaptiveExtension 方法是获取自适应拓展的入口方法。

public T getAdaptiveExtension() {
   
    // 从缓存中获取自适应拓展
    Object instance = cachedAdaptiveInstance.get();
    if (instance == null) {
   
        if (createAdaptiveInstanceError == null) {
   
            synchronized (cachedAdaptiveInstance) {
   
                instance = cachedAdaptiveInstance.get();
                //未命中缓存,则创建自适应拓展,然后放入缓存
                if (instance == null) {
   
                    try {
   
                        instance = createAdaptiveExtension();
                        cachedAdaptiveInstance.set(instance);
                    } catch (Throwable t) {
   
                        createAdaptiveInstanceError = t;
                        throw new IllegalStateException("fail to create 
                                                  adaptive instance: " + t.toString(), t);
                    }
                }
            }
        }
    }
    return (T) instance;
}

getAdaptiveExtension方法首先会检查缓存,缓存未命中,则调用 createAdaptiveExtension方法创建自适应拓展。

private T createAdaptiveExtension() {
   
    try {
   
        return injectExtension((T) getAdaptiveExtensionClass().newInstance());
    } catch (Exception e) {
   
        throw new IllegalStateException("
            Can not create adaptive extension " + type + ", cause: " + e.getMessage(), e);
    }
}

这里的代码较少,调用 getAdaptiveExtensionClass方法获取自适应拓展 Class 对象,然后通过反射实例化,最后调用injectExtension方法向拓展实例中注入依赖。

获取自适应扩展类过程如下:

private Class<?> getAdaptiveExtensionClass() {
   
    //获取当前接口的所有实现类
    //如果某个实现类标注了@Adaptive,此时cachedAdaptiveClass不为空
    getExtensionClasses();
    if (cachedAdaptiveClass != null) {
   
        return cachedAdaptiveClass;
    }
    //以上条件不成立,就创建自适应拓展类
    return cachedAdaptiveClass = createAdaptiveExtensionClass();
}

在上面方法中,它会先获取当前接口的所有实现类,如果某个实现类标注了@Adaptive,那么该类就被赋值给cachedAdaptiveClass变量并返回。如果没有,就调用createAdaptiveExtensionClass创建自适应拓展类。

private Class<?> createAdaptiveExtensionClass() {
   
    //构建自适应拓展代码
    String code = createAdaptiveExtensionClassCode();
    ClassLoader classLoader = findClassLoader();
    // 获取编译器实现类 这个Dubbo默认是采用javassist 
    Compiler compiler =ExtensionLoader.getExtensionLoader(Compiler.class).getAdaptiveExtension();
    //编译代码,返回类实例的对象
    return compiler.compile(code, classLoader);
}

在生成自适应扩展类之前,Dubbo会检查接口方法是否包含@Adaptive。如果方法上都没有此注解,就要抛出异常。

if (!hasAdaptiveAnnotation){
   
    throw new IllegalStateException(
        "No adaptive method on extension " + type.getName() + ", 
          refuse to create the adaptive class!");
}

我们还是以Protocol接口为例,它的export()refer()方法,都标注为@AdaptivedestroygetDefaultPort未标注 @Adaptive注解。Dubbo 不会为没有标注 Adaptive 注解的方法生成代理逻辑,对于该种类型的方法,仅会生成一句抛出异常的代码。

package com.alibaba.dubbo.rpc;
import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.extension.Adaptive;
import com.alibaba.dubbo.common.extension.SPI;

@SPI("dubbo")
public interface Protocol {
   
    int getDefaultPort();
    @Adaptive
    <T> Exporter<T> export(Invoker<T> invoker) throws RpcException;
    @Adaptive
    <T> Invoker<T> refer(Class<T> type, URL url) throws RpcException;
    void destroy();
}

所以说当我们调用这两个方法的时候,会先拿到URL对象中的协议名称,再根据名称找到具体的扩展点实现类,然后去调用。下面是生成自适应扩展类实例的源代码:

package com.viewscenes.netsupervisor.adaptive;

import com.alibaba.dubbo.common.URL;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
import com.alibaba.dubbo.rpc.Exporter;
import com.alibaba.dubbo.rpc.Invoker;
import com.alibaba.dubbo.rpc.Protocol;
import com.alibaba.dubbo.rpc.RpcException;

public class Protocol$Adaptive implements Protocol {
   
    public void destroy() {
   
        throw new UnsupportedOperationException(
                "method public abstract void Protocol.destroy() of interface Protocol is not adaptive method!");
    }
    public int getDefaultPort() {
   
        throw new UnsupportedOperationException(
                "method public abstract int Protocol.getDefaultPort() of interface Protocol is not adaptive method!");
    }
    public Exporter export(Invoker invoker)throws RpcException {
   
        if (invoker == null) {
   
            throw new IllegalArgumentException("Invoker argument == null");
        }
        if (invoker.getUrl() == null) {
   
            throw new IllegalArgumentException("Invoker argument getUrl() == null");
        }
            
        URL url = invoker.getUrl();
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null) {
   
            throw new IllegalStateException("Fail to get extension(Protocol) name from url("
                    + url.toString() + ") use keys([protocol])");
        }
            
        Protocol extension = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);
        return extension.export(invoker);
    }
    public Invoker refer(Class clazz,URL ur)throws RpcException {
   
        if (ur == null) {
   
            throw new IllegalArgumentException("url == null");
        }
        URL url = ur;
        String extName = (url.getProtocol() == null ? "dubbo" : url.getProtocol());
        if (extName == null) {
   
            throw new IllegalStateException("Fail to get extension(Protocol) name from url("+ url.toString() + ") use keys([protocol])");
        }
        Protocol extension = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(extName);
        return extension.refer(clazz, url);
    }
}

综上所述,当我们获取某个接口的自适应扩展类,实际就是一个Adaptive类实例。

ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);            
Protocol adaptiveExtension = extensionLoader.getAdaptiveExtension();
System.out.println(adaptiveExtension);

输出为:

com.alibaba.dubbo.rpc.Protocol$Adaptive@47f6473

在这里插入图片描述

5.自动激活扩展点机制

自动激活扩展点,有点类似springboot 用到的 conditional,根据条件进行自动激活。但是这里设计的初衷是,对于一个类会加载多个扩展点的实现,这个时候可以通过自动激活扩展点进行动态加载, 从而简化配置我们的配置。

@Activate 提供了一些配置来允许我们配置加载条件,比如 group 过滤,比如 key 过滤。

我们可以看看 org.apache.dubbo.Filter 这个类,它有非常多的实现,比如说 CacheFilter,这个缓存过滤器,配置信息如下:

group 表示客户端和和服务端都会加载,value 表示 url 中有 cache_key 的时候

@Activate(group = {
   CONSUMER, PROVIDER}, value = CACHE_KEY)
public class CacheFilter implements Filter {
   

通过下面这段代码,演示关于 Filter 的自动激活扩展点的效果。没有添加“注释代码”时,list 的结果是 10,添加之后 list激活扩展点的效果。没有添加“注释代码”时,list 的结果是 10,添加之后 list。会自动把 cacheFilter 加载进来。

	public static void main(String[] args) {
   
		SpringApplication.run(DubboProviderApplication.class, args);


		/*Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getExtension("myProtocol");
		System.out.println(protocol.getDefaultPort());*/

		/*Compiler compiler = ExtensionLoader.getExtensionLoader(Compiler.class).getAdaptiveExtension();
		//org.apache.dubbo.common.compiler.support.AdaptiveCompiler
		System.out.println("compiler.getClass() = " + compiler.getClass());*/

		ExtensionLoader<Filter> extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class);
		URL url = new URL("", "", 0);
		List<Filter> filters = extensionLoader.getActivateExtension(//url.addParameter("cache","cache"), "cache");
		System.out.println("filters.size() = " + filters.size());
	}

这个方法的底层逻辑其实就是先获取到所有对应的激活扩展类,在拿到URL,根据 @Activate 获取到对应的扩展类组合在一起返回。

十一,Dubbo原理-框架设计

config 配置层:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类

proxy 服务代理层:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory

registry 注册中心层:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService

cluster 路由层:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance

monitor 监控层:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService

protocol 远程调用层:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter

exchange 信息交换层:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer

transport 网络传输层:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec

serialize 数据序列化层:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool

十二,服务暴露

分析:如果需要完成服务发布预注册,需要实现哪些事情?

  • 解析配置文件或注解
  • 服务注册
  • 启动netty服务实现远程监听

1.dubbo对于spring的扩展

1)spring的标签扩展

在 spring 中定义了两个接口

  • NamespaceHandler: 注册一堆 BeanDefinitionParser,利用他们来进行解析
  • BeanDefinitionParser:用于解析每个 element 的内容

Spring 默认会加载 jar 包下的 META-INF/spring.handlers 文件寻找对应的 NamespaceHandler。 Dubbo-config 模块下的 dubbo-config-spring就含有这个文件。

2)dubbo的接入实现

Dubbo 中 spring 扩展就是使用 spring 的自定义类型,所以同样也有 NamespaceHandler、BeanDefinitionParser。而NamespaceHandler 是 DubboNamespaceHandler。

public class DubboNamespaceHandler extends NamespaceHandlerSupport implements ConfigurableSourceBeanMetadataElement {
   

    static {
   
        Version.checkDuplicate(DubboNamespaceHandler.class);
    }

    @Override
    public void init() {
   
        registerBeanDefinitionParser("application", new DubboBeanDefinitionParser(ApplicationConfig.class, true));
        registerBeanDefinitionParser("module", new DubboBeanDefinitionParser(ModuleConfig
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值