diff --git a/README.md b/README.md index afce08b..11df1f3 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ > > 6、所有更新日志,写作计划,公告等均在此发布 ==> [时间轴](https://imlql.cn/timeline/)。 > -> 7、由于现在上班挺忙的,更新频率会下降。但是基本可以保证一个月一篇文章(我文章基本都是万字长文这种)。具体的看下[时间轴](https://imlql.cn/timeline/) +> 7、由于现在上班挺忙的,更新频率会下降。具体安排看下[时间轴](https://imlql.cn/timeline/) @@ -23,14 +23,15 @@ # 目录 - [Java](#java) + - [基础](#基础) - [容器](#容器) - [并发](#并发) - [JVM](#JVM) - [各版本新特性](#各版本新特性) - - + + - [计算机网络](#计算机网络) @@ -181,6 +182,20 @@ AQS剩余部分,以及阻塞队列源码暂时先搁置一下。 [Netty入门-第三话](docs/netty/introduction/Netty入门-第三话.md):对前面两话一些迷惑的点进行细说,讲解handler调用机制,TCP粘包,以及用netty写一个十分简单的RPC + + +# RPC + +## Dubbo源码 + +[Dubbo基本应用与高级应用介绍](docs/rpc/dubbo/Dubbo源码系列V1-01和02.Dubbo第一二节-基本应用与高级应用.md) + +[Dubbo可扩展机制SPI源码解析](docs/rpc/dubbo/Dubbo源码系列V1-03.Dubbo第三节-可扩展机制SPI源码解析.md) + + + + + # Apollo [Apollo简单入门](docs/Apollo/Apollo简单入门.md) diff --git a/docs/design_patterns/behavior_type/设计模式-05.01-行为型-观察者&模板.md b/docs/design_patterns/behavior_type/设计模式-05.01-行为型-观察者&模板.md index e0a20e2..e734574 100644 --- a/docs/design_patterns/behavior_type/设计模式-05.01-行为型-观察者&模板.md +++ b/docs/design_patterns/behavior_type/设计模式-05.01-行为型-观察者&模板.md @@ -8,7 +8,7 @@ categories: - 05.行为型 keywords: 观察者模式,模板模式 description: 观察者模式,模板模式。很常用的两个模式 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/design_patterns/logo.jpg' abbrlink: dd09051e date: 2021-07-14 16:51:58 --- @@ -34,7 +34,7 @@ date: 2021-07-14 16:51:58 ### 方案一 - + @@ -721,11 +721,11 @@ public DObserver{ 1. Guava EventBus 的功能我们已经讲清楚了,总体上来说,还是比较简单的。接下来,我们就重复造轮子,“山寨”一个 EventBus 出来。 2. 我们重点来看,EventBus 中两个核心函数 register() 和 post() 的实现原理。弄懂了它们,基本上就弄懂了整个 EventBus 框架。下面两张图是这两个函数的实现原理图。 - + - + diff --git a/docs/design_patterns/behavior_type/设计模式-05.02-行为型-策略&职责链.md b/docs/design_patterns/behavior_type/设计模式-05.02-行为型-策略&职责链.md index f71a974..4db8bef 100644 --- a/docs/design_patterns/behavior_type/设计模式-05.02-行为型-策略&职责链.md +++ b/docs/design_patterns/behavior_type/设计模式-05.02-行为型-策略&职责链.md @@ -8,7 +8,7 @@ categories: - 05.行为型 keywords: 策略模式,职责链模式 description: 不多说,看文章 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/design_patterns/logo.jpg' abbrlink: 2c3cc5fd date: 2021-08-01 15:51:58 --- @@ -1171,7 +1171,7 @@ public class SensitiveWordFilter { Servlet Filter 是 Java Servlet 规范中定义的组件,翻译成中文就是过滤器,它可以实现对 HTTP 请求的过滤功能,比如鉴权、限流、记录日志、验证参数等等。因为它是 Servlet 规范的一部分,所以,只要是支持 Servlet 的 Web 容器(比如,Tomcat、Jetty 等),都支持过滤器功能。为了帮助你理解,我画了一张示意图阐述它的工作原理,如下所示。 - + 在实际项目中,我们该如何使用 Servlet Filter 呢?我写了一个简单的示例代码,如下所示。添加一个过滤器,我们只需要定义一个实现 javax.servlet.Filter 接口的过滤器类,并且将它配置在 web.xml 配置文件中。Web 容器启动的时候,会读取 web.xml 中的配置,创建过滤器对象。当有请求到来的时候,会先经过过滤器,然后才由 Servlet 来处理。 @@ -1279,7 +1279,7 @@ ApplicationFilterChain 中的 doFilter() 函数的代码实现比较有技巧, 1. 刚刚讲了 Servlet Filter,现在我们来讲一个功能上跟它非常类似的东西,Spring Interceptor,翻译成中文就是拦截器。尽管英文单词和中文翻译都不同,但这两者基本上可以看作一个概念,都用来实现对 HTTP 请求进行拦截处理。 2. 它们不同之处在于,Servlet Filter 是 Servlet 规范的一部分,实现依赖于 Web 容器。Spring Interceptor 是 Spring MVC 框架的一部分,由 Spring MVC 框架来提供实现。客户端发送的请求,会先经过 Servlet Filter,然后再经过 Spring Interceptor,最后到达具体的业务代码中。我画了一张图来阐述一个请求的处理流程,具体如下所示。 - + diff --git a/docs/design_patterns/behavior_type/设计模式-05.03-行为型-状态&迭代器.md b/docs/design_patterns/behavior_type/设计模式-05.03-行为型-状态&迭代器.md index 8732396..0cab072 100644 --- a/docs/design_patterns/behavior_type/设计模式-05.03-行为型-状态&迭代器.md +++ b/docs/design_patterns/behavior_type/设计模式-05.03-行为型-状态&迭代器.md @@ -8,7 +8,7 @@ categories: - 05.行为型 keywords: 状态模式,迭代器模式 description: 看文章 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/design_patterns/logo.jpg' abbrlink: 877f4ef2 date: 2021-08-02 15:51:58 --- @@ -30,7 +30,7 @@ date: 2021-08-02 15:51:58 4. 实际上,马里奥形态的转变就是一个状态机。其中,马里奥的不同形态就是状态机中的“状态”,游戏情节(比如吃了蘑菇)就是状态机中的“事件”,加减积分就是状态机中的“动作”。比如,吃蘑菇这个事件,会触发状态的转移:从小马里奥转移到超级马里奥,以及触发动作的执行(增加 100 积分)。 5. 为了方便接下来的讲解,我对游戏背景做了简化,只保留了部分状态和事件。简化之后的状态转移如下图所示: - + @@ -180,7 +180,7 @@ public class MarioStateMachine { 1. 实际上,上面这种实现方法有点类似 hard code,对于复杂的状态机来说不适用,而状态机的第二种实现方式查表法,就更加合适了。接下来,我们就一块儿来看下,如何利用查表法来补全骨架代码。 2. 实际上,除了用状态转移图来表示之外,状态机还可以用二维表来表示,如下所示。在这个二维表中,第一维表示当前状态,第二维表示事件,值表示当前状态经过事件之后,转移到的新状态及其执行的动作。 - + 3. 相对于分支逻辑的实现方式,查表法的代码实现更加清晰,可读性和可维护性更好。当修改状态机时,我们只需要修改 transitionTable 和 actionTable 两个二维数组即可。实际上,如果我们把这两个二维数组存储在配置文件中,当需要修改状态机时,我们甚至可以不修改任何代码,只需要修改配置文件就可以了。具体的代码如下所示: @@ -511,7 +511,7 @@ public class MarioStateMachine { 2. 在开篇中我们讲到,它用来遍历集合对象。这里说的“集合对象”也可以叫“容器”“聚合对象”,实际上就是包含一组对象的对象,比如数组、链表、树、图、跳表。迭代器模式将集合对象的遍历操作从集合类中拆分出来,放到迭代器类中,让两者的职责更加单一。 3. 迭代器是用来遍历容器的,所以,一个完整的迭代器模式一般会涉及**容器和容器迭代器**部分两部分内容。为了达到基于接口而非实现编程的目的,容器又包含容器接口、容器实现类,迭代器又包含迭代器接口、迭代器实现类。对于迭代器模式,我画了一张简单的类图,你可以看一看,先有个大致的印象。 - + @@ -629,7 +629,7 @@ public class Demo { 2. 结合刚刚的例子,我们来总结一下迭代器的设计思路。总结下来就三句话:迭代器中需要定义 hasNext()、currentItem()、next() 三个最基本的方法。待遍历的容器对象通过依赖注入传递到迭代器类中。容器通过 iterator() 方法来创建迭代器。 3. 这里我画了一张类图,如下所示。实际上就是对上面那张类图的细化,你可以结合着一块看。 - + @@ -752,7 +752,7 @@ public class Demo { 4. 为了保持数组存储数据的连续性,数组的删除操作会涉及元素的搬移。当执行到第 57 行代码的时候,我们从数组中将元素 a 删除掉,b、c、d 三个元素会依次往前搬移一位,这就会导致游标本来指向元素 b,现在变成了指向元素 c。原本在执行完第 56 行代码之后,我们还可以遍历到 b、c、d 三个元素,但在执行完第 57 行代码之后,我们只能遍历到 c、d 两个元素,b 遍历不到了。 5. 对于上面的描述,我画了一张图,你可以对照着理解。 - + 6. 不过,如果第 57 行代码删除的不是游标前面的元素(元素 a)以及游标所在位置的元素(元素 b),而是游标后面的元素(元素 c 和 d),这样就不会存在任何问题了,不会存在某个元素遍历不到的情况了。 7. 所以,我们前面说,在遍历的过程中删除集合元素,结果是不可预期的,有时候没问题(删除元素 c 或 d),有时候就有问题(删除元素 a 或 b),这个要视情况而定(到底删除的是哪个位置的元素),就是这个意思。 @@ -778,7 +778,7 @@ public class Demo { 10. 跟删除情况类似,如果我们在游标的后面添加元素,就不会存在任何问题。所以,在遍历的同时添加集合元素也是一种不可预期行为。 11. 同样,对于上面的添加元素的情况,我们也画了一张图,如下所示,你可以对照着理解。 - + diff --git a/docs/design_patterns/creational/设计模式-03.01-创建型-单例.md b/docs/design_patterns/creational/设计模式-03.01-创建型-单例.md index e9fb0c4..5e7b0db 100644 --- a/docs/design_patterns/creational/设计模式-03.01-创建型-单例.md +++ b/docs/design_patterns/creational/设计模式-03.01-创建型-单例.md @@ -8,7 +8,7 @@ categories: - 03.创建型 keywords: 设计模式,单例 description: 详解了单例设计模式。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/design_patterns/logo.jpg' abbrlink: b5a1ed4a date: 2021-06-26 21:51:58 --- diff --git a/docs/design_patterns/creational/设计模式-03.02-创建型-工厂&建造者&原型.md b/docs/design_patterns/creational/设计模式-03.02-创建型-工厂&建造者&原型.md index 73e2d34..a9c609a 100644 --- a/docs/design_patterns/creational/设计模式-03.02-创建型-工厂&建造者&原型.md +++ b/docs/design_patterns/creational/设计模式-03.02-创建型-工厂&建造者&原型.md @@ -10,7 +10,7 @@ categories: - 03.创建型 keywords: 设计模式,工厂,建造者,原型 description: 详解常用的工厂模式和建造者模式,以及不常用的原型模式 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/design_patterns/logo.jpg' abbrlink: ba432704 date: 2021-06-27 00:51:58 --- @@ -727,7 +727,7 @@ public class BeansFactory { 1. 在平时的开发中,创建一个对象最常用的方式是,使用 new 关键字调用类的构造函数来完成。我的问题是,什么情况下这种方式就不适用了,就需要采用建造者模式来创建对象呢?你可以先思考一下,下面我通过一个例子来带你看一下。 2. 假设有这样一道设计面试题:我们需要定义一个资源池配置类 ResourcePoolConfig。这里的资源池,你可以简单理解为线程池、连接池、对象池等。在这个资源池配置类中,有以下几个成员变量,也就是可配置项。现在,请你编写代码实现这个 ResourcePoolConfig 类。 - + @@ -1003,7 +1003,7 @@ r.setHeight(3); // r is valid 2. 如果你熟悉的是 Java 语言,可以直接使用语言中提供的 HashMap 容器来实现。其中,HashMap 的 key 为搜索关键词,value 为关键词详细信息(比如搜索次数)。我们只需要将数据从数据库中读取出来,放入 HashMap 就可以了。 3. 不过,我们还有另外一个系统 B,专门用来分析搜索日志,定期(比如间隔 10 分钟)批量地更新数据库中的数据,并且标记为新的数据版本。比如,在下面的示例图中,我们对 v2 版本的数据进行更新,得到 v3 版本的数据。这里我们假设只有更新和新添关键词,没有删除关键词的行为。 - + @@ -1150,15 +1150,15 @@ public class Demo { 我们来看,在内存中,用散列表组织的搜索关键词信息是如何存储的。我画了一张示意图,大致结构如下所示。从图中我们可以发现,散列表索引中,每个结点存储的 key 是搜索关键词,value 是 SearchWord 对象的内存地址。SearchWord 对象本身存储在散列表之外的内存空间中。 - + 浅拷贝和深拷贝的区别在于,浅拷贝只会复制图中的索引(散列表),不会复制数据(SearchWord 对象)本身。相反,深拷贝不仅仅会复制索引,还会复制数据本身。浅拷贝得到的对象(newKeywords)跟原始对象(currentKeywords)共享数据(SearchWord 对象),而深拷贝得到的是一份完完全全独立的对象。具体的对比如下图所示: - + - + diff --git a/docs/design_patterns/design_ideas/设计模式-01.设计思想.md b/docs/design_patterns/design_ideas/设计模式-01.设计思想.md index 706583d..bfb00e7 100644 --- a/docs/design_patterns/design_ideas/设计模式-01.设计思想.md +++ b/docs/design_patterns/design_ideas/设计模式-01.设计思想.md @@ -8,7 +8,7 @@ categories: - 01.设计思想 keywords: 设计模式,设计思想 description: 设计模式第一部分-常用设计思想。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/design_patterns/logo.jpg' abbrlink: c3dcce5d date: 2021-06-07 17:21:58 --- @@ -198,7 +198,7 @@ public class Ostrich extends AbstractBird { //鸵鸟 1. 这种设计思路虽然可以解决问题,但不够优美。因为除了鸵鸟之外,不会飞的鸟还有很多,比如企鹅。对于这些不会飞的鸟来说,我们都需要重写 fly() 方法,抛出异常。这样的设计,一方面,徒增了编码的工作量;另一方面,也违背了我们之后要讲的最小知识原则(Least Knowledge Principle,也叫最少知识原则或者迪米特法则),暴露不该暴露的接口给外部,增加了类使用过程中被误用的概率。 2. 你可能又会说,那我们再通过 AbstractBird 类派生出两个更加细分的抽象类:会飞的鸟类 AbstractFlyableBird 和不会飞的鸟类 AbstractUnFlyableBird,让麻雀、乌鸦这些会飞的鸟都继承 AbstractFlyableBird,让鸵鸟、企鹅这些不会飞的鸟,都继承 AbstractUnFlyableBird 类,不就可以了吗?具体的继承关系如下图所示: - + @@ -206,7 +206,7 @@ public class Ostrich extends AbstractBird { //鸵鸟 2. 是否会飞?是否会叫?两个行为搭配起来会产生四种情况:会飞会叫、不会飞会叫、会飞不会叫、不会飞不会叫。如果我们继续沿用刚才的设计思路,那就需要再定义四个抽象类(AbstractFlyableTweetableBird、AbstractFlyableUnTweetableBird、AbstractUnFlyableTweetableBird、AbstractUnFlyableUnTweetableBird)。 - + @@ -376,7 +376,7 @@ demofunction(client); - + diff --git a/docs/design_patterns/design_principles/设计模式-02.经典设计原则-第一节[必读].md b/docs/design_patterns/design_principles/设计模式-02.经典设计原则-第一节[必读].md index 0f9920d..3e179cd 100644 --- a/docs/design_patterns/design_principles/设计模式-02.经典设计原则-第一节[必读].md +++ b/docs/design_patterns/design_principles/设计模式-02.经典设计原则-第一节[必读].md @@ -8,7 +8,7 @@ categories: - 02.经典设计原则 keywords: 设计模式,经典设计原则 description: 设计模式-经典设计原则,例如:单一职责原则,开闭原则,接口隔离原则等等。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/design_patterns/logo.jpg' abbrlink: fc1c7619 date: 2021-06-13 19:21:58 --- diff --git a/docs/design_patterns/design_principles/设计模式-02.经典设计原则-第二节[必读].md b/docs/design_patterns/design_principles/设计模式-02.经典设计原则-第二节[必读].md index 6abc470..2dee587 100644 --- a/docs/design_patterns/design_principles/设计模式-02.经典设计原则-第二节[必读].md +++ b/docs/design_patterns/design_principles/设计模式-02.经典设计原则-第二节[必读].md @@ -8,7 +8,7 @@ categories: - 02.经典设计原则 keywords: 设计模式,经典设计原则 description: 设计模式-经典设计原则,例如:迪米特法则,依赖反转原则,KISS等等。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/design_patterns/logo.jpg' abbrlink: 994a8ed3 date: 2021-06-20 19:21:58 --- @@ -724,7 +724,7 @@ public class UserRepo { 前面也提到,“高内聚”有助于“松耦合”,同理,“低内聚”也会导致“紧耦合”。关于这一点,我画了一张对比图来解释。图中左边部分的代码结构是“高内聚、松耦合”;右边部分正好相反,是“低内聚、紧耦合”。 - + diff --git a/docs/design_patterns/structural_type/设计模式-04.01-结构型-代理&桥接&装饰器&适配器.md b/docs/design_patterns/structural_type/设计模式-04.01-结构型-代理&桥接&装饰器&适配器.md index 3e41c88..2d7b7e1 100644 --- a/docs/design_patterns/structural_type/设计模式-04.01-结构型-代理&桥接&装饰器&适配器.md +++ b/docs/design_patterns/structural_type/设计模式-04.01-结构型-代理&桥接&装饰器&适配器.md @@ -11,7 +11,7 @@ categories: - 04.结构型 keywords: 设计模式,代理模式,桥接模式,装饰器模式,适配器模式 description: 对代理模式,桥接模式,装饰器模式,适配器模式这4个模式进行了比较详细的讲述。其实学习设计模式主要是为了后序看源码 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/design_patterns/logo.jpg' abbrlink: 926a065c date: 2021-07-04 00:51:58 --- @@ -608,7 +608,7 @@ IUserController userController = (IUserController) proxy.createProxy(new UserCon 现在对不同手机类型的不同品牌实现操作编程(比如:开机、关机、上网,打电话等),如图: - + @@ -616,7 +616,7 @@ IUserController userController = (IUserController) proxy.createProxy(new UserCon 传统方法对应的类图 - + 1. 扩展性问题(**类爆炸**),如果我们再增加手机的样式(旋转式),就需要增加各个品牌手机的类,同样如果我们增加一个手机品牌,也要在各个手机样式类下增加。 2. 违反了单一职责原则,当我们增加手机样式时,要同时增加所有品牌的手机,这样增加了代码维护成本. @@ -933,7 +933,7 @@ public class DriverManager { - + @@ -1112,7 +1112,7 @@ public class TrivialNotification extends Notification { ### 方案一 - + @@ -1127,7 +1127,7 @@ public class TrivialNotification extends Notification { 前面分析到方案 1 因为咖啡单品+调料组合会造成类的倍增,因此可以做改进,将调料内置到 Drink 类,这样就不会造成类数量过多。从而提高项目的维护性(如图) - + @@ -1142,7 +1142,7 @@ public class TrivialNotification extends Notification { ### 装饰器模式代码 - + #### Drink【抽象类-主体Component】 @@ -1390,7 +1390,7 @@ Java IO 类库非常庞大和复杂,有几十个类,负责 IO 数据的读 针对不同的读取和写入场景,Java IO 又在这四个父类基础之上,扩展出了很多子类。具体如下所示: - + diff --git a/docs/design_patterns/structural_type/设计模式-04.02-结构型-门面&组合&享元.md b/docs/design_patterns/structural_type/设计模式-04.02-结构型-门面&组合&享元.md index d0879e8..397c8e7 100644 --- a/docs/design_patterns/structural_type/设计模式-04.02-结构型-门面&组合&享元.md +++ b/docs/design_patterns/structural_type/设计模式-04.02-结构型-门面&组合&享元.md @@ -10,7 +10,7 @@ categories: - 04.结构型 keywords: 设计模式,门面模式,组合模式,享元模式 description: 对代理模式,门面模式,组合模式,享元模式这3个设计模式进行了比较详细的讲述。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/design_patterns/logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/design_patterns/logo.jpg' abbrlink: 5ea604e0 date: 2021-07-05 16:51:58 --- @@ -75,7 +75,7 @@ date: 2021-07-05 16:51:58 ### 传统方案 - + 1. 在 ClientTest 的 main 方法中,创建各个子系统的对象,并直接去调用子系统(对象)相关方法,会造成调用过程 混乱,没有清晰的过程 不利于在 ClientTest 中,去维护对子系统的操作 @@ -623,7 +623,7 @@ public class Demo { - + diff --git a/docs/netty/introduction/Netty入门-第一话.md b/docs/netty/introduction/Netty入门-第一话.md index 5d97ebc..fc90114 100644 --- a/docs/netty/introduction/Netty入门-第一话.md +++ b/docs/netty/introduction/Netty入门-第一话.md @@ -7,7 +7,7 @@ categories: - 入门 keywords: Netty description: 第一话对BIO和NIO进行了讲解,为后续做准备。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/netty/netty_logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/netty/netty_logo.jpg' abbrlink: 3f9283e7 date: 2021-04-08 14:21:58 --- @@ -33,7 +33,7 @@ date: 2021-04-08 14:21:58 相对简单的一个体系图 - + ## Netty 的应用场景 @@ -67,7 +67,7 @@ date: 2021-04-08 14:21:58 ## Netty 的学习资料参考 - + @@ -97,11 +97,11 @@ date: 2021-04-08 14:21:58 2. `Java` 共支持 `3` 种网络编程模型 `I/O` 模式:`BIO`、`NIO`、`AIO`。 3. `Java BIO`:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。【简单示意图】 - + 4. `Java NIO`:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 `I/O` 请求就进行处理。【简单示意图】 - + 5. `Java AIO(NIO.2)`:异步非阻塞,`AIO` 引入异步通道的概念,采用了 `Proactor` 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。 6. 我们依次展开讲解。 @@ -120,7 +120,7 @@ date: 2021-04-08 14:21:58 ## Java BIO 工作机制 - + 对 `BIO` 编程流程的梳理 @@ -207,7 +207,7 @@ public class BIOServer { } ``` - + @@ -277,7 +277,7 @@ public class BasicBuffer { 3. `BIO` 基于字节流和字符流进行操作,而 `NIO` 基于 `Channel`(通道)和 `Buffer`(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。`Selector`(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道。 4. Buffer和Channel之间的数据流向是双向的 - + ## NIO 三大核心原理示意图 @@ -287,7 +287,7 @@ public class BasicBuffer { 关系图的说明: - + 1. 每个 `Channel` 都会对应一个 `Buffer`。 2. `Selector` 对应一个线程,一个线程对应多个 `Channel`(连接)。 @@ -303,17 +303,17 @@ public class BasicBuffer { 缓冲区(`Buffer`):缓冲区本质上是一个**可以读写数据的内存块**,可以理解成是一个**容器对象(含数组)**,该对象提供了一组方法,可以更轻松地使用内存块,,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。`Channel` 提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 `Buffer`,如图:【后面举例说明】 - + ### Buffer 类及其子类 1. 在 `NIO` 中,`Buffer` 是一个顶层父类,它是一个抽象类,类的层级关系图: - + 2. `Buffer` 类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息: - + @@ -321,13 +321,13 @@ public class BasicBuffer { 3. `Buffer` 类相关方法一览 - + ### ByteBuffer 从前面可以看出对于 `Java` 中的基本数据类型(`boolean` 除外),都有一个 `Buffer` 类型与之相对应,最常用的自然是 `ByteBuffer` 类(二进制数据),该类的主要方法如下: - + ## 通道(Channel) @@ -343,7 +343,7 @@ public class BasicBuffer { 5. `FileChannel` 用于文件的数据读写,`DatagramChannel` 用于 `UDP` 的数据读写,`ServerSocketChannel` 和 `SocketChannel` 用于 `TCP` 的数据读写。 6. 图示 - + ### FileChannel 类 @@ -444,7 +444,7 @@ public class NIOFileChannel02 { 2. 拷贝一个文本文件 `1.txt`,放在项目下即可 3. 代码演示 - + ```java package com.atguigu.nio; @@ -721,7 +721,7 @@ public class ScatteringAndGatheringTest { ### Selector 示意图和特点说明 - + 说明如下: @@ -733,7 +733,7 @@ public class ScatteringAndGatheringTest { ### Selector 类相关方法 - + ### 注意事项 @@ -748,7 +748,7 @@ public class ScatteringAndGatheringTest { `NIO` 非阻塞网络编程相关的(`Selector`、`SelectionKey`、`ServerScoketChannel` 和 `SocketChannel`)关系梳理图 - + 对上图的说明: @@ -938,21 +938,21 @@ public static final int OP_ACCEPT = 1 << 4; 2. `SelectionKey` 相关方法 - + ### ServerSocketChannel 1. `ServerSocketChannel` 在服务器端监听新的客户端 `Socket` 连接,负责监听,不负责实际的读写操作 2. 相关方法如下 - + ### SocketChannel 1. `SocketChannel`,网络 `IO` 通道,**具体负责进行读写操作**。`NIO` 把缓冲区的数据写入通道,或者把通道里的数据读到缓冲区。 2. 相关方法如下 - + ## NIO网络编程应用实例 - 群聊系统 @@ -965,7 +965,7 @@ public static final int OP_ACCEPT = 1 << 4; 5. 目的:进一步理解 `NIO` 非阻塞网络编程机制 6. 示意图分析和代码 - + 代码: @@ -1243,7 +1243,7 @@ socket.getOutputStream().write(arr); ### 传统 IO 模型 - + **DMA**:`direct memory access` 直接内存拷贝(不使用 `CPU`) @@ -1252,19 +1252,19 @@ socket.getOutputStream().write(arr); 1. `mmap` 通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图 2. `mmap` 示意图 - + ### sendFile 优化 1. `Linux2.1` 版本提供了 `sendFile` 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 `SocketBuffer`,同时,由于和用户态完全无关,就减少了一次上下文切换 2. 示意图和小结 - + 3. 提示:零拷贝从操作系统角度,是没有 `cpu` 拷贝 4. `Linux在2.4` 版本中,做了一些修改,避免了从内核缓冲区拷贝到 `Socketbuffer` 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝。具体如下图和小结: - + 5. 这里其实有一次 `cpu` 拷贝 `kernel buffer` -> `socket buffer` 但是,拷贝的信息很少,比如 `lenght`、`offset` 消耗低,可以忽略 diff --git a/docs/netty/introduction/Netty入门-第三话.md b/docs/netty/introduction/Netty入门-第三话.md index f50a3bc..ad7f2ad 100644 --- a/docs/netty/introduction/Netty入门-第三话.md +++ b/docs/netty/introduction/Netty入门-第三话.md @@ -7,7 +7,7 @@ categories: - 入门 keywords: Netty description: 对前面两话一些迷惑的点进行细说,讲解handler调用机制,TCP粘包,以及用netty写一个十分简单的RPC。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/netty/netty_logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/netty/netty_logo.jpg' abbrlink: 429acc6d date: 2021-04-21 17:38:58 --- @@ -25,7 +25,7 @@ date: 2021-04-21 17:38:58 1. 编写网络应用程序时,因为数据在网络中传输的都是二进制字节码数据,在发送数据时就需要编码,接收数据时就需要解码[示意图] 2. `codec`(编解码器)的组成部分有两个:`decoder`(解码器)和 `encoder`(编码器)。`encoder` 负责把业务数据转换成字节码数据,`decoder` 负责把字节码数据转换成业务数据 - + ## Netty 本身的编码解码的机制和问题分析 @@ -54,7 +54,7 @@ date: 2021-04-21 17:38:58 8. 然后通过 `protoc.exe` 编译器根据 `.proto` 自动生成 `.java` 文件 9. `protobuf` 使用示意图 - + ## Protobuf 快速入门实例 @@ -91,7 +91,7 @@ message Student { //会在 StudentPOJO 外部类生成一个内部类 Student, protoc.exe --java_out=.Student.proto 将生成的 StudentPOJO 放入到项目使用 - + 生成的StudentPOJO代码太长就不贴在这里了 @@ -676,7 +676,7 @@ public class NettyClientHandler extends ChannelInboundHandlerAdapter { 2. `ChannelHandler` 充当了处理入站和出站数据的应用程序逻辑的容器。例如,实现 `ChannelInboundHandler` 接口(或 `ChannelInboundHandlerAdapter`),你就可以接收入站事件和数据,这些数据会被业务逻辑处理。当要给客户端发送响应时,也可以从 `ChannelInboundHandler` 冲刷数据。业务逻辑通常写在一个或者多个 `ChannelInboundHandler` 中。`ChannelOutboundHandler` 原理一样,只不过它是用来处理出站数据的 3. `ChannelPipeline` 提供了 `ChannelHandler` 链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过 `pipeline` 中的一系列 `ChannelOutboundHandler`,并被这些 `Handler` 处理,反之则称为入站的 - + > 出站,入站如果搞不清楚,看下面的**Netty的handler链的调用机制**,通过一个例子和图讲清楚 @@ -689,12 +689,12 @@ public class NettyClientHandler extends ChannelInboundHandlerAdapter { 1. 关系继承图 - + 2. 由于不可能知道远程节点是否会一次性发送一个完整的信息,`tcp` 有可能出现粘包拆包的问题,这个类会对入站数据进行缓冲,直到它准备好被处理.【后面有说TCP的粘包和拆包问题】 3. 一个关于 `ByteToMessageDecoder` 实例分析 - + @@ -710,7 +710,7 @@ public class NettyClientHandler extends ChannelInboundHandlerAdapter { > 读者可以看下这个图,带着这个图去看下面的例子。 - + @@ -978,11 +978,11 @@ public class MyLongToByteEncoder extends MessageToByteEncoder { ### 效果 - + - + @@ -1002,7 +1002,7 @@ public class MyLongToByteEncoder extends MessageToByteEncoder { ​ - + @@ -1090,13 +1090,13 @@ public class MyByteToLongDecoder extends ByteToMessageDecoder { 如下图验证结果: - + 2. 同时又引出了一个小问题 - + 当我们`MyClientHandler`传一个Long时,会调用我们的`MyLongToByteEncoder`的编码器。那么控制台就会打印这样一句话:**MyLongToByteEncoder encode 被调用**。但是这里并没有调用编码器,这是为什么呢? @@ -1149,7 +1149,7 @@ public class MyByteToLongDecoder extends ByteToMessageDecoder { ctx.writeAndFlush(Unpooled.copiedBuffer("abcdabcdabcdabcd",CharsetUtil.UTF_8)); ``` - + @@ -1198,7 +1198,7 @@ public class MyByteToLongDecoder2 extends ReplayingDecoder { ## 其它编解码器 - + @@ -1249,7 +1249,7 @@ log4j.appender.stdout.layout.ConversionPattern=[%p]%C{1}-%m%n 3. 演示整合 - + @@ -1261,7 +1261,7 @@ log4j.appender.stdout.layout.ConversionPattern=[%p]%C{1}-%m%n 2. 由于 `TCP` 无消息保护边界,需要在接收端处理消息边界问题,也就是我们所说的粘包、拆包问题,看一张图 3. `TCP` 粘包、拆包图解 - + 假设客户端分别发送了两个数据包 `D1` 和 `D2` 给服务端,由于服务端一次读取到字节数是不确定的,故可能存在以下四种情况: @@ -1502,11 +1502,11 @@ public class MyClientHandler extends SimpleChannelInboundHandler { **Client** - + **Server** - + @@ -1514,13 +1514,13 @@ public class MyClientHandler extends SimpleChannelInboundHandler { **Client** - + **Server** - + @@ -1538,7 +1538,7 @@ public class MyClientHandler extends SimpleChannelInboundHandler { 1. 要求客户端发送 `5` 个 `Message` 对象,客户端每次发送一个 `Message` 对象 2. 服务器端每次接收一个 `Message`,分 `5` 次进行解码,每读取到一个 `Message`,会回复一个 `Message` 对象给客户端。 - + @@ -1996,7 +1996,7 @@ MyMessageEncoder encode 方法被调用 1. `RPC(Remote Procedure Call)`—远程过程调用,是一个计算机通信协议。该协议允许运行于一台计算机的程序调用另一台计算机的子程序,而程序员无需额外地为这个交互作用编程 2. 两个或多个应用程序都分布在不同的服务器上,它们之间的调用都像是本地方法调用一样(如图) - + 过程: @@ -2018,11 +2018,11 @@ MyMessageEncoder encode 方法被调用 3. 常见的 `RPC` 框架有:比较知名的如阿里的 `Dubbo`、`Google` 的 `gRPC`、`Go` 语言的 `rpcx`、`Apache` 的 `thrift`,`Spring` 旗下的 `SpringCloud`。 - + ## 我们的RPC 调用流程图 - + **RPC 调用流程说明** @@ -2052,7 +2052,7 @@ MyMessageEncoder encode 方法被调用 3. 创建一个消费者,该类需要透明的调用自己不存在的方法,内部需要使用 `Netty` 请求提供者返回数据 4. 开发的分析图 - + diff --git a/docs/netty/introduction/Netty入门-第二话.md b/docs/netty/introduction/Netty入门-第二话.md index a96fe97..7ed2153 100644 --- a/docs/netty/introduction/Netty入门-第二话.md +++ b/docs/netty/introduction/Netty入门-第二话.md @@ -7,7 +7,7 @@ categories: - 入门 keywords: Netty description: 对Netty的架构进行了解析,主要是Reactor设计模式的多种解决方案。同时讲解了Netty的核心模块组件。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/netty/netty_logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/netty/netty_logo.jpg' abbrlink: f846f3f date: 2021-04-15 14:21:58 --- @@ -29,7 +29,7 @@ date: 2021-04-15 14:21:58 Netty is an asynchronous event-driven network application framework for rapid development of maintainable high performance protocol servers & clients. - + ## Netty 的优点 @@ -80,7 +80,7 @@ Netty is an asynchronous event-driven network application framework for rapid de 1. 当并发数很大,就会创建大量的线程,占用很大系统资源 2. 连接创建后,如果当前线程暂时没有数据可读,该线程会阻塞在 Handler对象中的`read` 操作,导致上面的处理线程资源浪费 - + ## Reactor 模式 @@ -97,7 +97,7 @@ Netty is an asynchronous event-driven network application framework for rapid de 1. 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。(解决了当并发数很大时,会创建大量线程,占用很大系统资源) 2. 基于 `I/O` 复用模型:多个客户端进行连接,先把连接请求给`ServiceHandler`。多个连接共用一个阻塞对象`ServiceHandler`。假设,当C1连接没有数据要处理时,C1客户端只需要阻塞于`ServiceHandler`,C1之前的处理线程便可以处理其他有数据的连接,不会造成线程资源的浪费。当C1连接再次有数据时,`ServiceHandler`根据线程池的空闲状态,将请求分发给空闲的线程来处理C1连接的任务。(解决了线程资源浪费的那个问题) - + @@ -105,7 +105,7 @@ Netty is an asynchronous event-driven network application framework for rapid de ### I/O 复用结合线程池,就是 Reactor 模式基本设计思想,如图 - + 对上图说明: @@ -132,7 +132,7 @@ Netty is an asynchronous event-driven network application framework for rapid de 原理图,并使用 `NIO` 群聊系统验证 - + ### 方案说明 @@ -155,7 +155,7 @@ Netty is an asynchronous event-driven network application framework for rapid de ### 方案说明 - + @@ -178,11 +178,11 @@ Netty is an asynchronous event-driven network application framework for rapid de 针对单 `Reactor` 多线程模型中,`Reactor` 在单线程中运行,高并发场景下容易成为性能瓶颈,可以让 `Reactor` 在多线程中运行 - + - + > SubReactor是可以有多个的,如果只有一个SubReactor的话那和`单 Reactor 多线程`就没什么区别了。 @@ -197,7 +197,7 @@ Netty is an asynchronous event-driven network application framework for rapid de ### Scalable IO in Java 对 Multiple Reactors 的原理图解 - + ### 方案优缺点说明 @@ -230,7 +230,7 @@ Netty is an asynchronous event-driven network application framework for rapid de `Netty` 主要基于主从 `Reactors` 多线程模型(如图)做了一定的改进,其中主从 `Reactor` 多线程模型有多个 `Reactor` - + **对上图说明** @@ -240,7 +240,7 @@ Netty is an asynchronous event-driven network application framework for rapid de ### 工作原理示意图2 - 进阶版 - + @@ -248,7 +248,7 @@ Netty is an asynchronous event-driven network application framework for rapid de ### 工作原理示意图3 - 详细版 - + @@ -669,9 +669,9 @@ public class NettyServerHandler extends ChannelInboundHandlerAdapter { 下面第一张图就是管道,中间会经过多个handler - + - + 说明: @@ -917,11 +917,11 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + 4. 我们经常需要自定义一个 `Handler` 类去继承 `ChannelInboundHandlerAdapter`,然后通过重写相应方法实现业务逻辑,我们接下来看看一般都需要重写哪些方法 - + ## Pipeline 和 ChannelPipeline @@ -931,7 +931,7 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + 4. 常用方法 `ChannelPipeline addFirst(ChannelHandler... handlers)`,把一个业务处理类(`handler`)添加到链中的第一个位置`ChannelPipeline addLast(ChannelHandler... handlers)`,把一个业务处理类(`handler`)添加到链中的最后一个位置 @@ -940,7 +940,7 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + - `TestServerInitializer`和`HttpServerCodec`这些东西本身也是`handler` - 一般来说事件从客户端往服务器走我们称为出站,反之则是入站。 @@ -950,7 +950,7 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + 3. 常用方法 @@ -959,14 +959,14 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + ## ChannelOption 1. `Netty` 在创建 `Channel` 实例后,一般都需要设置 `ChannelOption` 参数。 2. `ChannelOption` 参数如下: - + ## EventLoopGroup 和其实现类 NioEventLoopGroup @@ -974,7 +974,7 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + 4. 常用方法 `public NioEventLoopGroup()`,构造方法 @@ -985,11 +985,11 @@ public class TestHttpServerHandler extends SimpleChannelInboundHandler + 3. 举例说明 `Unpooled` 获取 `Netty` 的数据容器 `ByteBuf` 的基本使用 - + 案例 1 @@ -1096,7 +1096,7 @@ public class NettyByteBuf02 { - + 代码如下: @@ -1523,7 +1523,7 @@ public class MyServerHandler extends ChannelInboundHandlerAdapter { 4. 客户端浏览器和服务器端会相互感知,比如服务器关闭了,浏览器会感知,同样浏览器关闭了,服务器会感知 5. 运行界面 - + @@ -1724,7 +1724,7 @@ public class MyTextWebSocketFrameHandler extends SimpleChannelInboundHandler + diff --git a/docs/os/操作系统-IO与零拷贝.md b/docs/os/操作系统-IO与零拷贝.md index 2279387..a5f2838 100644 --- a/docs/os/操作系统-IO与零拷贝.md +++ b/docs/os/操作系统-IO与零拷贝.md @@ -9,7 +9,7 @@ categories: - 操作系统 keywords: 操作系统,IO,零拷贝 description: 基本面试会问到的IO进行了详解,同时本篇文章也对面试以及平时工作中会看到的零拷贝进行了充分的解析。万字长文系列,读到就是赚到。 -cover: 'https://cdn.jsdelivr.net/gh/youthlql/lqlp@master/os/os_logo.jpg' +cover: 'https://unpkg.zhimg.com/youthlql@1.0.0/os/os_logo.jpg' abbrlink: e959db2e date: 2021-04-08 15:21:58 --- @@ -90,7 +90,7 @@ date: 2021-04-08 15:21:58 4. 通过Reactor的方式,可以将用户线程轮询IO操作状态的工作统一交给handle_events事件循环进行处理。用户线程注册事件处理器之后可以继续执行做其他的工作(异步),而Reactor线程负责调用内核的select函数检查socket状态。当有socket被激活时(就是数据准备好的时候),则通知相应的用户线程(或执行用户线程的回调函数),执行handle_event进行数据读取、处理的工作。 5. 由于select函数是阻塞的,因此多路IO复用模型也被称为异步阻塞IO模型。注意,这里的所说的阻塞是指select函数执行时线程被阻塞,而不是指socket。(一般在使用IO多路复用模型时,socket都是设置为NONBLOCK的,不过这并不会产生影响,因为用户发起IO请求时,数据已经到达了,用户线程一定不会被阻塞。) - + @@ -196,7 +196,7 @@ date: 2021-04-08 15:21:58 1. 来看一个标准设备(不是真实存在的,相当于一个逻辑上抽象的东西),通过它来帮助我们更好地理解设备交互的机制。可以看到一个包含两部分重要组件的设备。第一部分是向系统其他部分展现的硬件接口(interface)。同软件一样,硬件也需要一些接口,让系统软件来控制它的操作。因此,所有设备都有自己的特定接口以及典型交互的协议。 - + 2. 第2部分是它的内部结构(internal structure)。这部分包含设备相关的特定实现,负责具体实现设备展示给系统的抽象接口。 @@ -231,11 +231,11 @@ While (STATUS == BUSY);//wait until device is done with your request 1. 没有中断时:进程1在CPU上运行一段时间(对应CPU那一行上重复的1),然后发出一个读取数据的I/O请求给磁盘。如果没有中断,那么操作系统就会简单自旋,不断轮询设备状态,直到设备完成I/O操作(对应其中的p)。当设备完成请求的操作后,进程1又可以继续运行。 - + 2. 有了中断后:中断允许计算与I/O重叠(overlap),这是提高CPU利用率的关键。我们利用中断并允许重叠,操作系统就可以在等待磁盘操作时做其他事情。 - + - 在这个例子中,在磁盘处理进程1的请求时,操作系统在CPU上运行进程2。磁盘处理完成后,触发一个中断,然后操作系统唤醒进程1继续运行。这样,在这段时间,无论CPU还是磁盘都可以有效地利用。 @@ -243,7 +243,7 @@ While (STATUS == BUSY);//wait until device is done with your request 中断仍旧存在的缺点: - + IO过程简述: @@ -268,7 +268,7 @@ IO过程简述: > > 标准协议还有一点需要我们注意。具体来说,如果使用编程的I/O将一大块数据传给设备,CPU又会因为琐碎的任务而变得负载很重,浪费了时间和算力,本来更好是用于运行其他进程。下面的时间线展示了这个问题: > -> +> > > 进程1在运行过程中需要向磁盘写一些数据,所以它开始进行I/O操作,将数据从内存拷贝到磁盘(其中标示c的过程)。**拷贝结束后,磁盘上的I/O操作开始执行,此时CPU才可以处理其他请求。** @@ -278,13 +278,13 @@ IO过程简述: > > DMA工作过程如下。为了能够将数据传送给设备,操作系统会通过编程告诉DMA引擎数据在内存的位置,要拷贝的大小以及要拷贝到哪个设备。在此之后,操作系统就可以处理其他请求了。当DMA的任务完成后,DMA控制器会抛出一个中断来告诉操作系统自己已经完成数据传输。修改后的时间线如下: > -> +> > > 从时间线中可以看到,数据的拷贝工作都是由DMA控制器来完成的。因为CPU在此时是空闲的,所以操作系统可以让它做一些其他事情,比如此处调度进程2到CPU来运行。因此进程2在进程1再次运行之前可以使用更多的CPU。 为了更好理解,看图: - + 过程: @@ -302,7 +302,7 @@ IO过程简述: 场景:将磁盘上的文件读取出来,然后通过网络协议发送给客户端。 - + 1. 很明显发生了4次拷贝 @@ -326,7 +326,7 @@ IO过程简述: `read()` 系统调用的过程中会把内核缓冲区的数据拷贝到用户的缓冲区里,为了减少这一步开销,我们可以用 `mmap()` 替换 `read()` 系统调用函数。`mmap()` 系统调用函数会直接把内核缓冲区里的数据映射到用户空间,这样,操作系统内核与用户空间共享缓冲区,就不需要再进行任何的数据拷贝操作。 - + 总的来说mmap减少了一次数据拷贝,总共4次上下文切换,3次数据拷贝 @@ -336,7 +336,7 @@ IO过程简述: `Linux2.1` 版本提供了 `sendFile` 函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到 `SocketBuffer` - + 总的来说有2次上下文切换,3次数据拷贝。 @@ -344,7 +344,7 @@ IO过程简述: `Linux在2.4` 版本中,做了一些修改,避免了从内核缓冲区拷贝到 `Socketbuffer` 的操作,直接拷贝到协议栈,从而再一次减少了数据拷贝 - + diff --git a/docs/rpc/dubbo/Dubbo源码系列V1-01和02.Dubbo第一二节-基本应用与高级应用.md b/docs/rpc/dubbo/Dubbo源码系列V1-01和02.Dubbo第一二节-基本应用与高级应用.md new file mode 100644 index 0000000..9c25b9a --- /dev/null +++ b/docs/rpc/dubbo/Dubbo源码系列V1-01和02.Dubbo第一二节-基本应用与高级应用.md @@ -0,0 +1,1040 @@ +--- +title: Dubbo源码系列V1-01和02.Dubbo第一二节-基本应用与高级应用 +tags: + - Dubbo + - rpc +categories: + - rpc + - Dubbo源码系列v1 +keywords: Dubbo,rpc +description: 前两节合成一节 +cover: 'https://cdn.jsdelivr.net/gh/youthlql/youthlql/img/dubbo.png' +abbrlink: d3c530c4 +date: 2021-09-11 15:21:58 +--- + + + + + + + + +# Dubbo源码 + + + +## 第一节: Dubbo框架介绍 + +### 什么是RPC + +**维基百科**是这么定义RPC的: + + + +> 在[分布式计算](https://zh.wikipedia.org/wiki/%E5%88%86%E5%B8%83%E5%BC%8F%E8%AE%A1%E7%AE%97)**,远程过程调用**(英语:Remote Procedure Call,缩写为 RPC)是一个计算机通信[协议](https://zh.wikipedia.org/wiki/%E7%B6%B2%E7%B5%A1%E5%82%B3%E8%BC%B8%E5%8D%94%E8%AD%B0)。该协议允许运行于一台计算机的[程序](https://zh.wikipedia.org/wiki/%E7%A8%8B%E5%BA%8F)调用另一个[地址空间](https://zh.wikipedia.org/wiki/%E5%9C%B0%E5%9D%80%E7%A9%BA%E9%97%B4)(通常为一个开放网络的一台计算机)的[子程序](https://zh.wikipedia.org/wiki/%E5%AD%90%E7%A8%8B%E5%BA%8F),而程序员就像调用本地程序一样,无需额外地为这个交互作用编程(无需关注细节)。RPC是一种服务器-客户端(Client/Server)模式,经典实现是一个通过**发送请求-接受回应**进行信息交互的系统。 +> +> +> +> 如果涉及的软件采用[面向对象编程](https://zh.wikipedia.org/wiki/%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E7%BC%96%E7%A8%8B),那么远程过程调用亦可称作**远程调用**或**远程方法调用**,例:[Java RMI](https://zh.wikipedia.org/wiki/Java_RMI)。 + + + +所以,对于Java程序员而言,RPC就是**远程方法调用**。 + + + +**远程方法调用**和**本地方法调用**是相对的两个概念,本地方法调用指的是进程内部的方法调用,而远程方法调用指的是两个进程内的方法相互调用。 + + + +如果实现远程方法调用,基本的就是通过网络,通过传输数据来进行调用。 + + + +所以就有了: + +1. RPC over Http:基于Http协议来传输数据 +2. PRC over Tcp:基于Tcp协议来传输数据 + + + +对于所传输的数据,可以交由RPC的双方来协商定义,但基本都会包括: + +1. 调用的是哪个类或接口 +2. 调用的是哪个方法,方法名和方法参数类型(考虑方法重载) +3. 调用方法的入参 + + + +所以,我们其实可以看到RPC的自定义性是很高的,各个公司内部都可以实现自己的一套RPC框架,而**Dubbo**就是阿里所开源出来的一套RPC框架。 + + + +### 什么是Dubbo + +官网地址:[http://dubbo.apache.org/zh/](http://dubbo.apache.org/zh/) + + + +目前,官网上是这么介绍的:**Apache Dubbo 是一款高性能、轻量级的开源 Java服务框架** + +在几个月前,官网的介绍是:**Apache Dubbo 是一款高性能、轻量级的开源 JavaRPC框架** + + + +为什么会将**RPC**改为**服务**? + + + +Dubbo一开始的定位就是RPC,专注于两个服务之间的调用。但随着微服务的盛行,除开**服务调用**之外,Dubbo也在逐步的涉猎服务治理、服务监控、服务网关等等,所以现在的Dubbo目标已经不止是RPC框架了,而是和Spring Cloud类似想成为了一个**服务**框架。 + + + +Dubbo网关参考:[https://github.com/apache/dubbo-proxy](https://github.com/apache/dubbo-proxy)(社区不是很活跃) + + + +### 基本原理 + + + + + +### 开源RPC框架对比 + +| 功能 | Hessian | Montan | rpcx | gRPC | Thrift | Dubbo | Dubbox | Spring Cloud | +| ---------------- | ------- | ---------------------------- | ------ | ----------------- | ------------- | ------- | -------- | ------------ | +| 开发语言 | 跨语言 | Java | Go | 跨语言 | 跨语言 | Java | Java | Java | +| 分布式(服务治理) | × | √ | √ | × | × | √ | √ | √ | +| 多序列化框架支持 | hessian | √(支持Hessian2、Json,可扩展) | √ | × 只支持protobuf) | ×(thrift格式) | √ | √ | √ | +| 多种注册中心 | × | √ | √ | × | × | √ | √ | √ | +| 管理中心 | × | √ | √ | × | × | √ | √ | √ | +| 跨编程语言 | √ | ×(支持php client和C server) | × | √ | √ | × | × | × | +| 支持REST | × | × | × | × | × | × | √ | √ | +| 关注度 | 低 | 中 | 低 | 中 | 中 | 中 | 高 | 中 | +| 上手难度 | 低 | 低 | 中 | 中 | 中 | 低 | 低 | 中 | +| 运维成本 | 低 | 中 | 中 | 中 | 低 | 中 | 中 | 中 | +| 开源机构 | Caucho | Weibo | Apache | Google | Apache | Alibaba | Dangdang | Apache | + +## 第二节: Dubbo的基本应用与高级应用 + + + +官网:[http://dubbo.apache.org/zh/docs/v2.7/user/](http://dubbo.apache.org/zh/docs/v2.7/user/) + +管理台github地址:[https://github.com/apache/dubbo-admin](https://github.com/apache/dubbo-admin) + + + +Dubbo提供了很多功能,这里我们只介绍几种比较重要的,其他功能可以去Dubbo官网上查看。 + + + +### 项目目录 + +```java +dubbo-youthlql-demo +├── consumer/ +| ├── consumer.iml +| ├── pom.xml +| └── src/ +| ├── main/ +| | ├── java/ +| | | └── com/ +| | | └── youthlql/ +| | | ├── consumer/ +| | | | ├── AsyncDubboConsumerDemo.java +| | | | ├── CallbackDubboConsumerDemo.java +| | | | ├── ClusterDubboConsumerDemo.java +| | | | ├── DemoServiceListenerImpl.java +| | | | ├── GenericDubboConsumerDemo.java +| | | | ├── LoadBalanceDubboConsumerDemo.java +| | | | ├── MockDubboConsumerDemo.java +| | | | ├── StubDubboConsumerDemo.java +| | | | └── TimeoutDubboConsumerDemo.java +| | | ├── controller/ +| | | | ├── ConsumerInterceptor.java +| | | | └── HelloController.java +| | | └── DubboConsumerDemo.java +| | └── resources/ +| | └── application.yml +| └── test/ +| └── java/ +├── dubbo-youthlql-demo.iml +├── interface/ +| ├── interface.iml +| ├── pom.xml +| └── src/ +| ├── main/ +| | ├── java/ +| | | └── com/ +| | | └── youthlql/ +| | | ├── DemoService.java +| | | ├── DemoServiceListener.java +| | | ├── DemoServiceMock.java +| | | └── DemoServiceStub.java +| | └── resources/ +| └── test/ +| └── java/ +├── pom.xml +└── provider/ + ├── pom.xml + ├── provider.iml + └── src/ + ├── main/ + | ├── java/ + | | └── com/ + | | └── youthlql/ + | | ├── DubboProviderDemo.java + | | ├── provider/ + | | | └── service/ + | | | ├── AsyncDemoService.java + | | | ├── CallBackDemoService.java + | | | ├── DefaultDemoService.java + | | | ├── GenericDemoService.java + | | | ├── RestDemoService.java + | | | └── TimeoutDemoService.java + | | └── UpdateBeanPostProcessors.java + | └── resources/ + | ├── application.properties + | └── log4j.properties + └── test/ + └── java/ +``` + + + +项目目录要大概记住一下,后面代码层次大概心里有个数。 + +### 负载均衡 + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/loadbalance/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/loadbalance/) + + + +1. 消费端就这样配置 + +```java + @Reference(version = "default", loadbalance = "consistenthash") + private DemoService demoService; +``` + +2. 服务端就这样配置 + +```java +@Service(version = "default",loadbalance = "consistenthash") +public class DefaultDemoService implements DemoService { +... +} +``` + +3. 如果在消费端和服务端都配置了负载均衡策略,以消费端为准。 + - 上面两个配的是一致性hash算法。 + - 根据消费者传的参数来进行hash,多次调用,如果参数一样,那么都会负载均衡到服务提供者的同一台机器上。 + + + + + +> 这其中比较难理解的就是**最少活跃调用数**是如何进行统计的?讲道理,最少活跃数应该是在**服务提供者端**进行统计的,服务提供者统计**有多少个请求正在执行中**。但在Dubbo中,就是**不讲道理**,它是在消费端进行统计的,为什么能在消费端进行统计? + +逻辑是这样的: + +1. 每个消费者都会从注册中心(常用的是Zookeeper)缓存所调用服务的所有提供者信息到本地,比如记为p1、p2、p3三个服务提供者,每个提供者内都有一个属性记为active,默认位0 +2. 消费者在调用服务时,如果负载均衡策略是**leastactive** +3. 消费者端会判断缓存的所有服务提供者的active,选择最小的,如果都相同,则随机 +4. 选出某一个服务提供者后,假设是p2,Dubbo就会对p2.active+1 +5. 然后真正发出请求调用该服务 +6. 消费端收到响应结果后,对p2.active-1 +7. 这样就完成了对某个服务提供者当前活跃调用数进行了统计,并且并不影响服务调用的性能 +8. 如果由服务提供者来统计调用数反而不好统计,因为服务提供者有多个,你无法确定是哪个服务提供者统计调用数,除非你放到zookeeper这种分布式共享的数据中心,但是这样的话,每个消费者都要请求zookeeper找到需要调用的那一台服务提供者机器然后加1,在调用结束后,还要在zookeeper上进行减1操作,zookeeper明显扛不住。 +9. 由服务消费者来统计调用数的话,虽然每个消费者都有自己的一套调用数数据,调用数数据可能不一样,但是经过长时间的调用后,每个消费者自己本地存的调用数据还是能够有差不多的趋势(这里的趋势不是指数据相等)。比如说p2响应很慢,堆积了很多请求,那么每个消费者在请求多次后,短时间内都不会再请求p2 + + + +### 服务超时 + +在服务提供者和服务消费者上都可以配置服务超时时间,这两者是不一样的。 + +消费者调用一个服务,分为三步: + +1. 消费者发送请求(网络传输) +2. 服务端执行服务 +3. 服务端返回响应(网络传输) + + + +> 如果在服务端和消费端只在**其中一方**配置了timeout,那么没有歧义,表示消费端调用服务的超时时间,**消费端如果超过时间还没有收到响应结果,则消费端会抛超时异常**。但服务端不会抛异常,服务端在执行服务后,会检查**执行该服务**的时间,如果超过timeout,则会打印一个**超时日志**。服务会正常的执行完。 + + + +1. 如果在服务端和消费端各配了一个timeout,那就比较复杂了,假设 + 1. 服务执行为5s + 2. 消费端timeout=3s + 3. 服务端timeout=6s + + ```java + //服务端 + @Service(version = "timeout", timeout = 6000) + public class TimeoutDemoService implements DemoService { + ... + } + + //消费端 + @Reference(version = "timeout", loadbalance = "roundrobin",timeout = 3000) + private DemoService demoService; + ``` + +2. 那么消费端调用服务时,消费端会收到超时异常(因为消费端超时了),服务端一切正常(服务端没有超时)。 +3. 无论何种情况,服务端的timeout配置的作用是:如果服务执行时间超过这个timeout,仅仅只是打印一个超时日志。 + + + + + +### 集群容错 + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/fault-tolerent-strategy/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/fault-tolerent-strategy/) + + + +> 集群容错表示:服务消费者在调用某个服务时,这个服务有多个服务提供者,在经过负载均衡后选出其中一个服务提供者之后进行调用,但调用报错后,Dubbo所采取的后续处理策略。后续处理策略就是会重试调用其它机器上的服务提供者,加上第一次调用默认是重试2次,总共调用3次。 + +也是既可以在消费端配置,也可以在服务端配置 + +```java +@Reference(timeout = 1000, cluster = "failover") +private DemoService demoService; +``` + + + +### 服务降级 + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/service-downgrade/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/service-downgrade/) + + + +1. 服务降级表示:服务消费者在调用某个服务提供者时,如果该服务提供者报错了,所采取的备选措施。 + +2. 集群容错和服务降级的区别在于: + 1. 集群容错是整个集群范围内的容错 + 2. 服务降级是单个服务提供者的自身容错 + +3. 下面的mock就是一种容错,意思是服务调用失败后,直接返回123 + +```java +@Reference(version = "timeout", timeout = 1000, mock = "fail: return 123") +private DemoService demoService; +``` + +4. mock如何返回对象这种复杂数据可以看官网。 + +### 本地存根 + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/local-stub/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/local-stub/) + + + +本地存根,名字很抽象,但实际上不难理解,本地存根就是一段逻辑,这段逻辑是在服务消费端执行的,这段逻辑一般都是由服务提供者提供,服务提供者可以利用这种机制在服务消费者远程调用服务提供者之前或之后再做一些其他事情,比如结果缓存,请求参数验证等等。 + +> consumer项目 + +```java +package com.youthlql.consumer; + +@EnableAutoConfiguration +public class StubDubboConsumerDemo { + + + // @Reference(version = "timeout", timeout = 1000, stub = "com.youthlql.DemoServiceStub") + @Reference(version = "timeout", timeout = 1000, stub = "true") + private DemoService demoService; + + public static void main(String[] args) throws IOException { + ConfigurableApplicationContext context = SpringApplication.run(StubDubboConsumerDemo.class); + + DemoService demoService = context.getBean(DemoService.class); + + System.out.println((demoService.sayHello("周瑜"))); + + } +} +``` + +1. stub=true的是个默认配置 +2. 默认用接口的全限定类名+Stub去调用,也就是`com.youthlql.DemoServiceStub` +3. 比如这个例子,当消费者去调用`demoService.sayHello("周瑜")`时,会首先调用`interface项目`下的`DemoServiceStub`的`sayHello`方法。如果调用失败就返回`"容错数据"` +4. 如果找不到`com.youthlql.DemoServiceStub`就抛异常 +5. 注意`DemoServiceStub`这个类不一定需要写在interface项目里的,写在哪里都行,只要能通过pom.xml的maven依赖找到`com.youthlql.DemoServiceStub`你这个路径就行 +6. 也可以stub直接指定全类名,这样就可以每一个消费者都提供一个本地存根 + +```java +@Reference(version = "timeout", timeout = 1000, stub = "com.youthlql.DemoServiceStub") +private DemoService demoService; +``` + + + +> interface项目 + +```java +package com.youthlql; + +public class DemoServiceStub implements DemoService { + + private final DemoService demoService; + + // 构造函数传入真正的远程代理对象 + public DemoServiceStub(DemoService demoService){ + this.demoService = demoService; + } + + @Override + public String sayHello(String name) { + /** + *

+ * 1.本地存根和服务降级有一些类似,不过本地存根比服务降级功能要强一点,比如你可以在这个 + * 地方做一些事情 + * 2.此代码在客户端(消费端)执行, 你可以在客户端做ThreadLocal本地缓存,把服务端返回的结果缓存到消费端。或预先验证参数是否合法,等等 + *

+ * @since 2021/8/7 - 17:41 + */ + try { + return demoService.sayHello(name); // safe null + } catch (Exception e) { + // 你可以容错,可以做任何AOP拦截事项 + return "容错数据"; + } + } +} +``` + + + +### 本地伪装 + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/local-mock/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/local-mock/) + + + +本地伪装就是Mock,Dubbo中Mock的功能相对于本地存根更简单一点,Mock其实就是Dubbo中的服务降级,不同的名词罢了 + + + +### 参数回调 + + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/callback-parameter/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/callback-parameter/) + + + +> - 参数回调方式与调用本地 callback 或 listener 相同,只需要在 Spring 的配置文件中声明哪个参数是 callback 类型即可。 +> - 参数回调的用途:Dubbo 将基于长连接生成反向代理,**这样就可以从服务器端调用客户端逻辑。** + +1. 首先,如果当前服务支持参数回调,意思就是:对于某个服务接口中的某个方法,如果想支持消费者在调用这个方法时能设置回调逻辑,那么该方法就需要提供一个入参用来表示回调逻辑。 + +> interface项目 + +```java +package com.youthlql; + +import java.util.concurrent.CompletableFuture; + +public interface DemoService { + // 同步调用方法 + String sayHello(String name); + + // 异步调用方法 + default CompletableFuture sayHelloAsync(String name) { + return null; + }; + + // 添加回调 + default String sayHello(String name, String key, DemoServiceListener listener) { + return null; + }; +} +``` + +> consumer项目 + +```java +package com.youthlql.consumer; + +@EnableAutoConfiguration +public class CallbackDubboConsumerDemo { + + + @Reference(version = "callback") + private DemoService demoService; + + public static void main(String[] args) throws IOException { + ConfigurableApplicationContext context = SpringApplication.run(CallbackDubboConsumerDemo.class); + + DemoService demoService = context.getBean(DemoService.class); + + // 用来进行callback + System.out.println(demoService.sayHello("周瑜", "d1", new DemoServiceListenerImpl())); + System.out.println(demoService.sayHello("周瑜", "d2", new DemoServiceListenerImpl())); + System.out.println(demoService.sayHello("周瑜", "d3", new DemoServiceListenerImpl())); + } + +} +``` + + + +```java +package com.youthlql.consumer; + +public class DemoServiceListenerImpl implements DemoServiceListener { + + @Override + public void changed(String msg) { + System.out.println("被回调了:"+msg); + } +} + +``` + + + +```java +package com.youthlql; + +public interface DemoServiceListener { + void changed(String msg); +} +``` + +> provider项目 + +```java +package com.youthlql.provider.service; + + +/** + *

+ * 1.通过下面sayhello的方法注释,我们可以知道DemoService的sayHello方法的index=2的参数是回调对象, + * 这个回调对象DemoServiceListener是Dubbo给我们生成的代理对象。 + * 2.那么Dubbo怎么知道,sayhello的index=2的参数是回调对象呢?为什么不可以是普通参数呢? + * dubbo又是怎样区分DemoService里的两个sayhello方法呢? + * 3.全都是通过@Service注解里的methods属性来标识的。通过methods属性指定name为sayHello的方法, + * 它的index=2的参数是回调对象。并且同时只支持callbacks = 3,3个回调,数目超过了会报错 + * 3.version = "callback" 这个字符串是可以随便写的,version = "call"也行 + *

+ * @author https://github.com/youthlql + * @since 2021/8/7 - 18:32 + */ +@Service(version = "callback", methods = {@Method(name = "sayHello", arguments = {@Argument(index = 2, callback = true)})}, callbacks = 3) +public class CallBackDemoService implements DemoService { + + private final Map listeners = new ConcurrentHashMap(); + + public CallBackDemoService() { + Thread t = new Thread(new Runnable() { + @Override + public void run() { + while (true) { + for (Map.Entry entry : listeners.entrySet()) { + entry.getValue().changed(getChanged(entry.getKey())); + } + try { + Thread.sleep(5000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + }); + + t.start(); + + } + + public void addListener(String key, DemoServiceListener listener) { + listeners.put(key, listener); + listener.changed(getChanged(key)); // 发送变更通知 + } + + private String getChanged(String key) { + return "Changed: " + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()); + } + + @Override + public String sayHello(String name) { + return null; + } + + /** + *

+ * @param name + * @param key + * 1.上面两参数肯定都是消费端那边传过来的,第三个参数DemoServiceListener + * 不可能是消费者那边传过来的一个空对象吧。它实际上是dubbo生成的代理对象。 + *

+ * @since 2021/8/7 - 18:17 + */ + @Override + public String sayHello(String name, String key, DemoServiceListener callback) { + System.out.println("执行了回调服务" + name); + + callback.changed("xxxx"); + + listeners.put(key, callback); + URL url = RpcContext.getContext().getUrl(); + return String.format("%s:%s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常访问 + } + +} + +``` + + + +3. 因为Dubbo协议是基于长连接的,所以消费端在两次调用同一个方法时想指定不同的回调逻辑,那么就需要在调用时在指定一定key进行区分,这里就是d1,d2,d3。 + +```java +System.out.println(demoService.sayHello("周瑜", "d1", new DemoServiceListenerImpl1())); +System.out.println(demoService.sayHello("周瑜", "d2", new DemoServiceListenerImpl2())); +System.out.println(demoService.sayHello("周瑜", "d3", new DemoServiceListenerImpl3())); +``` + + + + + +### 异步调用 + + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/async-call/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/async-call/) + + + +理解起来比较容易,主要要理解[CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html)。只是说dubbo也可以支持java的CompletableFuture + +其他异步调用方式:[https://mp.weixin.qq.com/s/U3eyBUy6HBVy-xRw3LGbRQ](https://mp.weixin.qq.com/s/U3eyBUy6HBVy-xRw3LGbRQ) + + + +```java +package com.youthlql; + +import java.util.concurrent.CompletableFuture; + +public interface DemoService { + // 同步调用方法 + String sayHello(String name); + + // 异步调用方法 + default CompletableFuture sayHelloAsync(String name) { + return null; + }; + + // 添加回调 + default String sayHello(String name, String key, DemoServiceListener listener) { + return null; + }; +} +``` + + + +```java +package com.youthlql.provider.service; + + +@Service(version = "async") +public class AsyncDemoService implements DemoService { + + @Override + public String sayHello(String name) { + System.out.println("执行了同步服务" + name); + URL url = RpcContext.getContext().getUrl(); + return String.format("%s:%s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常访问 + } + + @Override + public CompletableFuture sayHelloAsync(String name) { + System.out.println("执行了异步服务" + name); + + return CompletableFuture.supplyAsync(() -> { + return sayHello(name); + }); + } +} +``` + + + +```java +package com.youthlql.consumer; + +@EnableAutoConfiguration +public class AsyncDubboConsumerDemo { + + @Reference(version = "async") + private DemoService demoService; + + public static void main(String[] args) throws IOException { + ConfigurableApplicationContext context = SpringApplication.run(AsyncDubboConsumerDemo.class); + + DemoService demoService = context.getBean(DemoService.class); + + // 调用直接返回CompletableFuture + CompletableFuture future = demoService.sayHelloAsync("异步调用"); // 5 + + future.whenComplete((v, t) -> { + if (t != null) { + t.printStackTrace(); + } else { + System.out.println("Response: " + v); + } + }); + + System.out.println("结束了"); + + } + +} +``` + +### 泛化调用 + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/generic-reference/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/generic-reference/) + + + +泛化调用可以用来做服务测试。 + + + +1. 在Dubbo中,如果某个服务想要支持泛化调用,就可以将该服务的generic属性设置为true,那对于服务消费者来说,**就可以不用依赖该服务的接口**(pom.xml里都可以不用加这个依赖),直接利用GenericService接口来进行服务调用。 +2. GenericService是dubbo提供的 + +```java +package com.youthlql.consumer; + + +@EnableAutoConfiguration +public class GenericDubboConsumerDemo { + + + @Reference(id = "demoService", version = "default", interfaceName = "com.youthlql.DemoService", generic = true) + private GenericService genericService; + + public static void main(String[] args) throws IOException { + ConfigurableApplicationContext context = SpringApplication.run(GenericDubboConsumerDemo.class); + + GenericService genericService = (GenericService) context.getBean("demoService"); + + Object result = genericService.$invoke("sayHello", new String[]{"java.lang.String"}, new Object[]{"周瑜"}); + System.out.println(result); + + + } + +} +``` + +### 泛化服务 + + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/generic-service/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/generic-service/) + + + +**作用:实现一个通用的远程服务 Mock 框架,可通过实现 GenericService 接口处理所有服务请求** + +实现了GenericService接口的就是泛化服务 + +```java +package com.youthlql.provider.service; + +import com.youthlql.DemoService; +import org.apache.dubbo.config.annotation.Service; +import org.apache.dubbo.rpc.service.GenericException; +import org.apache.dubbo.rpc.service.GenericService; + +@Service(interfaceName = "com.youthlql.DemoService", version = "generic") +public class GenericDemoService implements GenericService { + + /** + *

+ * @param s 方法名字 + * @param strings 参数类型数组 + * @param objects 参数值数组 + *

+ * @since 2021/8/7 - 18:57 + */ + @Override + public Object $invoke(String s, String[] strings, Object[] objects) throws GenericException { + System.out.println("执行了generic服务"); + + return "执行的方法是" + s; + } +} + +``` + +意思就是实际暴露出去的服务依然是`com.youthlql.DemoService`,并且版本是generic。只是消费者调用的时候,最终执行的逻辑是这个`$invoke`方法。就是服务消费者你该怎么用还是怎么用,只是服务提供者后面真正执行逻辑不再是实现demoservice接口,实现sayhello方法了。而是GenericService的$invoke方法 + +### Dubbo中的REST + + + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/rest/](http://dubbo.apache.org/zh/docs/v2.7/user/rest/) + + + +注意Dubbo的REST也是Dubbo所支持的一种**协议**。 + + + +当我们用Dubbo提供了一个服务后,如果消费者没有使用Dubbo也想调用服务,那么这个时候我们就可以让我们的服务支持REST协议,这样消费者就可以通过REST形式调用我们的服务了。 + +注意:如果某个服务只有REST协议可用,那么该服务必须用@Path注解定义访问路径 + + + +> application.properties 即支持dubbo协议,又支持rest协议 + +```properties + # Spring boot application +spring.application.name=dubbo-provider-demo +server.port=8081 + +# Base packages to scan Dubbo Component: @org.apache.dubbo.config.annotation.Service +dubbo.scan.base-packages=com.youthlql.provider.service +dubbo.application.name=${spring.application.name} + + +## Dubbo Registry +dubbo.registry.address=zookeeper://127.0.0.1:2181 + +# Dubbo Protocol +#dubbo.protocol.name=dubbo +#dubbo.protocol.port=20880 + + +#dubbo.protocol.name=rest +#dubbo.protocol.port=8083 + +dubbo.protocols.p1.id=dubbo1 +dubbo.protocols.p1.name=dubbo +dubbo.protocols.p1.port=20881 +dubbo.protocols.p1.host=0.0.0.0 + +dubbo.protocols.p2.id=rest +dubbo.protocols.p2.name=rest +dubbo.protocols.p2.port=8083 +dubbo.protocols.p2.host=0.0.0.0 + +#dubbo.protocols.p3.id=dubbo3 +#dubbo.protocols.p3.name=dubbo +#dubbo.protocols.p3.port=20883 +#dubbo.protocols.p3.host=0.0.0.0 +``` + + + + + +```java +package com.youthlql.provider.service; + + +@Service(version = "rest", protocol = "p2") +@Path("demo") +public class RestDemoService implements DemoService { + + @GET + @Path("say") + @Produces({ContentType.APPLICATION_JSON_UTF_8, ContentType.TEXT_XML_UTF_8}) + @Override + public String sayHello(@QueryParam("name") String name) { + System.out.println("执行了rest服务" + name); + + URL url = RpcContext.getContext().getUrl(); + return String.format("%s: %s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常访问 + } + +} +``` + + + +```java +package com.youthlql.provider.service; + +import com.youthlql.DemoService; +import org.apache.dubbo.common.URL; +import org.apache.dubbo.config.annotation.Service; +import org.apache.dubbo.rpc.RpcContext; + +import java.util.concurrent.CompletableFuture; + +@Service(version = "async", protocol = "p1") +public class AsyncDemoService implements DemoService { + + @Override + public String sayHello(String name) { + System.out.println("执行了同步服务" + name); + URL url = RpcContext.getContext().getUrl(); + return String.format("%s:%s, Hello, %s", url.getProtocol(), url.getPort(), name); // 正常访问 + } + + @Override + public CompletableFuture sayHelloAsync(String name) { + System.out.println("执行了异步服务" + name); + + return CompletableFuture.supplyAsync(() -> { + return sayHello(name); + }); + } +} +``` + +### 管理台 + +github地址:[https://github.com/apache/dubbo-admin](https://github.com/apache/dubbo-admin) + +用户名和密码默认都是root + + + +1. Dubbo分为注册中心和配置中心,如果spring文件里没有明确写配置中心,配置中心默认就用注册中心。 + +```properties +dubbo.registry.address=zookeeper://127.0.0.1:2181 +dubbo.config-center.address= +``` + +2. 管理台的**配置管理**作用就是可以实时更改dubbo相关的配置,在这里面写了和在appliaction.properties里面写是一样的效果,这个还不用重启服务。如果appliaction.properties里和管理台写了相同的配置,以管理台的为主。 + + + +3. **动态配置**这里,也可以很方便的替代服务提供者@service注解上标注的那些配置。管理台是实时生效的,如果改代码里的@service还需要重启服务。 + + + +很多配置都可以在管理台上配。管理台上写的配置会持久化在**你配置的配置中心**里。只有注册中心里的服务提供者信息不持久化,如果注册中心是zookeeper,那么服务提供者在zk上就是临时节点。 + +### 动态配置 + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/config-rule/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/config-rule/) + + + +注意动态配置修改的是服务**参数**,并不能修改服务的协议、IP、PORT、VERSION、GROUP,因为这5个信息是服务的标识信息,是服务的身份证号,是不能修改的。 + + + +### 服务路由 + +官网地址:[http://dubbo.apache.org/zh/docs/v2.7/user/examples/routing-rule/](http://dubbo.apache.org/zh/docs/v2.7/user/examples/routing-rule/) + +> 注意所有的东西都要跟官网结合着看 + +举个官网的例子,简单说下 + +```yaml +# app1的消费者只能消费所有端口为20880的服务实例 +# app2的消费者只能消费所有端口为20881的服务实例 +--- +scope: application +force: true +runtime: true +enabled: true +key: governance-conditionrouter-consumer +conditions: + - application=app1 => address=*:20880 # 如果你的应用是app1,那么你只能访问20880这个端口的服务 + - application=app2 => address=*:20881 +... +``` + + + +#### 标签路由 + +```yaml +# governance-tagrouter-provider应用增加了两个标签分组tag1和tag2 +# tag1包含一个实例 127.0.0.1:20880 +# tag2包含一个实例 127.0.0.1:20881 +--- + force: false + runtime: true + enabled: true + key: governance-tagrouter-provider + tags: + - name: tag1 + addresses: ["127.0.0.1:20880"] + - name: tag2 + addresses: ["127.0.0.1:20881"] + ... +``` + + + +```java +package com.youthlql.controller; + +import org.apache.dubbo.rpc.Constants; +import org.apache.dubbo.rpc.RpcContext; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.ArrayList; +import java.util.List; + +public class ConsumerInterceptor implements HandlerInterceptor { + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + + // 测试账号 + List tester = new ArrayList<>(); + tester.add("18888888888"); + + //标签路由 + String account = request.getParameter("account"); + if (tester.contains(account)) { + RpcContext.getContext().setAttachment("dubbo.tag", "tag1"); + } else { + RpcContext.getContext().setAttachment("dubbo.tag", "tag2"); + } + return true; + } +} + +``` + +1. 比如说现在服务上线,消费者去请求提供者,通过拦截器在发送请求之前,判断账号是不是内测用户账号,如果是就打一个tag1标签,否则就打tag2标签 +2. 那么如果打了tag1标签,内测账号就会访问"127.0.0.1:20880"这个服务器。 +3. 可以用来做灰度发布,比如新版本的服务只给内测用户群体用。 + + + +#### 什么是蓝绿发布、灰度发布 + +[https://zhuanlan.zhihu.com/p/42671353](https://zhuanlan.zhihu.com/p/42671353) + + + +### Zookeeper可视化客户端工具 + +Zookeeper可视化客户端:ZooInspector + +## Dubbo与其他微服务组件整合 + +sentinel: https://github.com/alibaba/Sentinel/tree/master/sentinel-demo/sentinel-demo-dubbo + +nacos: https://nacos.io/zh-cn/docs/use-nacos-with-dubbo.html + +seata: http://seata.io/zh-cn/docs/user/microservice.html + + + diff --git a/docs/rpc/dubbo/Dubbo源码系列V1-03.Dubbo第三节-可扩展机制SPI源码解析.md b/docs/rpc/dubbo/Dubbo源码系列V1-03.Dubbo第三节-可扩展机制SPI源码解析.md new file mode 100644 index 0000000..880276d --- /dev/null +++ b/docs/rpc/dubbo/Dubbo源码系列V1-03.Dubbo第三节-可扩展机制SPI源码解析.md @@ -0,0 +1,1179 @@ +--- +title: Dubbo源码系列V1-03.Dubbo第三节-可扩展机制SPI源码解析 +tags: + - Dubbo + - rpc +categories: + - rpc + - Dubbo源码系列v1 +keywords: Dubbo,rpc +description: Dubbo里面SPI是基础,大量用到了SPI +cover: 'https://cdn.jsdelivr.net/gh/youthlql/youthlql/img/dubbo.png' +abbrlink: dbcfef47 +date: 2021-09-12 15:21:58 +--- + + + + + + + +## 第三节: Dubbo的可扩展机制SPI源码解析 + + + +### SPI的概念 + +https://www.cnblogs.com/happyframework/archive/2013/09/17/3325560.html + +https://zhuanlan.zhihu.com/p/28909673 + +spi的概念看上面两就行了,案例还是看我下面的举例,讲的比较通俗。 + +### Java的SPI机制 + +#### 项目目录 + +```java +spi-demo +├── api-db-impl-mysql/ +| ├── api-db-impl-mysql.iml +| ├── pom.xml +| ├── src/ +| | ├── main/ +| | | ├── java/ +| | | | └── com/ +| | | | └── youthlql/ +| | | | └── mysql/ +| | | | └── MySQLSaveService.java +| | | └── resources/ +| | | └── META-INF/ +| | | └── services/ +| | | └── com.youthlql.data.DataSaveService +| | └── test/ +| | └── java/ +| └── target/ +| ├── classes/ +| | ├── com/ +| | | └── youthlql/ +| | | └── mysql/ +| | | └── MySQLSaveService.class +| | └── META-INF/ +| | └── services/ +| | └── com.youthlql.data.DataSaveService +| └── generated-sources/ +| └── annotations/ +├── api-db-impl-redis/ +| ├── api-db-impl-redis.iml +| ├── pom.xml +| ├── src/ +| | ├── main/ +| | | ├── java/ +| | | | └── com/ +| | | | └── youthlql/ +| | | | └── redis/ +| | | | └── RedisSaveService.java +| | | └── resources/ +| | | └── META-INF/ +| | | └── services/ +| | | └── com.youthlql.data.DataSaveService +| | └── test/ +| | └── java/ +| └── target/ +| ├── classes/ +| | ├── com/ +| | | └── youthlql/ +| | | └── redis/ +| | | └── RedisSaveService.class +| | └── META-INF/ +| | └── services/ +| | └── com.youthlql.data.DataSaveService +| └── generated-sources/ +| └── annotations/ +├── api-db-interface/ +| ├── api-db-interface.iml +| ├── pom.xml +| ├── src/ +| | ├── main/ +| | | ├── java/ +| | | | └── com/ +| | | | └── youthlql/ +| | | | └── data/ +| | | | └── DataSaveService.java +| | | └── resources/ +| | └── test/ +| | └── java/ +| └── target/ +| ├── classes/ +| | └── com/ +| | └── youthlql/ +| | └── data/ +| | └── DataSaveService.class +| └── generated-sources/ +| └── annotations/ +├── app/ +| ├── app.iml +| ├── pom.xml +| ├── src/ +| | ├── main/ +| | | ├── java/ +| | | | └── com/ +| | | | └── youthlql/ +| | | | └── redis/ +| | | | └── MainTest.java +| | | └── resources/ +| | └── test/ +| | └── java/ +| └── target/ +| ├── classes/ +| | └── com/ +| | └── youthlql/ +| | └── redis/ +| | └── MainTest.class +| └── generated-sources/ +| └── annotations/ +├── pom.xml +└── spi-demo.iml + +``` + + + +#### MainTest + +```java +import com.youthlql.data.DataSaveService; + +import java.util.ServiceLoader; + + +/** + * 1、 ServiceLoader:load()指定一个接口, + * 他就会加载当前系统里面所有的这个接口的【指定实现】 + * 2、SPI(Service Provider Interface) + * 接口工程---提供接口 + * ---- 实现工程1 : 实现接口 【META-INF/services 创建文件 接口名作为文件名 实现类全路径作为文件内容】 + * ---- 实现工程2 : 实现接口 + * + * + * 客户端----引用 工程1、或者 工程2 + * + * + * + */ +public class MainTest { + + public static void main(String[] args) { + + //1、加载 可用的接口实现 + ServiceLoader load = ServiceLoader.load(DataSaveService.class); + + //拿到实现进行调用 + for (DataSaveService service : load) { + service.saveData("你好...."); + } + + } +} +``` + +输出: + +```java +MySQL保存了数据.......你好.... +Redis保存了数据.......你好.... +``` + +Java的SPI机制会默认加载**类路径**下`META-INF/services`的东西 + +#### DataSaveService + +``` +public interface DataSaveService { + + void saveData(String data); +} +``` + +#### MySQLSaveService + +```java +public class MySQLSaveService implements DataSaveService { + @Override + public void saveData(String data) { + System.out.println("MySQL保存了数据......." + data); + } +} +``` + +#### RedisSaveService + +```java +public class RedisSaveService implements DataSaveService { + @Override + public void saveData(String data) { + System.out.println("Redis保存了数据......."+data); + } +} +``` + + + +#### SPI文件示例 + +api-db-impl-redis\src\main\resources\META-INF\services\com.youthlql.data.DataSaveService + +```txt +com.youthlql.redis.RedisSaveService +``` + + + +api-db-impl-mysql\src\main\resources\META-INF\services\com.youthlql.data.DataSaveService + +```txt +com.youthlql.mysql.MySQLSaveService +``` + +你没看错就是这么简单 + + + +#### Java的SPI机制的作用 + +我只需要规定接口就可以开放给任何人实现 + +> META-INF\services下的文件,本文统称为**SPI文件** + +### Dubbo为什么要实现自己的SPI机制? + +> SPI机制,dubbo源码中大量用到,读者务必理解清楚。 + +SPI文件里写了什么,java的`ServiceLoader`都会给你一次性加载完 + +```java +com.youthlql.RedCar +com.youthlql.BlackCar +com.youthlql,WhiteCar +.... +``` + +`SpiTest.java` + +```java + public static void main(String[] args) { + ServiceLoader cars = ServiceLoader.load(Car.class); + for (Car car : cars) { + System.out.println(car.getCarName(null)); + } + } +``` + +假设现在有一个需求,我们某一时刻只需要RedCar,某一时刻又需要BlackCar,就是说我们不想让它一次性加载完。 + +spi文件我们可以这样写: + +```java +red=com.youthlql.RedCar +black=com.youthlql.BlackCar +``` + +`SpiTest.java` + +```java + public static void main(String[] args) { + ServiceLoader cars = ServiceLoader.load(Car.class,"red"); + for (Car car : cars) { + System.out.println(car.getCarName(null)); + } + } +``` + +因为这样的需求,就引出了dubbo想要实现的SPI的功能。 + + + +前面介绍过Dubbo支持http协议,支持dubbo协议等等,实现这样的SPI之后。写代码时只需要在application.properties里直接指定你想要用的协议,需要http协议时就只加载对应的protocol的http实现类 + +```properties +dubbo.protocols.p1.id=dubbo1 +dubbo.protocols.p1.name=dubbo +dubbo.protocols.p1.port=20881 +dubbo.protocols.p1.host=0.0.0.0 + +dubbo.protocols.p2.id=http +dubbo.protocols.p2.name=http +dubbo.protocols.p2.port=8083 +dubbo.protocols.p2.host=0.0.0.0 +``` + + + +### Demo + +```java +ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class); +Protocol http = extensionLoader.getExtension("dubbo"); +System.out.println(http); +``` + + + +上面这个Demo就是Dubbo常见的写法,表示获取"dubbo"对应的Protocol扩展点。Protocol是一个接口。 + +#### getExtensionLoader源码 + +```java + private static final ConcurrentMap, ExtensionLoader> EXTENSION_LOADERS = new ConcurrentHashMap<>(); + //中间代码略... + public static ExtensionLoader getExtensionLoader(Class type) { + if (type == null) { + throw new IllegalArgumentException("Extension type == null"); + } + if (!type.isInterface()) { + throw new IllegalArgumentException("Extension type (" + type + ") is not an interface!"); + } + if (!withExtensionAnnotation(type)) { + throw new IllegalArgumentException("Extension type (" + type + + ") is not an extension, because it is NOT annotated with @" + SPI.class.getSimpleName() + "!"); + } + + ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + if (loader == null) { + EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type)); + loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + } + return loader; + } +``` + + + +在ExtensionLoader类的内部有一个static的ConcurrentHashMap,用来缓存**某个接口类型所对应的ExtensionLoader实例** + + + + + +### ExtensionLoader源码 + +- Exte nsionLoader表示某个接口的扩展点加载器,可以用来加载某个扩展点实例。 + +- 在ExtensionLoader中除开有上文的static的Map外,还有两个非常重要的属性: + +1. **Class type:**表示当前ExtensionLoader实例是哪个接口的扩展点加载器 + +2. **ExtensionFactory objectFactory:**扩展点工厂(对象工厂),可以获得某个对象 + + + +- **ExtensionLoader**和**ExtensionFactory**的区别在于: + +1. ExtensionLoader最终所得到的对象是Dubbo SPI机制产生的 +2. ExtensionFactory最终所得到的对象可能是Dubbo SPI机制所产生的,也可能是从Spring容器中所获得的对象 + + + + + +- 在**ExtensionLoader**中有三个常用的方法: + +1. **getExtension("dubbo"):**表示获取名字为dubbo的扩展点实例 +2. **getAdaptiveExtension():**表示获取一个自适应的扩展点实例 +3. **getActivateExtension(URL url, String\[\] values, String group):**表示一个可以被url激活的扩展点实例,后文详细解释 + + + +其中,什么是**自适应扩展点实例**?它其实就是当前这个接口的一个**代理对象。** + + ```java + ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class); + Protocol protocol = extensionLoader.getExtension("dubbo"); + ``` + + + + + +当我们调用上述代码,我们会将得到一个DubboProtocol的实例对象,但在getExtension()方法中,Dubbo会对DubboProtocol对象进行**依赖注入(也就是自动给属性赋值,属性的类型为一个接口,记为A接口),**这个时候,对于Dubbo来说它并不知道该给这个属性赋什么值,换句话说,Dubbo并不知道在进行依赖注入时该找一个什么的的扩展点对象给这个属性,这时就会预先赋值一个A接口的自适应扩展点实例,也就是A接口的一个代理对象。 + +后续,在A接口的代理对象被真正用到时,才会结合URL信息找到真正的A接口对应的扩展点实例进行调用。 + + + +#### getExtension() + +在调用getExtension去获取一个扩展点实例后,会对实例进行缓存,下次再获取同样名字的扩展点实例时就会从缓存中拿了。 + + ```java + public T getExtension(String name) { + if (StringUtils.isEmpty(name)) { + throw new IllegalArgumentException("Extension name == null"); + } + // 获取默认扩展类 + if ("true".equals(name)) { + return getDefaultExtension(); + } + + final Holder holder = getOrCreateHolder(name); + Object instance = holder.get(); + + // 如果有两个线程同时来获取同一个name的扩展点对象,那只会有一个线程会进行创建 + if (instance == null) { + synchronized (holder) { // 一个name对应一把锁 + instance = holder.get(); + if (instance == null) { + // 创建扩展点实例对象 + instance = createExtension(name); // 创建扩展点对象 + holder.set(instance); + } + } + } + return (T) instance; + } + + private Holder getOrCreateHolder(String name) { + // Map + Holder holder = cachedInstances.get(name); + if (holder == null) { + cachedInstances.putIfAbsent(name, new Holder<>()); + holder = cachedInstances.get(name); + } + return holder; + } + ``` + + + +什么是默认的扩展类,就是像下面这样,在接口上指定了spi注解 + +```java +@SPI("red") +public interface Car { + String getCarName); +} +``` + +#### createExtension() + +```java + private T createExtension(String name) { + + //根据name获取扩展类 {name: Class} key-Value 接口的所有实现类 + 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); + + // AOP:对依赖注入后的实例进行AOP(Wrapper),把当前接口类的所有的Wrapper全部一层一层包裹在实例对象上 + // 每包裹个Wrapper后,也会对Wrapper对象进行依赖注入 + Set> wrapperClasses = cachedWrapperClasses; + if (CollectionUtils.isNotEmpty(wrapperClasses)) { + for (Class wrapperClass : wrapperClasses) { + instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); + } + } + //返回最终的Wrapper对象,debug一下你就会看到返回的不是redCar而是carWrapper + return instance; + } catch (Throwable t) { + throw new IllegalStateException("Extension instance (name: " + name + ", class: " + + type + ") couldn't be instantiated: " + t.getMessage(), t); + } + } + +``` + +> wrapper就相当于在你想要的对象外面再封一层,可以做一些事情,详见下面讲的AOP + + + +#### getExtensionClasses() + +```java + /** + * 加载当前ExtensionLoader对象中指定的接口的所有扩展 + * @return + */ + private Map> getExtensionClasses() { + // cachedClasses是一个Holder对象,持有的就是一个Map> + // 为什么要多此一举,也是为了解决并发,Holder对象用来作为锁 + + Map> classes = cachedClasses.get(); + if (classes == null) { + synchronized (cachedClasses) { + classes = cachedClasses.get(); + if (classes == null) { + classes = loadExtensionClasses(); // 加载、解析文件 Map + cachedClasses.set(classes); + } + } + } + return classes; + } + + /** + * synchronized in getExtensionClasses + * */ + private Map> loadExtensionClasses() { + // cache接口默认的扩展类,也就是上面说的@SPI("red")注解标记的接口指定的名 + cacheDefaultExtensionName(); + + Map> extensionClasses = new HashMap<>(); + loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName()); + loadDirectory(extensionClasses, DUBBO_INTERNAL_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); + loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName()); + loadDirectory(extensionClasses, DUBBO_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); + loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName()); + loadDirectory(extensionClasses, SERVICES_DIRECTORY, type.getName().replace("org.apache", "com.alibaba")); + return extensionClasses; + } + + 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/"; +``` + + + +getExtensionClasses()是用来加载当前接口所有的扩展点实现类的,返回一个Map。之后可以从这个Map中按照指定的name获取对应的扩展点实现类。 + +Dubbo在加载一个接口的扩展点时,思路是这样的: + +1. 根据接口的全限定名去META-INF/dubbo/internal/目录下寻找对应的文件,调用loadResource方法进行加载 +2. 根据接口的全限定名去META-INF/dubbo/目录下寻找对应的文件,调用loadResource方法进行加载 +3. 根据接口的全限定名去META-INF/services/目录下寻找对应的文件,调用loadResource方法进行加载 + +这里其实会设计到老版本兼容的逻辑,不解释了。 + +#### loadDirectory() + + ```java + private void loadDirectory(Map> extensionClasses, String dir, String type) { + String fileName = dir + type; + try { + // 根据文件中的内容得到urls, 每个url表示一个扩展 http=org.apache.dubbo.rpc.protocol.http.HttpProtocol + Enumeration urls; + ClassLoader classLoader = findClassLoader(); + if (classLoader != null) { + urls = classLoader.getResources(fileName); + } else { + urls = ClassLoader.getSystemResources(fileName); + } + if (urls != null) { + while (urls.hasMoreElements()) { + java.net.URL resourceURL = urls.nextElement(); + // 遍历url进行加载,把扩展类添加到extensionClasses中 + loadResource(extensionClasses, classLoader, resourceURL); + } + } + } catch (Throwable t) { + logger.error("Exception occurred when loading extension class (interface: " + + type + ", description file: " + fileName + ").", t); + } + } + ``` + +1. BootstrapClassLoader用于加载JAVA核心类库,也就是环境变量的%JRE_HOME%\lib下的rt.jar、resources.jar、charsets.jar等。在JVM启动时加入-Xbootclasspath参数,可以把对应路径也加载到Bootstrap的路径列表中来 +2. ExtentionClassLoader扩展类加载器,加载环境变量%JRE_HOME%\lib\ext目录下的class文件 +3. AppclassLoader加载**classpath**中的class类。 + +#### loadResource() + +loadResource方法就是完成对文件内容的解析,按行进行解析,会解析出**"="**两边的内容,"="左边的内容就是扩展点的name,右边的内容就是扩展点实现类,并且会利用ExtensionLoader类的类加载器来加载扩展点实现类。然后调用loadClass方法对name和扩展点实例进行详细的解析,并且最终把他们放到Map中去。 + + ```java + private void loadResource(Map> extensionClasses, ClassLoader classLoader, java.net.URL resourceURL) { + try { + try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceURL.openStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + final int ci = line.indexOf('#'); + if (ci >= 0) { + line = line.substring(0, ci); + } + line = line.trim(); + if (line.length() > 0) { + try { + String name = null; + int i = line.indexOf('='); + if (i > 0) { + name = line.substring(0, i).trim(); + line = line.substring(i + 1).trim(); + } + if (line.length() > 0) { + // 加载类,并添加到extensionClasses中 + loadClass(extensionClasses, resourceURL, Class.forName(line, true, classLoader), name); + } + } catch (Throwable t) { + IllegalStateException e = new IllegalStateException("Failed to load extension class (interface: " + type + ", class line: " + line + ") in " + resourceURL + ", cause: " + t.getMessage(), t); + exceptions.put(line, e); + } + } + } + } + } catch (Throwable t) { + logger.error("Exception occurred when loading extension class (interface: " + + type + ", class file: " + resourceURL + ") in " + resourceURL, t); + } + } + ``` + +#### loadClass() + + ```java + private void loadClass(Map> extensionClasses, java.net.URL resourceURL, Class clazz, String name) throws NoSuchMethodException { + if (!type.isAssignableFrom(clazz)) { + throw new IllegalStateException("Error occurred when loading extension class (interface: " + + type + ", class line: " + clazz.getName() + "), class " + + clazz.getName() + " is not subtype of interface."); + } + /* + 1.当前扩展点实现类上是否存在@Adaptive注解,如果存在则把该类认为是当前接口的默认自适应类 + (接口代理类),并把该类存到cachedAdaptiveClass属性上。 + 2.当前扩展点实现是否是一个当前接口的一个Wrapper类,如何判断的?就是看当前类中是否存在一 + 个构造方法,该构造方法只有一个参数,参数类型为接口类型,如果存在这一的构造方法,那么这个 + 类就是该接口的Wrapper类,如果是,则把该类添加到cachedWrapperClasses中去. + cachedWrapperClasses是一个set。 + */ + if (clazz.isAnnotationPresent(Adaptive.class)) { + cacheAdaptiveClass(clazz); + } else if (isWrapperClass(clazz)) { + cacheWrapperClass(clazz); + } else { + // 需要有无参的构造方法,没有会报错 + clazz.getConstructor(); + + /* + 1. 本来应该这样写的 red=com.youthlql.RedCar + 2.如果你前面的red这个name没写,像这样只写了个全类名com.youthlql.RedCar + 3.默认会去com.youthlql.RedCar这个类上找有没有@Extension注解起名了【官方已经标记成废弃了】 + */ + if (StringUtils.isEmpty(name)) { + name = findAnnotationName(clazz); + if (name.length() == 0) { + throw new IllegalStateException("No such extension name for the class " + clazz.getName() + " in the config " + resourceURL); + } + } + + //name可以配多个 + String[] names = NAME_SEPARATOR.split(name); + if (ArrayUtils.isNotEmpty(names)) { + //判断一下当前扩展点实现类上是否存在@Activate注解,如果存在,则把该类添加到cachedActivates中 + cacheActivateClass(clazz, names[0]); + + //遍历多个name,把每个name和对应的实现类存到extensionClasses中去,extensionClasses就是上文所提到的map + for (String n : names) { + // clazz: name + cacheName(clazz, n); + // name: clazz + saveInExtensionClass(extensionClasses, clazz, n); + } + } + } + } + ``` + + + +至此,加载类就走完了。回到createExtension(String name)方法中的逻辑,当前这个接口的所有扩展点实现类都扫描完了之后,就可以根据用户所指定的名字,找到对应的实现类了,然后进行实例化,然后进行IOC(依赖注入)和AOP。 + + + +### Dubbo中的IOC + +#### 什么是dubbo的IOC? + +```java +package cn.imlql.ioc; + +import cn.imlql.spi.Car; + +public class BlackPerson implements Person { + + private Car car; + + public void setCar(Car car) { + this.car = car; + } + + @Override + public Car getCar() { + return car; + } +} +``` + + + +```java +package cn.imlql.ioc; + +import cn.imlql.spi.Car; +import com.alibaba.dubbo.common.extension.SPI; + +@SPI +public interface Person { + + Car getCar(); +} +``` + +SPI文件:resources\META-INF\dubbo\cn.imlql.ioc.Person + +```java +black=cn.imlql.ioc.BlackPerson +``` + +1. BlackPerson类中有个car属性,dubbo会把car属性通过set方法注入到BlackPerson中 +2. 但是具体注入哪一个,dubbo并不知道,person.getCar()这一步dubbo虽然不知道具体注入哪一个实现类对象,但是dubbo生成了一个代理对象给注入鉣了 +3. 真正需要确定是哪个car的实现类时候是调用car的方法的时候,通过URL这个东西来确定注入哪个car的实现类 + +```java +package cn.imlql.ioc; + +import com.alibaba.dubbo.common.URL; +import com.alibaba.dubbo.common.extension.ExtensionLoader; + +/** + *

+ *

+ * + * @author https://github.com/youthlql + * @since 2021/9/4 - 20:08 + */ +public class DubboIOCTest { + public static void main(String[] args) { + ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Person.class); + Person person = extensionLoader.getExtension("black"); // BlackPerson + + URL url = new URL("x", "localhost", 8080); + url = url.addParameter("car", "black"); + + System.out.println(person.getCar().getCarName(url)); // 代理逻辑 + } +} +``` + + + +#### injectExtension() + +上面提到过这个方法 + +```java + private T injectExtension(T instance) { + + if (objectFactory == null) { + return instance; + } + + try { + //根据当前实例的类,找到这个类中的setter方法,进行依赖注入 + for (Method method : instance.getClass().getMethods()) { + if (!isSetter(method)) { + continue; + } + + // 利用set方法注入 + + /** + * Check {@link DisableInject} to see if we need auto injection for this property + */ + if (method.getAnnotation(DisableInject.class) != null) { + continue; + } + + //先分析出setter方法的参数类型pt + Class pt = method.getParameterTypes()[0]; // Person接口 + if (ReflectUtils.isPrimitives(pt)) { + continue; + } + + try { + //再截取出setter方法所对应的属性名property,得到setxxx中的xxx + String property = getSetterProperty(method); // person + + //1.得到一个对象,这里就会从Spring容器或通过DubboSpi机制得到一个对象,比较特殊的是, + //2.如果是通过DubboSpi机制得到的对象,是pt这个类型的一个自适应对象(代理对象)。 + //3.也就是说这里既可以从spring容器里拿到一个对象,也可以从dubbo里拿到一个代理对象 + Object object = objectFactory.getExtension(pt, property); // User.class, user + if (object != null) { + //再反射调用setter方法进行注入 + method.invoke(instance, object); + } + } catch (Exception e) { + logger.error("Failed to inject via method " + method.getName() + + " of interface " + type.getName() + ": " + e.getMessage(), e); + } + + } + } catch (Exception e) { + logger.error(e.getMessage(), e); + } + return instance; + } +``` + +objectFactory这个类是如何拿到Spring容器里的对象? + +```java +//我们可以看到objectFactory是在这里赋值的 + +private ExtensionLoader(Class type) { + this.type = type; + // objectFactory表示当前ExtensionLoader内部的一个对象工厂,可以用来获取对象 AdaptiveExtensionFactory + objectFactory = (type == ExtensionFactory.class ? null : ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getAdaptiveExtension()); +} +``` + +#### getAdaptiveExtension() + +关键就是后面的`getAdaptiveExtension()`方法,点进去调用链是这样的 + +```java +getAdaptiveExtension() ---> createAdaptiveExtension() ---> getAdaptiveExtensionClass() +``` + + + +```java +private Class getAdaptiveExtensionClass() { + // 获取当前接口的所有扩展类 + getExtensionClasses(); + // 缓存了@Adaptive注解标记的类 + if (cachedAdaptiveClass != null) { + return cachedAdaptiveClass; + } + // 如果某个接口没有手动指定一个Adaptive类(@Adaptive注解),那么就自动生成一个Adaptive类 + return cachedAdaptiveClass = createAdaptiveExtensionClass(); +} + +private Class createAdaptiveExtensionClass() { + // cachedDefaultName表示接口默认的扩展类 + String code = new AdaptiveClassCodeGenerator(type, cachedDefaultName).generate(); + + ClassLoader classLoader = findClassLoader(); + org.apache.dubbo.common.compiler.Compiler compiler = ExtensionLoader.getExtensionLoader(org.apache.dubbo.common.compiler.Compiler.class).getAdaptiveExtension(); + return compiler.compile(code, classLoader); +} +``` + +1. getAdaptiveExtension()的作用就是获取标注了@Adaptive注解的factory工厂,ExtensionFactory的实现类中只有AdaptiveExtensionFactory标注了此注解 +2. 也就是说objectFactory就是AdaptiveExtensionFactory类型,并且调用了AdaptiveExtensionFactory的getExtension()方法 + + + +#### AdaptiveExtensionFactory类 + +```java +@Adaptive +public class AdaptiveExtensionFactory implements ExtensionFactory { + + private final List factories; + + public AdaptiveExtensionFactory() { + // 支持哪些ExtensionFactory (Spi, Spring),从这里就把SpringExtensionFactory搞进来了 + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(ExtensionFactory.class); + + List list = new ArrayList(); + + for (String name : loader.getSupportedExtensions()) { // spi, spring + list.add(loader.getExtension(name)); + } + factories = Collections.unmodifiableList(list); + } + + @Override + public T getExtension(Class type, String name) { + // 遍历两个ExtensionFactory,从ExtensionFactory中得到实例,只要从某个ExtensionFactory中获取到对象实例就可以了 + for (ExtensionFactory factory : factories) { + T extension = factory.getExtension(type, name); // 顺序是这样的SpringExtensionFactory,, SpiExtensionFactory + if (extension != null) { + return extension; + } + } + return null; + } + +} +``` + +#### SpringExtensionFactory类 + +```java + // 从Spring容器中获取bean + // 先根据name拿,再根据类型拿 + @Override + @SuppressWarnings("unchecked") + public T getExtension(Class type, String name) { + + //SPI should be get from SpiExtensionFactory + // 如果接口上存在SPI注解,就不从spring中获取对象实例了 + if (type.isInterface() && type.isAnnotationPresent(SPI.class)) { + return null; + } + + // 从ApplicationContext中获取bean, byname + for (ApplicationContext context : CONTEXTS) { + if (context.containsBean(name)) { + Object bean = context.getBean(name); + if (type.isInstance(bean)) { + return (T) bean; + } + } + } +``` + +#### SpiExtensionFactory类 + +```java +public class SpiExtensionFactory implements ExtensionFactory { + //type就是属性的类型Car + @Override + public T getExtension(Class type, String name) { + + + // 接口上存在SPI注解 + if (type.isInterface() && type.isAnnotationPresent(SPI.class)) { + //根据属性类型得到一个扩展点加载器 + ExtensionLoader loader = ExtensionLoader.getExtensionLoader(type); + + if (!loader.getSupportedExtensions().isEmpty()) { + //这里就又回到了上面写过的getAdaptiveExtension方法 + return loader.getAdaptiveExtension(); + } + } + return null; + } + +} +``` + + + +dubbo生成的代理对象代码可以看下面的`自适应扩展点补充`,这里应该就知道为什么要用URL了 + + + +```java +public class DubboIOCTest { + public static void main(String[] args) { + ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Person.class); + Person person = extensionLoader.getExtension("black"); // BlackPerson + + URL url = new URL("x", "localhost", 8080); + url = url.addParameter("car", "black"); + + System.out.println(person.getCar().getCarName(url)); // 代理逻辑 + } +} +``` + + + +需要注意的是代理对象被代理的方法也要加@Adaptive注解,同时加URL参数,就像上面调用了car的getCarName方法,那么下面就长这样。如果不加的话会抛一个UnsupportedOperationException。具体的可以自己debug到生成代理对象那一步,然后把代理对象值复制到txt文件里自己看下 + +```java +@SPI +public interface Car { + + @Adaptive + String getCarName(URL url); + + //参数如果是自己定义的类,只要这个类里也有URL这个属性就可以 + @Adaptive + String hello(MyClass myclass); +} +``` + +### Dubbo中的AOP + +dubbo中也实现了一套非常简单的AOP,就是利用Wrapper,如果一个接口的扩展点中包含了多个Wrapper类,那么在实例化完某个扩展点后,就会利用这些Wrapper类对这个实例进行包裹,比如:现在有一个DubboProtocol的实例,同时对于Protocol这个接口还有很多的Wrapper,比如ProtocolFilterWrapper、ProtocolListenerWrapper,那么,当对DubboProtocol的实例完成了IOC之后,就会先调用new ProtocolFilterWrapper(DubboProtocol实例)生成一个新的Protocol的实例,再对此实例进行IOC,完了之后,会再调用new ProtocolListenerWrapper(ProtocolFilterWrapper实例)生成一个新的Protocol的实例,然后进行IOC,从而完成DubboProtocol实例的AOP。 + + ```java + private T createExtension(String name) { + + //根据name获取扩展类 {name: Class} key-Value 接口的所有实现类 + 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); + + // AOP:对依赖注入后的实例进行AOP(Wrapper),把当前接口类的所有的Wrapper全部一层一层包裹在实例对象上 + // 每包裹个Wrapper后,也会对Wrapper对象进行依赖注入 + Set> wrapperClasses = cachedWrapperClasses; + if (CollectionUtils.isNotEmpty(wrapperClasses)) { + for (Class wrapperClass : wrapperClasses) { + instance = injectExtension((T) wrapperClass.getConstructor(type).newInstance(instance)); + } + } + //返回最终的Wrapper对象,debug一下你就会看到返回的不是redCar而是carWrapper + return instance; + } catch (Throwable t) { + throw new IllegalStateException("Extension instance (name: " + name + ", class: " + + type + ") couldn't be instantiated: " + t.getMessage(), t); + } + } + + ``` + + + +### 自适应扩展点补充 + +1. 上面提到的自适应扩展点对象,也就是某个接口的代理对象是通过Dubbo内部生成代理类,然后生成代理对象的。 +2. 额外的,在Dubbo中还设计另外一种机制来生成自适应扩展点,这种机制就是可以通过@Adaptive注解来指定某个类为某个接口的代理类,如果指定了,Dubbo在生成自适应扩展点对象时实际上生成的就是@Adaptive注解所注解的类的实例对象。 + +#### createAdaptiveExtensionClass方法 + +createAdaptiveExtensionClass方法就是Dubbo中默认生成Adaptive类实例的逻辑。说白了,这个实例就是当前这个接口的一个代理对象。比如下面的代码: + +```java +ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class); +Protocol protocol = extensionLoader.getAdaptiveExtension(); +``` + +这个代码就是Protocol接口的一个代理对象,那么代理逻辑就是在**new** AdaptiveClassCodeGenerator(**type**, **cachedDefaultName**).generate()方法中。 + +1. type就是接口 +2. cacheDefaultName就是该接口默认的扩展点实现的名字 + + + +再看个例子,Protocol接口的Adaptive类的代理: + +```java +package org.apache.dubbo.rpc; +import org.apache.dubbo.common.extension.ExtensionLoader; +public class Protocol$Adaptive implements org.apache.dubbo.rpc.Protocol { + + public void destroy() { + throw new UnsupportedOperationException("The method public abstract void org.apache.dubbo.rpc.Protocol.destroy() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); + } + + public int getDefaultPort() { + throw new UnsupportedOperationException("The method public abstract int org.apache.dubbo.rpc.Protocol.getDefaultPort() of interface org.apache.dubbo.rpc.Protocol is not adaptive method!"); + } + + public org.apache.dubbo.rpc.Exporter export(org.apache.dubbo.rpc.Invoker arg0) throws org.apache.dubbo.rpc.RpcException { + if (arg0 == null) + throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument == null"); + if (arg0.getUrl() == null) + throw new IllegalArgumentException("org.apache.dubbo.rpc.Invoker argument getUrl() == null"); + + org.apache.dubbo.common.URL url = arg0.getUrl(); + + String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() ); + + if(extName == null) + throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])"); + + org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); + + return extension.export(arg0); + } + + public org.apache.dubbo.rpc.Invoker refer(java.lang.Class arg0, org.apache.dubbo.common.URL arg1) throws org.apache.dubbo.rpc.RpcException { + + if (arg1 == null) throw new IllegalArgumentException("url == null"); + + org.apache.dubbo.common.URL url = arg1; + + String extName = ( url.getProtocol() == null ? "dubbo" : url.getProtocol() ); + + if(extName == null) throw new IllegalStateException("Failed to get extension (org.apache.dubbo.rpc.Protocol) name from url (" + url.toString() + ") use keys([protocol])"); + + org.apache.dubbo.rpc.Protocol extension = (org.apache.dubbo.rpc.Protocol)ExtensionLoader.getExtensionLoader(org.apache.dubbo.rpc.Protocol.class).getExtension(extName); + + return extension.refer(arg0, arg1); + } +} +``` + + + + + +可以看到,Protocol接口中有四个方法,但是只有export和refer两个方法进行代理。为什么?因为Protocol接口中在export方法和refer方法上加了@Adaptive注解。但是,不是只要在方法上加了@Adaptive注解就可以进行代理,还有其他条件,比如: + +1. 该方法如果是无参的,那么则会报错 +2. 该方法有参数,可以有多个,并且其中某个参数类型是URL,那么则可以进行代理 +3. 该方法有参数,可以有多个,但是没有URL类型的参数,那么则不能进行代理 +4. 该方法有参数,可以有多个,没有URL类型的参数,但是如果这些参数类型,对应的类中存在getUrl方法(返回值类型为URL),那么也可以进行代理 + + + +所以,可以发现,某个接口的Adaptive对象,在调用某个方法时,是通过该方法中的URL参数或者getURL方法,通过调用ExtensionLoader.getExtensionLoader(com.luban.Car.class).getExtension(extName);得到一个扩展点实例,然后调用该实例对应的方法。 + + + +### Activate扩展点 + +上文说到,每个扩展点都有一个name,通过这个name可以获得该name对应的扩展点实例,但是有的场景下,希望一次性获得多个扩展点实例 + +#### demo + +```java +ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(Filter.class); +URL url = new URL("http://", "localhost", 8080); +url = url.addParameter("cache", "test"); + +List activateExtensions = extensionLoader.getActivateExtension(url, + new String[]{"validation"}, + CommonConstants.CONSUMER); +for (Filter activateExtension : activateExtensions) { + System.out.println(activateExtension); +} +``` + + + +会找到5个Filter + +```java +org.apache.dubbo.rpc.filter.ConsumerContextFilter@4566e5bd +org.apache.dubbo.rpc.protocol.dubbo.filter.FutureFilter@1ed4004b +org.apache.dubbo.monitor.support.MonitorFilter@ff5b51f +org.apache.dubbo.cache.filter.CacheFilter@25bbe1b6 +org.apache.dubbo.validation.filter.ValidationFilter@5702b3b1 +``` + +前三个是通过CommonConstants.CONSUMER找到的 + +CacheFilter是通过url中的参数找到的 + +ValidationFilter是通过指定的name找到的 + + + +在一个扩展点类上,可以添加@Activate注解,这个注解的属性有: + +1. String\[\] group():表示这个扩展点是属于哪组的,这里组通常分为PROVIDER和CONSUMER,表示该扩展点能在服务提供者端,或者消费端使用 +2. String\[\] value():表示的是URL中的某个参数key,当利用getActivateExtension方法来寻找扩展点时,如果传入的url中包含的参数的所有key中,包括了当前扩展点中的value值,那么则表示当前url可以使用该扩展点。 \ No newline at end of file diff --git a/docs/suibi/我的校招-不完全知识点整理.md b/docs/suibi/我的校招-不完全知识点整理.md index ba3d025..35467e0 100644 --- a/docs/suibi/我的校招-不完全知识点整理.md +++ b/docs/suibi/我的校招-不完全知识点整理.md @@ -435,7 +435,7 @@ public class JZ_056 { } ``` - + 由上面代码可以看出来,this()方法可以调用本类的无参构造函数。如果本类继承的有父类,那么无参构造函数会有一个隐式的super()的存在。所以在同一个构造函数里面如果this()之后再调用super(),那么就相当于调用了两次super(),也就是调用了两次父类的无参构造,就失去了语句的意义,编译器也不会通过。 @@ -525,7 +525,7 @@ https://blog.csdn.net/striner/article/details/86375684 2. 遍历链表或调用红黑树相关的删除方法 3. 从 LinkedHashMap 维护的双链表中移除要删除的节点 - + @@ -537,7 +537,7 @@ https://blog.csdn.net/qq_36610462/article/details/83277524 ## 集合继承结构 - + @@ -675,11 +675,11 @@ https://blog.csdn.net/puppylpg/article/details/80433271 这里我只截图下我准备的面试题目录,因为所有答案你都能在笔者的JVM文章里找到答案 - + - + @@ -1335,7 +1335,7 @@ redis cluster 功能强大,直接集成了 replication 和 sentinel 的功能 ## 存储引擎Innodb和Myisam的区别以及使用场景 - + @@ -2039,7 +2039,7 @@ snowflake 算法是 twitter 开源的分布式 id 生成算法,采用 Scala ## Bean生命周期 - + @@ -2199,7 +2199,7 @@ https://github.com/Snailclimb/springboot-guide/blob/master/docs/interview/spring - + @@ -2331,7 +2331,7 @@ private final Map earlySingletonObjects = new HashMap + @@ -2416,7 +2416,7 @@ https://blog.csdn.net/j04110414/article/details/78914787 - + 从整个秒杀系统的架构其实和一般的互联网系统架构本身没有太多的不同,核心理念还是通过缓存、异步、限流来保证系统的高并发和高可用。下面从一笔秒杀交易的流程来描述下秒杀系统架构设计的要点: