这篇博文是我决心深度学习 Dubbo 框架时记录的笔记, 主题是 Dubbo 的拓展点, 下面的几个部分相对来说比较零散, 貌似是不和主题挂钩的 , 并且是一些很冷门的知识点 , 但是它们确实是深入学习 Dubbo 的前置知识
知识储备一: Dubbo 的架构图
细化一下上图的各个组成部分:
服务提供者
提供服务接口的实现类
注册服务 (远程注册, 本地注册)
对外暴露服务
注册中心
保存 服务名称 & 服务地址 的映射关系
当服务地址发生变动时, 主动通知消费者
服务消费者
启动时从注册中心拉取服务提供者的地址, 缓存在本地
根据负载均衡策略选出一个服务进行远程调用 (Dubbo 会将下面的信息封装成对象通过网络发送给服务提供者)
参数 1: 接口名
参数 2: 方法名
参数 3: 参数列表类型
参数 4: 参数值列表
监控中心
统计 RPC 过程的细节数据, 如: 服务调用次数, 调用时间
知识储备二: Dubbo 中的核心概念
URL
Dubbo 自己有个封装类叫 URL 如下: URL: 全称 Uniform Resources Loactor 统一资源定位符, 它是不可变的, 也是线程安全的
URL 的作用
其实, Dubbo 它作为一款 RPC 通信框架, 主体功能就是负责在服务集群中各个点之间进行数据的传递, 打个例子比如: 服务消费者调用服务的提供者, 这个过程中的通信是 Dubbo 框架实现的, 通信的格式就好比自定义协议一样, Dubbo 将服务提供者和服务消费者两种之间进行数据传递 需要的协议信息 / 端口号信息 / 请求那个接口 / 参数信息 / 账号 / 密码信息. 等一系列的信息进行封装, 于是上图中的 URL 诞生了
对 URL 的理解
对 URL 最直观的理解: URL 是 dobbo 对一系列数据的封装, 方便代码的编写, 参数的传递
很多人也将 URL 称为 Dubbo 的消息总线, 说 URL 贯穿于 Dubbo 的上下文, 我感觉到这个结论也许是这样得出的, 就是说 Dubbo 作为一款 RPC 框架, 首要的任务就是 RPC 远程过程调用, 怎么样找到提供服务的机器呢? 无论是发起 socket 还是借助 Thrift 或者 Netty 这种框架实现也罢, 前提是得知道提供服务的机器在哪里, 它的哪些接口对外暴露服务 , 没错! 这些信息都被 Dubbo 封装在了 URL 中
URL 常见的组成
protobuf - 协议信息, 如 zk / Dubbo / http / Thrift
host/port - 目标主机端口信息
path - 接口的名称
parameters - 参数键值对信息
典型的 Dubbo URL 格式
- # 描述 Dubbo 协议的服务
- Dubbo://192.168.1.6:20880/moe.cnkirito.sample.HelloService?timeout=3000
- # 描述 zookeeper 注册中心
- zookeeper://127.0.0.1:2181/org.apache.Dubbo.registry.RegistryService?application=demo-consumer&Dubbo=2.0.2&interface=org.apache.Dubbo.registry.RegistryService&pid=1214&qos.port=33333×tamp=1545721981946
- # 描述消费者 服务
- consumer://30.5.120.217/org.apache.Dubbo.demo.DemoService?application=demo-consumer&category=consumers&check=false&Dubbo=2.0.2&interface=org.apache.Dubbo.demo.DemoService&methods=sayHello&pid=1209&qos.port=33333&side=consumer×tamp=1545721827784
- # for this case, url protocol = null, url host = 192.168.1.3, port = 20880, url path = null
- 192.168.1.3:20880
- # for this case, url protocol = file, url host = null, url path = home/user1/router.JS
- file:///home/user1/router.JS?type=script
... 更多参照 URL 源码
Invoker
invoker 直译调用者
在服务提供方: invoker 对象被构造出来去调用提供服务的函数
在服务的消费方: invoker 用于调用 执行远程过程调用的类
Invocation
指代程序中的调用对象, 包含了 接口名 / 方法名 / 参数类型列表 / 参数值列表 等
知识储备三: Java SPI (Service Provider Interface )
怎么理解 SPI 机制呢?
如果说 SPI 是 java 提供的一种拓展机制, 其实是不明确的, 结合 java 本身的语言特性来说, SPI 直观的看就是 基于接口的编程 + 策略模式 + 配置文件 组合实现的动态加载机制, 用大白话解释就是说, 一个框架的设计为了后期的拓展性, 肯定先会在顶层设计接口, 然后再为这些接口提供一些默认的实现类, 未了良好的拓展性, 如果想让, 如果想实现允许当前框架 识别 / 加载 / 使用 第三方提供的 jar 包时 , 就可以使用 SPI 实现接口的动态加载, 只要遵循 SPI 的规范, java 就能将我们自己的类也加载进 JVM 供我们使用
说起来总归是模糊的, 看下面的小 Demo 自然就懂了
// 接口 public interface Person { String getName(); } // 实现类一: public class Student implements Person { @Override public String getName() { return "Student"; } } // 实现类二: public class Teacher implements Person { @Override public String getName() { return "Teacher"; } }
在 resource/META-INF/services/ 目录下面添加配置文件, 文件名称为 Person 接口的全限定名, 内容如下
com.changwu.javaspi.API.Student com.changwu.javaspi.API.Teacher
测试程序:
public class Test { public static void main(String[] args) { // 加载接口中的实现类 ServiceLoader<Person> load = ServiceLoader.load(Person.class); Iterator<Person> iterator = load.iterator(); while (iterator.hasNext()){ Person next = iterator.next(); System.out.println(next.getName()); } } }
测试结果控制台输出如下:
Student Teacher Dubbo SPI
Dubbo 自己也封装了一套 SPI 机制, 并将此作为它的扩展点, 如果我们有更好的想法, 可以使用 Dubbo 这个特性加将我们自己的类注入给 Dubbo, 它用法和 JDK 原生的 SPI 相似, 不同点在哪里呢? Dubbo 的更强大, 比如相对于 JDK 的 SPI , 它支持根据名称获取出指定的拓展类
一个小 demo
接口如下 , 注意点 Dubbo 的 SPI 需要在接口上标注注解 @SPI
@SPI public interface PersonInterface { String getName(); }
两个实现类
public class Student implements PersonInterface { @Override public String getName() { return "Student"; } } public class Teacher implements PersonInterface { @Override public String getName() { return "Teacher"; } }
配置文件
测试类 可以根据名称明确指出使用哪一个实现类
public class Test { public static void main(String[] args) { // todo 第一点: Dubbo 的 SPI 算作是他的一个可扩展的机制 ExtensionLoader<PersonInterface> extensionLoader = ExtensionLoader.getExtensionLoader(PersonInterface.class); PersonInterface carInterface = extensionLoader.getExtension("student"); System.out.println(carInterface.getName()); } } Dubbo IoC
Spring 的 IoC 肯定是鼎鼎大名的, 很直接的能想到 Spring 的 @Autowired 注解, 或者的配置文件版本的 <bean > 标签中可以帮我们自动维护 bean 之间的相互的依赖的关系
Dubbo 也实现了自己的 IoC
比如下面的代码这样: Human.java 中依赖了 PersonInterface 类型的对象, 打眼看上去, 这个对象肯定是借助我们提供的 setter 方法完成的注入
public class Human implements PersonInterface { private PersonInterface carInterface; //todo 第一个关注点: 我们的关注点就是说, BeazCar 会帮我们将哪一个实现类当成入参注入进来呢? //todo 答案是 URL ,Dubbo 自己封装的 URL, 统一资源定位符, Dubbo 会解析入参位置的 url 中封装的 map //todo map 中的 key 与 CarInteface 中的使用 @Adaptive("car") 注解标记的 value 对应, 那么值就是将要注入的实际类型 //todo 第二个关注点: Dubbo 底层很可能是通过反射使用构造方法完成的属性注入 public void setCarInterface(PersonInterface carInterface) { this.carInterface = carInterface; } @Override public String getColor(URL url) { System.out.println("i am Human"); return "i am Human +" + carInterface.getColor(url); } }
那么问题来了, 假如我们在配置文件中添加了多个 PersonInterface 接口的实现类, 那 Dubbo 是如何得知需要注入哪一个的呢? 答案就在入参位置的 URL 中, 也就是我在 知识储备二中提到的概念 URL
可以看下面这段测试代码, 怎么读下面的这段代码呢?
单独看 (PersonInterface) extensionLoader.getExtension("human"); 其实就是前面所说的 Dubbo 的 SPI 机制, 但是在这个基础上多出来的逻辑是啥呢? 是我们构建了一个 URL, 那为什么加进去一个 URL? 因为上面的示例代码说了, human 依赖了一个 PersonInterface 类型的变量, Dubbo 就是根据这个 URL 变量, 进而得知自己到底该该注入哪一个变量 Personinterface 实例的 (因为我提供了两个 一个是 Student , 另一个是 Teacher)
此外, 他需要的是 map , 我们给它的也是一个 hashmap , 特性就是 HashMap 的 key 是不重复的, 用大白话说, 它的底层肯定是 key=value 唯一绑定, 并且 key 也不会出现重复的情况
public class Test { public static void main(String[] args) { // todo 源码的入口, 进入 getExtensionLoader() ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(PersonInterface.class); HashMap<String, String> map = new HashMap<>(); map.put("human", "student"); URL url = new URL("","",1,map); // todo 继续跟进这个方法 PersonInterface carInterface = (PersonInterface) extensionLoader.getExtension("human"); System.out.println(carInterface.getName(url)); } }
那说了这么多, 到底注入的是哪一个对象呢? 从 map.put("human", "student"); 也能很清楚的看出来, 不就是 Student 吗? 是的, 确实是它, 但是还少了点东西, 就是 Personinterface 怎么编写呢? 如下:
// @SPI("stu") 可以给注解添加参数, 参数表示 CarInterface 的默认实现类 @SPI public interface PersonInterface { // todo 下面的注解很重要, 啥意思呢? 可以点进这个注解, 我有一些翻译 // 验证 AOP, 依然注入的信息从 url 中获取出来 @Adaptive("human") String getName(URL url); }
看上面的代码, 除了 @SPI 注解, 还有一个注解就是 @Adaptive 注解, 这个注解的 value 部分决定了 Dubbo 到底需要注入哪一个 ExtensionObject
因为 Dubbo 在启动的过程中会去读取 / META-INF/services/ Dubbo-SPI 配置文件, 并将每行数据读取维护在一个 map 中, key 就是我们自定义的名字, 值就是左边的全类名
看下面我们传递进去的是 human , 表示告诉 Dubbo, 让 Dubbo 拿着 human 去查找, 很显然 Dubbo 把我们前面传递给它的 student 找出来, 有了 Student 进一步再从上下文中所有的 ExtensionObject 中 (包含了我们在配置文件中添加进去的 Personinterface 的两个实现) 找到具体的注入对象
Dubbo AOP
还是说, AOP 是面向切面编程的思想, Spring 自己实现了一套, Dubbo 也实现了一套
验证 Dubbo 的 AOP 实现类如下:
Dubbo 的 AOP 增强实现和静态代理的编码方式相似, 比如我们就增强 PersonInterface 中的方法, 所以我们继承 PersonInterface, 提供构造方法入, 留给 Dubbo 通过反射完成指定目标对象的注入, 并在注入进来的目标对象的目标方法前后植入增强的逻辑
public class PersonWrapper implements PersonInterface { // todo 验证 Dubbo 的自动注入 private PersonInterface carInterface; // todo 根据构造方法进行注入 public PersonWrapper(PersonInterface in){ // 假设传递进来的就是具体的实现类 this.carInterface=in; } // todo 当我们将 CarWrapper 配置进 Dubbo 的 spi 中时, 通过 Dubbo 的 Spi 获取 CarInterface 执行时, 下面的方法就会被执行 @Override public String getName() { System.out.println("before..."); String color = carInterface.getName(); System.out.println("after..."); return "getName"; } }
那如何让 Dubbo 知道包含我们增强逻辑的 PersonWrapper 对象呢? 还是老样子, 通过配置文件完成, 如下:
测试如下:
public static void main(String[] args) { // todo 第一点: Dubbo 的 SPI 算作是他的一个可扩展的机制 ExtensionLoader<PersonInterface> extensionLoader = ExtensionLoader.getExtensionLoader(PersonInterface.class); PersonInterface carInterface = extensionLoader.getExtension("student"); System.out.println(carInterface.getName()); }
结果如下:
before... after... getName
如过存在多个 AOP 增强类, 比如从上到下出现的顺序是 w1 w2 ... 那么增强的逻辑添加顺序是 before2 before1
结语:
下一篇博文就是探究 Dubbo 的这些拓展点的底层实现细节了 , 还是挺带劲的...
最后打一个小广告: 我是 bloger 赐我白日梦, 本科大三在读, 热衷 java 研发, 期望有一份 Java 相关实习岗位的工作, 可以全职实习半年左右, 最理想城市是北京, 求大佬的内推哇
来源: https://www.cnblogs.com/ZhuChangwu/p/12174031.html