开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

详细分析:当我们用浏览器访问一个网站到页面展示,背后经历了什

发表于 2021-11-28

这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战


当我们用浏览器访问一个网站到页面显示出来,身为用户我们只要用鼠标点点点就实现了页面展示,但是实际过程是有亿点点复杂。

大致过程为:

  1. 域名解析获取域名对应的IP地址
  2. 获得服务器的IP地址后与其建立TCP连接
  3. 客户机发送请求和接收资源

那么具体是怎样的过程的呢?


一、前提概要


1、网络分层模型


2、什么是IP地址?

  • IP地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。
  • IP地址是32比特的全球唯一标识符
  • IP地址在整个因特网范围内是唯一的。

3、什么是域名?

  • 因特网采用层次树状结构的命名方法,任何一个连接到因特网的主机或路由器,都有一个唯一的层次结构名称, 即域名(DomainDomainDomain NameNameName)。
  • 域(DomainDomainDomain) 是名字空间中一个可被管理的划分。域还可以划分为子域,而子域还可以继续划分为子域的子域,这样就形成了顶级域、二 级域、三级域等。
  • 每个域名都由标号序列组成,而各标号之间用点(“.”) 隔开。

4、什么是DNS

  • 域名系统(DomainDomainDomain NameNameName SystemSystemSystem,DNSDNSDNS):因特网使用的命名系统,用来把便于人们记忆的具有特定含义的主机名(如 www.baidu.com)转换为便于机器处理的 IPIPIP 地址。
  • DNS使用了大量的域名服务器,他们以层次方式组织
    1. 根域名服务器1
    2. 顶级域名服务器2
    3. 授权域名服务器(权限域名服务器)3
    4. 本地域名服务器4

二、漫漫取经之路就在脚下


1、域名解析

访问www.baidu.com时,需要将域名映射为对应的 IPIPIP 地址。即域名解析的过程。

  1. 首先由客户机向其本地域名服务器发出一个 DNSDNSDNS 请求报文
  2. 本地域名服务器在收到请求后,先查询本地缓存中是否存在该记录,若没有,则以 DNSDNSDNS 客户的身份向根域名服务器发出解析请求报文
  3. 根域名服务器根据收到的请求,判断该域名(www.baidu.comwww.baidu.comwww.baidu.com)属于.com.com.com 域,将对应的顶级域名服务器 dns.comdns.comdns.com 的 IPIPIP 地址返回到本地域名服务器
  4. 本地域名服务器在向顶级域名服务器 dns.comdns.comdns.com 发送解析请求报文
  5. 顶级域名服务器 dns.comdns.comdns.com 根据请求,判断该域名属于(baidu.combaidu.combaidu.com)域,将授权域名服务器 dns.baidu.comdns.baidu.comdns.baidu.com 的IP地址返回给本地域名服务器
  6. 本地域名服务器在向授权域名服务器 dns.baidu.comdns.baidu.comdns.baidu.com 发送解析请求报文
  7. 授权域名服务器 dns.baidu.comdns.baidu.comdns.baidu.com 根据请求,判断域名属于(www.baidu.comwww.baidu.comwww.baidu.com )域,将查询结果(即域名www.baidu.comwww.baidu.comwww.baidu.com 对应的IP地址)返回给本地域名服务器
  8. 本地域名服务器将域名www.baidu.comwww.baidu.comwww.baidu.com 对应 IPIPIP 地址保存到本地缓存,并返回给客户机,至此域名解析完成

那么是怎么通过 TCP/IPTCP/IPTCP/IP 网络结构发送到域名服务器?

  1. 应用层发送一个 DNSDNSDNS 请求报文,传送给传输层
  2. 传输层为 UDPUDPUDP 请求报文加上 UDPUDPUDP 请求头,形成 UDPUDPUDP 数据报,传送给网络层
  3. 网络层在 UDPUDPUDP 数据报的基础上添加 IPIPIP 请求头形成 IPIPIP 数据报,用 ARPARPARP 地址解析协议解析出下一跳的 MACMACMAC 地址
  4. 数据链路层为 IPIPIP 数据报加上帧头和帧尾,封装成帧
  5. 物理层将报文传输给下一跳,(这里下一跳即域名服务器,事实上应该会跳转多个路由器)传输到域名服务器,域名服务器的每层再对数据进行一个拆封的过程。
  6. 域名服务器查询到 IPIPIP 地址之后原路返回

2、建立TCP连接(三次握手)

  1. 服务器上会有一个进程一直在不断地监听 TCPTCPTCP 80端口
  2. 客户机在获取到对应的 IPIPIP 地址之后,客户机向服务器发送一个连接请求报文
  3. 服务器监听到连接请求报文之后,向客户机返回一个确认报文,并为此次 TCPTCPTCP 连接分配缓存和变量
  4. 当客户机收到确认报文段后,再向服务器发出一个确认报文,并为此次连接分配缓存和变量
  5. 服务器再收到确认报文之后, TCPTCPTCP 连接就建立成功了

3、获取资源

设HTTP协议使用持久连接5:

  1. TCP连接建立成功之后,客户机向服务器通过TCP连接发送携带请求资源信息的HTTP请求报文
  2. 服务器收到HTTP请求报文之后,通过HTTP响应报文将客户机请求的资源发送给客户机
  3. 如需多个资源,重复以上操作即可

注:

实际上,客户机向服务器在建立TCP的第三次握手时发送的确认报文可以携带上HTTP请求报文


4、浏览器解释

  • 客户机在接收到服务器返回的资源后,对其进行解释,用超文本标记语言(HTMLHTMLHTML)将其显示在屏幕上

把握住今天,胜过两个明天。

Footnotes

  1. 最高层次的域名服务器 ↩
  2. 负责管理再该顶级域名服务器注册的所有二级域名 ↩
  3. 将管辖的主机名转换为该主机的IP地址 ↩
  4. 当主机发出DNS查询请求时,这个请求报文就发送给该主机的本地域名服务器 ↩
  5. 持久连接,是指万维网服务器在发送响应后仍保持这条连接,使同一个客户和服务器可以继续使用这条连接传送后续的HTTP请求与响应报文。HTTP/1.1HTTP/1.1HTTP/1.1 支持 ↩

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

消息队列(如 Kafka 等)的应用场景

发表于 2021-11-28

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」。

消息队列在实际应用中包括如下四个场景:

应用耦合:多应用间通过消息队列对同一消息进行处理,避免调用接口失败导致整个过程失败;

异步处理:多应用对消息队列中同一消息进行处理,应用间并发处理消息,相比串行处理,减少处理时间;

限流削峰:广泛应用于秒杀或抢购活动中,避免流量过大导致应用系统挂掉的情况;

消息驱动的系统:系统分为消息队列、消息生产者、消息消费者,生产者负责产生消息,消费者(可能有多个)负责对消息进行处理;

下面详细介绍上述四个场景以及消息队列如何在上述四个场景中使用:

  1. 异步处理

具体场景:用户为了使用某个应用,进行注册,系统需要发送注册邮件并验证短信。对这两个操作的处理方式有两种:串行及并行。

(1)串行方式:新注册信息生成后,先发送注册邮件,再发送验证短信;

在这种方式下,需要最终发送验证短信后再返回给客户端。

(2)并行处理:新注册信息写入后,由发短信和发邮件并行处理;

在这种方式下,发短信和发邮件 需处理完成后再返回给客户端。

假设以上三个子系统处理的时间均为 50ms,且不考虑网络延迟,则总的处理时间:

串行:50+50+50=150ms 并行:50+50 = 100ms

若使用消息队列:

并在写入消息队列后立即返回成功给客户端,则总的响应时间依赖于写入消息队列的时间,而写入消息队列的时间本身是可以很快的,基本可以忽略不计,因此总的处理时间相比串行提高了 2 倍,相比并行提高了一倍;

  1. 应用耦合

具体场景:用户使用 QQ 相册上传一张图片,人脸识别系统会对该图片进行人脸识别,一般的做法是,服务器接收到图片后,图片上传系统立即调用人脸识别系统,调用完成后再返回成功,如下图所示:

该方法有如下缺点:

人脸识别系统被调失败,导致图片上传失败;

延迟高,需要人脸识别系统处理完成后,再返回给客户端,即使用户并不需要立即知道结果;

图片上传系统与人脸识别系统之间互相调用,需要做耦合;

若使用消息队列:

客户端上传图片后,图片上传系统将图片信息如 uin、批次写入消息队列,直接返回成功;而人脸识别系统则定时从消息队列中取数据,完成对新增图片的识别。此时图片上传系统并不需要关心人脸识别系统是否对这些图片信息的处理、以及何时对这些图片信息进行处理。事实上,由于用户并不需要立即知道人脸识别结果,人脸识别系统可以选择不同的调度策略,按照闲时、忙时、正常时间,对队列中的图片信息进行处理。

  1. 限流削峰

具体场景:购物网站开展秒杀活动,一般由于瞬时访问量过大,服务器接收过大,会导致流量暴增,相关系统无法处理请求甚至崩溃。而加入消息队列后,系统可以从消息队列中取数据,相当于消息队列做了一次缓冲。

该方法有如下优点:

请求先入消息队列,而不是由业务处理系统直接处理,做了一次缓冲,极大地减少了业务处理系统的压力;

队列长度可以做限制,事实上,秒杀时,后入队列的用户无法秒杀到商品,这些请求可以直接被抛弃,返回活动已结束或商品已售完信息;

  1. 消息驱动的系统

具体场景:用户新上传了一批照片, 人脸识别系统需要对这个用户的所有照片进行聚类,聚类完成后由对账系统重新生成用户的人脸索引(加快查询)。这三个子系统间由消息队列连接起来,前一个阶段的处理结果放入队列中,后一个阶段从队列中获取消息继续处理。

该方法有如下优点:

避免了直接调用下一个系统导致当前系统失败;

每个子系统对于消息的处理方式可以更为灵活,可以选择收到消息时就处理,可以选择定时处理,也可以划分时间段按不同处理速度处理;

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【JavaSE】Collection 接口和常用方法 1

发表于 2021-11-28
  • 这是我参与11月更文挑战的第23天,活动详情查看:2021最后一次更文挑战
  1. Collection 接口实现类的特点

1
java复制代码public interface Collection<E> extends Iterable<E>

1)Collection实现子类可以存放多个元素,每个元素可以是Object
2)有些Collection的实现类,可以存放重复的元素,有些不可以
3)Collection的实现类,有些是有序的(List),有些不是有序(Set)
4)Collection接口没有直接的实现子类,是通过它的子接口Set 和 List来实现的

  1. Collection 接口常用方法

  • 以实现子类 ArrayList 来演示 , CollectionMethod.java
  1. add:添加单个元素
1
2
3
4
5
6
java复制代码	List list = new ArrayList();
// add:添加单个元素
list.add("兮动人");
list.add(11);//list.add(new Integer(11))
list.add(true);
System.out.println("list=" + list);

在这里插入图片描述

  1. remove:删除指定元素
  • 指定删除第一个元素的索引
1
2
3
4
5
6
7
8
9
java复制代码	List list = new ArrayList();
//1.add:添加单个元素
list.add("兮动人");
list.add(11);//list.add(new Integer(11))
list.add(true);
System.out.println("list=" + list);
//2.remove:删除指定元素
list.remove(0);//删除第一个元素
System.out.println("list=" + list);

在这里插入图片描述

  • 指定删除某个元素
1
2
3
4
5
6
7
8
9
10
java复制代码	List list = new ArrayList();
//1.add:添加单个元素
list.add("兮动人");
list.add(11);//list.add(new Integer(11))
list.add(true);
System.out.println("list=" + list);
//2.remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);

在这里插入图片描述

  1. container:查找元素是否存在
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码	List list = new ArrayList();
//1.add:添加单个元素
list.add("兮动人");
list.add(11);//list.add(new Integer(11))
list.add(true);
System.out.println("list=" + list);
//2.remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);
//3.container:查找元素是否存在
System.out.println(list.contains("兮动人"));//T

在这里插入图片描述

  1. size:获取元素个数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码	List list = new ArrayList();
//1.add:添加单个元素
list.add("兮动人");
list.add(11);//list.add(new Integer(11))
list.add(true);
System.out.println("list=" + list);
//2.remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);
//3.container:查找元素是否存在
System.out.println(list.contains("兮动人"));//T
//4.size获取元素个数
System.out.println(list.size());//2

在这里插入图片描述

  1. isEmpty:判断是否为空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码	List list = new ArrayList();
//1.add:添加单个元素
list.add("兮动人");
list.add(11);//list.add(new Integer(11))
list.add(true);
System.out.println("list=" + list);
//2.remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);
//3.container:查找元素是否存在
System.out.println(list.contains("兮动人"));//T
//4.size获取元素个数
System.out.println(list.size());//2
//5.isEmpty:判断是否为空
System.out.println(list.isEmpty());//F

在这里插入图片描述

  1. clear:清空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码	List list = new ArrayList();
//1.add:添加单个元素
list.add("兮动人");
list.add(11);//list.add(new Integer(11))
list.add(true);
System.out.println("list=" + list);
//2.remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);
//3.container:查找元素是否存在
System.out.println(list.contains("兮动人"));//T
//4.size获取元素个数
System.out.println(list.size());//2
//5.isEmpty:判断是否为空
System.out.println(list.isEmpty());//F
//6.clear:清空
list.clear();
System.out.println("list=" + list);

在这里插入图片描述

  1. addAll:添加多个元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码	List list = new ArrayList();
//1.add:添加单个元素
list.add("兮动人");
list.add(11);//list.add(new Integer(11))
list.add(true);
System.out.println("list=" + list);
//2.remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);
//3.container:查找元素是否存在
System.out.println(list.contains("兮动人"));//T
//4.size获取元素个数
System.out.println(list.size());//2
//5.isEmpty:判断是否为空
System.out.println(list.isEmpty());//F
//6.clear:清空
list.clear();
System.out.println("list=" + list);
//7.addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("百年孤独");
list2.add("边城");
list.addAll(list2);
System.out.println("list=" + list);

在这里插入图片描述

  1. containerALL:查找多个元素是否存在
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码	List list = new ArrayList();
//1.add:添加单个元素
list.add("兮动人");
list.add(11);//list.add(new Integer(11))
list.add(true);
System.out.println("list=" + list);
//2.remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);
//3.container:查找元素是否存在
System.out.println(list.contains("兮动人"));//T
//4.size获取元素个数
System.out.println(list.size());//2
//5.isEmpty:判断是否为空
System.out.println(list.isEmpty());//F
//6.clear:清空
list.clear();
System.out.println("list=" + list);
//7.addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("百年孤独");
list2.add("边城");
list.addAll(list2);
System.out.println("list=" + list);
//8.containerALL:查找多个元素是否存在
System.out.println(list.containsAll(list2));//T

在这里插入图片描述

  1. removeAll:删除多个元素
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码	List list = new ArrayList();
//1.add:添加单个元素
list.add("兮动人");
list.add(11);//list.add(new Integer(11))
list.add(true);
System.out.println("list=" + list);
//2.remove:删除指定元素
//list.remove(0);//删除第一个元素
list.remove(true);//指定删除某个元素
System.out.println("list=" + list);
//3.container:查找元素是否存在
System.out.println(list.contains("兮动人"));//T
//4.size获取元素个数
System.out.println(list.size());//2
//5.isEmpty:判断是否为空
System.out.println(list.isEmpty());//F
//6.clear:清空
list.clear();
System.out.println("list=" + list);
//7.addAll:添加多个元素
ArrayList list2 = new ArrayList();
list2.add("百年孤独");
list2.add("边城");
list.addAll(list2);
System.out.println("list=" + list);
//8.containerALL:查找多个元素是否存在
System.out.println(list.containsAll(list2));//T
//9.removeAll:删除多个元素
list.removeAll(list2);
System.out.println("list=" + list);

在这里插入图片描述

  1. Collection接口遍历元素方式1- 使用Iterator(迭代器)

  • 基本介绍
  1. Iterator对象称为迭代器,主要用于遍历Collection集合中的元素。
  2. 所有实现了Collection接口的集合类都有一个iterator()方法,用以返回一个实现了lterator接口的对象,即可以返回一个迭代器。
  3. Iterator的结构
  4. Iterator仅用于遍历集合,lterator本身并不存放对象。

迭代器的执行原理

1
2
3
4
5
6
java复制代码Iterator iterator = coll.iterator();//得到一个集合的迭代器
//hasNext():判断是否还有下一个元素
while(iterator.hasNext()){
//next()作用:1.指针下移 2.将下移以后集合位置上的元素返回
System.out.println(iterator.next());
}

在这里插入图片描述

  • Iterator接口的方法
    在这里插入图片描述
    在这里插入图片描述
  • 提示:在调用iterator.next()方法之前必须要调用iterator.hasNext()进行检测。若不调用,且下一条记录无效,直接调用iterator.next()会抛出NoSuchElementException异常。
  • 迭代器的使用:案例演示 CollectionIterator.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码public class CollectionIterator {
public static void main(String[] args) {
Collection col = new ArrayList();

col.add(new Book("三国演义", "罗贯中",10.1));
col.add(new Book("红楼梦", "曹雪芹",20.6));
col.add(new Book("西游记", "吴承恩",30.5));

//System.out.println("col=" + col);
//希望能够遍历 col 集合
//1.先得到 col 对应的 迭代器
Iterator iterator = col.iterator();
//2.使用 while 循环遍历
while (iterator.hasNext()) {//判断是否还有数据
//返回下一个元素,类型是 Object
Object obj = iterator.next();
System.out.println("obj=" + obj);

}
//3.当退出while循环后,这时 iterator 指向最后的元素,会抛出异常
//iterator.next(); // NoSuchElementException

//4.如果希望再次遍历,需要重置迭代器
iterator = col.iterator();
System.out.println("====第二次遍历====");
while (iterator.hasNext()) {
Object obj = iterator.next();
System.out.println("obj=" + obj);
}
}
}

class Book {
private String name;
private String author;
private double price;

public Book(String name, String author, double price) {
this.name = name;
this.author = author;
this.price = price;
}

@Override
public String toString() {
return "Book{" +
"name='" + name + '\'' +
", author='" + author + '\'' +
", price=" + price +
'}';
}
}

在这里插入图片描述

  1. Collection 接口遍历对象方式2- for 循环增强

  • 增强for循环,可以代替iterator迭代器,特点:增强for就是简化版的iterator,本质一样。只能用于遍历集合或数组。
  • 基本语法
1
2
3
java复制代码for(元素类型 元素名 : 集合名或数组名){
访问元素
}
  • 案例演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码public class CollectionFor {
@SuppressWarnings("all")
public static void main(String[] args) {
Collection col = new ArrayList();

col.add(new Book("三国演义", "罗贯中",10.1));
col.add(new Book("红楼梦", "曹雪芹",20.6));
col.add(new Book("西游记", "吴承恩",30.5));

//解读:
//1.使用增强 for 循环
//2.增强 for ,底层仍然是迭代器
//3.增强 for 可以理解为就是简化版的 迭代器 遍历
for (Object book : col) {
System.out.println("book=" + book);
}
//增强for,也可以直接在数组中使用
int[] nums = {1, 2, 3, 4, 5, 6};
for (int num : nums) {
System.out.println("num=" + num);
}

}
}

在这里插入图片描述

  1. 练习

  • 请编写程序 CollectionExercise.java

1.创建3个 Dog {name,age}对象,放入到ArrayList 中,赋给 List引用
2.用迭代器和增强for循环两种方式来遍历
3.重写Dog的toString方法,输出name和age

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
java复制代码public class CollectionExercise {
public static void main(String[] args) {
List list = new ArrayList();
list.add(new Dog("小黑", 2));
list.add(new Dog("小白", 4));
list.add(new Dog("小青", 6));

//1.使用迭代器,快速生成while:itit 查看所有快捷键:Ctrl + j
Iterator iterator = list.iterator();
while (iterator.hasNext()) {
Object dog = iterator.next();
System.out.println("dog=" + dog);
}

System.out.println("======使用for循环增强======");
//2.for循环增强,快捷键:list.fori 或 I
for (Object dog : list) {
System.out.println("dog=" + dog);
}



}
}

class Dog {
private String name;
private int age;

public Dog(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Dog{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Pyshorteners 创建你的专属短连接! 前言 关

发表于 2021-11-28

「这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战」。

前言

在我们的日常工作生活中,通常遇到一些很长的超链接,当你想要将链接转发或者记录的时候,由于链接很长而不方便操作,国内云厂商提供的短链接服务又是收费的,这让生活本就贫苦的自己怎么办呢?推荐你使用 pyshorteners,两行代码将长连接变短,从而创建专属于你的短链接。

关于 pyshorteners

它是 Python 的一个流行的第三方库,能够方便快捷的帮你生产简单的短链接,从而让你的工作生活变得美好。

实践

安装

1
复制代码pip install pyshorteners

一个例子入门

1
2
3
4
5
6
7
8
ini复制代码from pyshorteners import Shortener

# 实例化短链接引擎
short_engine = Shortener()

# 使用tinyurl缩短
res = short_engine.tinyurl.short('https://phygerr.github.io/httpx-%E4%BC%98%E7%A7%80%E7%9A%84http%E5%AE%A2%E6%88%B7%E7%AB%AF/')
print(res)

图片

代码运行结果

短链接测试

浏览器打开短链接,测试其能否正常跳转。

图片

跳转成功

短链列表

tinyurl 的缩短功能可以直接使用,但是部分短链接比如 po.st 这种短链是需要注册后使用 APIkey 才能使用的。

pyshorteners 支持的短链类型如下:

断链 是否需要Key
Adf.ly True
Bit.ly True
Cutt.ly True
Git.io True
Po.st True
Short.cm True
Tiny.cc True
TinyURL.com False
Qps.ru False
Ow.ly False
Os.db False
NullPointer False
Is.gd False
Da.gd False
Clck.ru False
Chilp.it False

多缩短几个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ini复制代码from pyshorteners import Shortener

# 实例化短链接引擎
short_engine = Shortener()

base_url='https://phygerr.github.io/httpx-%E4%BC%98%E7%A7%80%E7%9A%84http%E5%AE%A2%E6%88%B7%E7%AB%AF/'

# 缩短
res1 = short_engine.tinyurl.short(base_url)
res2 = short_engine.osdb.short(base_url)
res3 = short_engine.isgd.short(base_url)
res4 = short_engine.dagd.short(base_url)
res5 = short_engine.qpsru.short(base_url)

print(res1+'\n',res2+'\n',res3+'\n',res4+'\n',res5+'\n')

图片

代码运行结果

通过对比,你会发现 isgd 和 dagd 的短链相对比较简洁。

5单独说说 NullPointer

之所以单独拿它出来说, 是因为 nullpointer 支持自定义域,目前它支持:0x0.st 和 ttm.sh 两个域。用户可以在实例化缩短引擎的时候自己定义,默认为:0x0.st。

默认

1
2
3
4
5
6
7
8
9
10
11
ini复制代码from pyshorteners import Shortener

# 实例化短链接引擎
short_engine = Shortener()

base_url='https://phygerr.github.io/httpx-%E4%BC%98%E7%A7%80%E7%9A%84http%E5%AE%A2%E6%88%B7%E7%AB%AF/'

# NullPointer,default domain is https://0x0.st
res = short_engine.nullpointer.short(base_url)

print(res)

图片

代码运行结果

指定

1
2
3
4
5
6
7
8
9
10
11
ini复制代码from pyshorteners import Shortener

# 实例化短链接引擎
short_engine = Shortener(domain='https://ttm.sh')

base_url='https://phygerr.github.io/httpx-%E4%BC%98%E7%A7%80%E7%9A%84http%E5%AE%A2%E6%88%B7%E7%AB%AF/'

# NullPointer,default domain is https://0x0.st
res = short_engine.nullpointer.short(base_url)

print(res)

图片

代码运行结果

你会发现,NullPointer 生成的短链接非常优秀。

以上就是今天的全部内容了,感谢您的阅读,我们下节再会。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Netty编程(九)—— 协议设计与解析

发表于 2021-11-28

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

TCP/IP 中消息传输基于流的方式,没有边界,而协议的目的就是划定消息的边界,制定通信双方要共同遵守的通信规则

Redis协议

如果我们要向Redis服务器发送一条set name Nyima的指令,需要遵守如下协议

1
2
3
4
5
6
7
8
9
10
11
swift复制代码// 该指令一共有3部分,每条指令之后都要添加回车与换行符
*3\r\n
// 第一个指令的长度是3
$3\r\n
// 第一个指令是set指令
set\r\n
// 下面的指令以此类推
$4\r\n
name\r\n
$5\r\n
Nyima\r\n

下面是客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码public class RedisClient {
static final Logger log = LoggerFactory.getLogger(StudyServer.class);
public static void main(String[] args) {
NioEventLoopGroup group = new NioEventLoopGroup();
try {
ChannelFuture channelFuture = new Bootstrap()
.group(group)
.channel(NioSocketChannel.class)
.handler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
// 打印日志
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
ch.pipeline().addLast(new ChannelInboundHandlerAdapter() {
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
// 回车与换行符
final byte[] LINE = {'\r','\n'};
// 获得ByteBuf
ByteBuf buffer = ctx.alloc().buffer();
// 连接建立后,向Redis中发送一条指令,注意添加回车与换行
// set name Nyima
buffer.writeBytes("*3".getBytes());
buffer.writeBytes(LINE);
buffer.writeBytes("$3".getBytes());
buffer.writeBytes(LINE);
buffer.writeBytes("set".getBytes());
buffer.writeBytes(LINE);
buffer.writeBytes("$4".getBytes());
buffer.writeBytes(LINE);
buffer.writeBytes("name".getBytes());
buffer.writeBytes(LINE);
buffer.writeBytes("$5".getBytes());
buffer.writeBytes(LINE);
buffer.writeBytes("Nyima".getBytes());
buffer.writeBytes(LINE);
ctx.writeAndFlush(buffer);
}

});
}
})
.connect(new InetSocketAddress("localhost", 6379));
channelFuture.sync();
// 关闭channel
channelFuture.channel().close().sync();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 关闭group
group.shutdownGracefully();
}
}
}

控制台输出结果:
在这里插入图片描述

之后服务端接到了发送的数据包后就能够根据协议进行解析内容

HTTP协议

HTTP协议在请求行请求头中都有很多的内容,自己实现较为困难,可以使用HttpServerCodec作为服务器端的解码器与编码器,来处理HTTP请求

1
2
3
4
scala复制代码// HttpServerCodec 中既有请求的解码器 HttpRequestDecoder 又有响应的编码器 HttpResponseEncoder
// Codec(CodeCombine) 一般代表该类既作为 编码器 又作为 解码器
public final class HttpServerCodec extends CombinedChannelDuplexHandler<HttpRequestDecoder, HttpResponseEncoder>
implements HttpServerUpgradeHandler.SourceCodecCopy

使用这个方法可以发现会解析出两种类型的消息:请求行请求头、请求体

tips: 可以使用ch.pipeline().addLast(new SimpleChannelInboundHandler())指定单对一种类型的消息感兴趣,这个是只对HttpRequest感兴趣

下面是服务器代码, 服务器负责处理请求并响应浏览器。所以只需要处理HTTP请求即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码public class HttpServer {
static final Logger log = LoggerFactory.getLogger(StudyServer.class);

public static void main(String[] args) {
NioEventLoopGroup group = new NioEventLoopGroup();
new ServerBootstrap()
.group(group)
.channel(NioServerSocketChannel.class)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
// 作为服务器,使用 HttpServerCodec 作为编码器与解码器
ch.pipeline().addLast(new HttpServerCodec());
// 服务器只处理HTTPRequest
ch.pipeline().addLast(new SimpleChannelInboundHandler<HttpRequest>() {
@Override
protected void channelRead0(ChannelHandlerContext ctx, HttpRequest msg) {
// 获得请求uri
log.debug(msg.uri());

// 获得完整响应,设置版本号与状态码
DefaultFullHttpResponse response = new DefaultFullHttpResponse(msg.protocolVersion(), HttpResponseStatus.OK);
// 设置响应内容
byte[] bytes = "<h1>Hello, World!</h1>".getBytes(StandardCharsets.UTF_8);
// 设置响应体长度,避免浏览器一直接收响应内容
response.headers().setInt(CONTENT_LENGTH, bytes.length);
// 设置响应体
response.content().writeBytes(bytes);

// 写回响应
ctx.writeAndFlush(response);
}
});
}
})
.bind(8080);
}
}

浏览器结果:

在这里插入图片描述

控制台结果:

在这里插入图片描述

自定义协议

除了上面两种协议,我们自己也可以使用netty定义自己的协议

组成要素

  • 魔数:用来在第一时间判定接收的数据是否为无效数据包
  • 版本号:可以支持协议的升级
  • 序列化算法

:消息正文到底采用哪种序列化反序列化方式

+ 如:json、protobuf、hessian、jdk
  • 指令类型:是登录、注册、单聊、群聊… 跟业务相关
  • 请求序号:为了双工通信,提供异步能力
  • 正文长度
  • 消息正文

编码器解码器

  • 编码器与解码器方法源于父类ByteToMessageCodec,通过该类可以自定义编码器与解码器,泛型类型为被编码与被解码的类。==此处使用了自定义类Message,代表消息==
1
scala复制代码public class MessageCodec extends ByteToMessageCodec<Message>
  • 编码器负责将附加信息与正文信息写入到ByteBuf中,其中附加信息总字节数最好为2n,不足需要补齐。==正文内容如果为对象,需要通过序列化将其放入到ByteBuf中==
  • 解码器负责将ByteBuf中的信息取出,并放入List中,该List用于将信息传递给下一个handler
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
java复制代码public class MessageCodec extends ByteToMessageCodec<Message> {

@Override
protected void encode(ChannelHandlerContext ctx, Message msg, ByteBuf out) throws Exception {
// 设置魔数 4个字节
out.writeBytes(new byte[]{'N','Y','I','M'});
// 设置版本号 1个字节
out.writeByte(1);
// 设置序列化方式 1个字节
out.writeByte(1);
// 设置指令类型 1个字节
out.writeByte(msg.getMessageType());
// 设置请求序号 4个字节
out.writeInt(msg.getSequenceId());
// 为了补齐为16个字节,填充1个字节的数据,无意义
out.writeByte(0xff);

// 获得序列化后的msg
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(msg);
byte[] bytes = bos.toByteArray();

// 获得并设置正文长度 长度用4个字节标识
out.writeInt(bytes.length);
// 设置消息正文
out.writeBytes(bytes);
}

@Override
protected void decode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) throws Exception {
// 获取魔数
int magic = in.readInt();
// 获取版本号
byte version = in.readByte();
// 获得序列化方式
byte seqType = in.readByte();
// 获得指令类型
byte messageType = in.readByte();
// 获得请求序号
int sequenceId = in.readInt();
// 移除补齐字节
in.readByte();
// 获得正文长度
int length = in.readInt();
// 获得正文
byte[] bytes = new byte[length];
in.readBytes(bytes, 0, length);
ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
Message message = (Message) ois.readObject();
// 将信息放入List中,传递给下一个handler
out.add(message);

// 打印获得的信息正文
System.out.println("===========魔数===========");
System.out.println(magic);
System.out.println("===========版本号===========");
System.out.println(version);
System.out.println("===========序列化方法===========");
System.out.println(seqType);
System.out.println("===========指令类型===========");
System.out.println(messageType);
System.out.println("===========请求序号===========");
System.out.println(sequenceId);
System.out.println("===========正文长度===========");
System.out.println(length);
System.out.println("===========正文===========");
System.out.println(message);
}
}

编写测试类

  • 测试类中用到了LengthFieldBasedFrameDecoder,避免粘包半包问题
  • 通过MessageCodec的encode方法将附加信息与正文写入到ByteBuf中,通过channel执行入站操作。入站时会调用decode方法进行解码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public class TestCodec {
static final org.slf4j.Logger log = LoggerFactory.getLogger(StudyServer.class);
public static void main(String[] args) throws Exception {
EmbeddedChannel channel = new EmbeddedChannel();
// 添加解码器,避免粘包半包问题
channel.pipeline().addLast(new LengthFieldBasedFrameDecoder(1024, 12, 4, 0, 0));
channel.pipeline().addLast(new LoggingHandler(LogLevel.DEBUG));
channel.pipeline().addLast(new MessageCodec());
// LoginRequestMessage 是自建类
LoginRequestMessage user = new LoginRequestMessage("Nyima", "123");

// 测试编码与解码
ByteBuf byteBuf = ByteBufAllocator.DEFAULT.buffer();
new MessageCodec().encode(null, user, byteBuf);
channel.writeInbound(byteBuf);
}
}

结果:

在这里插入图片描述

在这里插入图片描述

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

数据分析从零开始实战,Pandas读写Excel/XML数据

发表于 2021-11-28

这是我参与11月更文挑战的第18天,活动详情查看:2021最后一次更文挑战

零 写在前面

本系列学习笔记参考书籍: 《数据分析实战》托马兹·卓巴斯,会将自己学习本书的笔记分享给大家,同样开成一个系列『数据分析从零开始实战』。

点击查看第一篇文章:# 数据分析从零开始实战,Pandas读写CSV数据

点击查看第一篇文章:# 数据分析从零开始实战,Pandas读写TSV/Json数据

前面两篇文章讲了数据分析虚拟环境创建和pandas读写csv、tsv、json格式的数据,今天我们继续探索pandas读取数据。

一 基本知识概要

1.利用pandas读写Excel文件

2.利用pandas读写XML文件

二 开始动手动脑

1.利用Python读写Excel

读取,利用Pandas库的ExcelFile()方法。

写入,利用Pandas库的利用to_excel方法。

代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
python复制代码import pandas as pd
import os

# 获取当前文件父目录路径
father_path = os.getcwd()
# 原始数据文件路径
rpath_excel = father_path+r'\data01\realEstate_trans.xlsx'
# 数据保存路径
wpath_excel = father_path+r'\data01\temp_excel.xlsx'

# 打开excel文件
excel_file = pd.ExcelFile(rpath_excel)

# 读取文件内容
"""
ExcelFile对象的parse()方法读取指定工作表的内容
ExcelFile对象的sheet_names属性可以获取Excel文件中的所有工作表
这里还用到了字典表达式来给字典赋值(看起来更加优雅)
"""

excel_read = {sheetName : excel_file.parse(sheetName) for sheetName in excel_file.sheet_names}

# 输出Sacramento表格的price列的头10行记录
print(excel_read['Sacramento'].head(10)['price'])
print(type(excel_read['Sacramento'].head(10)['price']))
# 遇到错误:ModuleNotFoundError: No module named 'xlrd'

# 写入表格的price列的前10行
excel_read['Sacramento'].head(10)['price'].to_excel(wpath_excel, "price", index=False)
# 遇到错误:ModuleNotFoundError: No module named 'openpyxl'
读取结果:

写入结果:

可能报错:
1
2
3
4
vbnet复制代码读操作时:
ModuleNotFoundError: No module named 'xlrd'
写操作时:
ModuleNotFoundError: No module named 'openpyxl'
解决方法:
1
2
3
bash复制代码# 在环境里安装xlrd和openpyxl模块即可
pip install xlrd
pip install openpyxl
2.利用Python读写XML文件

学过java的同学对XML应该不陌生,全称是eXtensible Markup Language(扩展标记语言),虽然平时不常见,但是Web API里支持XML编码。

读写代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
python复制代码import pandas as pd
# 一个轻量的XML解析器
import xml.etree.ElementTree as ET
import os

"""
读入XML数据,返回pa.DataFrame
"""
def read_xml(xml_FileName):
with open(xml_FileName, "r") as xml_file:
# 读取数据,以树的结构存储
tree = ET.parse(xml_file)
# 访问树的梗节点
root = tree.getroot()
# 返回DataFrame格式数据
return pd.DataFrame(list(iter_records(root)))

"""
遍历有记录的生成器
"""
def iter_records(records):
for record in records :
# 保存值的临时字典
temp_dict = {}
# 遍历所有字段
for var in record:
temp_dict[
var.attrib["var_name"]
] = var.text
# 生成值
yield temp_dict

"""
以XML格式保存数据
"""
def write_xml(xmlFileName, data):
with open(xmlFileName, "w") as xmlFile:
# 写头部
xmlFile.write(
'<?xml version="1.0" encoding="UTF-8"?>'
)
xmlFile.write('<records>\n')
# 写数据
xmlFile.write(
'\n'.join(data.apply(xml_encode, axis=1))
)
# 写尾部
xmlFile.write("\n</records>")

"""
以特定的嵌套格式将每一行编码成XML
"""
def xml_encode(row):
# 第一步--输出record节点
xmlItem = [' <record>']
# 第二步--给行中每个字段加上XML格式<field name=···>···</field>
for field in row.index:
xmlItem.append(
'<var var_name="{0}">{1}</var>'.format(field, row[field])
)
# 最后一步--标记record节点的结束标签
xmlItem.append(" </record>")
return '\n'.join(xmlItem)


# 获取当前文件父目录路径
father_path = os.getcwd()
# 原始数据文件路径
rpath_xml = father_path+r'\data01\realEstate_trans.xml'
# 数据保存路径
wpath_xml = father_path+r'\data01\temp_xml.xml'
# 读取数据
xml_read = read_xml(rpath_xml)
# 输出头10行记录
print(xml_read.head(10))
# 以XML格式写回文件
write_xml(wpath_xml, xml_read.head(10))
运行结果

代码解析

(1)read_xml(xml_FileName)函数

功能:读入XML数据,返回pa.DataFrame

这里利用到了一个轻量级的XML解析器:xml.etree.ElementTree。传入文件名,先读取文件内容,然后利用parse()函数解析XML,创建一个树状结构并存放在tree变量中,在tree对象上调用getroot()方法得到根节点,最后调用iter_records()函数,传入根节点,进而将返回的信息转换成DataFrame。

(2)iter_records(records)函数

功能:遍历有记录的生成器

iter_records()方法是一个生成器,从关键字yield可以看出来,与return不同,生成器每次只向主调方法返回一个值,直到结束。

(3)write_xml(xmlFile, data)函数

功能:以XML格式保存数据

这里需要注意的是得按XML文件格式进行保存,我们要做的就是三步:保存头部格式、按格式保存数据、保存尾部格式。保存数据时用到了DataFrame对象的apply()方法,遍历内部每一行,第一个参数xml_encode指定了要应用到每一行记录上的方法,axis=1表示按行处理,默认值为0,表示按列处理。

(4)xml_encode(row)函数

功能:以特定的嵌套格式将每一行编码成XML

在写数据的过程我们会调用这个方法,对每行数据进行处理,变成XML格式。

三 送你的话

昨天开了个会,然后思考了写问题,这里分享给大家:

1、思想觉悟,辩证思考。不要随声附和,要有己见,聪明的人应该是坚持输出自己的思想,从事情本身和和他人评论去思考,再辩正自己的思考,再输出;

2、少喊口号,多做实事。本来我是很推崇做个人规划的,但是,我发现不止是我周边和某些读者朋友,包括我自己,规划作的越来越假大空,规划本身没有错,错的是:现实生活中我们把规划变成了日日口号,而为能如实完成,所以我现在推崇:规划,先做再说。

坚持 and 努力 : 终有所获。

思想很复杂,

实现很有趣,

只要不放弃,

终有成名日。

—《老表打油诗》

下期见,我是爱猫爱技术的老表,如果觉得本文对你学习有所帮助,欢迎点赞、评论、关注我!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

leetcode 1627 Graph Connectiv

发表于 2021-11-28

「这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战」

描述

We have n cities labeled from 1 to n. Two different cities with labels x and y are directly connected by a bidirectional road if and only if x and y share a common divisor strictly greater than some threshold. More formally, cities with labels x and y have a road between them if there exists an integer z such that all of the following are true:

  • x % z == 0,
  • y % z == 0, and
  • z > threshold.

Given the two integers, n and threshold, and an array of queries, you must determine for each queries[i] = [ai, bi] if cities ai and bi are connected directly or indirectly. (i.e. there is some path between them).

Return an array answer, where answer.length == queries.length and answer[i] is true if for the ith query, there is a path between ai and bi, or answer[i] is false if there is no path.

Example 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vbnet复制代码Input: n = 6, threshold = 2, queries = [[1,4],[2,5],[3,6]]
Output: [false,false,true]
Explanation: The divisors for each number:
1: 1
2: 1, 2
3: 1, 3
4: 1, 2, 4
5: 1, 5
6: 1, 2, 3, 6
Using the underlined divisors above the threshold, only cities 3 and 6 share a common divisor, so they are the
only ones directly connected. The result of each query:
[1,4] 1 is not connected to 4
[2,5] 2 is not connected to 5
[3,6] 3 is connected to 6 through path 3--6

Example 2:

1
2
3
4
vbnet复制代码Input: n = 6, threshold = 0, queries = [[4,5],[3,4],[3,2],[2,6],[1,3]]
Output: [true,true,true,true,true]
Explanation: The divisors for each number are the same as the previous example. However, since the threshold is 0,
all divisors can be used. Since all numbers share 1 as a divisor, all cities are connected.

Example 3:

1
2
3
4
vbnet复制代码Input: n = 5, threshold = 1, queries = [[4,5],[4,5],[3,2],[2,3],[3,4]]
Output: [false,false,false,false,false]
Explanation: Only cities 2 and 4 share a common divisor 2 which is strictly greater than the threshold 1, so they are the only ones directly connected.
Please notice that there can be multiple queries for the same pair of nodes [x, y], and that the query [x, y] is equivalent to the query [y, x].

Note:

  • 2 <= n <= 10^4
  • 0 <= threshold <= n
  • 1 <= queries.length <= 10^5
  • queries[i].length == 2
  • 1 <= ai, bi <= cities
  • ai != bi

解析

根据题意,给定 n 个城市,从 1 到 n 标记。 当且仅当 x 和 y 共享一个严格大于某个 threshold 的公约数时,标签为 x 和 y 的两个不同城市通过一条双向道路直接相连。 如果存在一个整数 z 使得以下所有条件都为真,则标签为 x 和 y 的城市之间有一条道路:

  • x % z == 0
  • y % z == 0
  • z > threshold

给定两个整数 n 和 threshold ,以及一个数组 queries ,针对每个 queries[i] = [ai, bi] ,确认城市 ai 和 bi 是否相连接,不管直接还是间接。 返回一个数组 answer ,其中 answer.length == queries.length 并且 answer[i] 如果对于第 i 个查询,如果城市之间存在路径,则 answer[i] 为 true,如果没有路径,则 answer[i] 为 false。

本题其实考察的是快读选择两个城市进行合并,如果用暴力解法,先遍历 x 城市再遍历 y 城市,在判断是否相通,时间复杂度太高了。换一个思路就是遍历大于 threshold 的公约数 t ,然后在小于等于 n 范围内找 t 的倍数,只要是 t 的倍数的城市都可以合并到一块,给这些城市赋予最小的城市 id 作为他们的祖先。最后便利 queries 中的每一对城市,如果他们的祖先相同说明就是相通的,否则说明不相通。

解答

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
python复制代码class Solution(object):
def areConnected(self, n, threshold, queries):
"""
:type n: int
:type threshold: int
:type queries: List[List[int]]
:rtype: List[bool]
"""
father = {i:i for i in range(1, n+1)}
visited = {i:0 for i in range(1, n+1)}
for t in range(threshold+1, n+1):
if visited[t]:continue
for x in range(t, n+1, t):
visited[x] = 1
if self.getFather(x,father) != self.getFather(t,father): self.union(x, t,father)
result = []
for query in queries:
result.append(self.getFather(query[0],father)==self.getFather(query[1],father))
return result

def union(self,a,b,father):
a = father[a]
b = father[b]
if a<b: father[b] = a
else: father[a] = b


def getFather(self,x,father):
if father[x] != x:
father[x] = self.getFather(father[x],father)
return father[x]

运行结果

1
2
erlang复制代码Runtime: 824 ms, faster than 100.00% of Python online submissions for Graph Connectivity With Threshold.
Memory Usage: 51.4 MB, less than 100.00% of Python online submissions for Graph Connectivity With Threshold.

原题链接:leetcode.com/problems/gr…

您的支持是我最大的动力

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

《流畅的Python》读书笔记13(第十一章:接口:从协议到

发表于 2021-11-28

11.1 Python文化中的接口和协议

协议被定义为非正式接口,是让Python这种动态类型语言实现多态的方式

11.2 Python喜欢序列

image.png
图 11-1展示了定义为抽象基类的Sequence正式接口

示例11-3 定义__getitme__方法,只实现了序列协议的一部分,这样足够访问元素、迭代和使用in运算符了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
python复制代码>>> class Foo:

...     def __getitem__(self, pos):

...         return range(0, 30, 10)[pos]

... 

>>> f = Foo()

>>> f[1]

10

>>> for i in f:print(i)

... 

0

10

20

>>> 20 in f

True

>>> 15 in f

False

鉴于序列协议的重要性,如果没有__iter__和__contains__方法,Python会调用__getitem__方法,设法让迭代和in运算符可用。

11.3 使用猴子补丁在运行时实现协议

标注库中的random.shuffle函数用法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
python复制代码>>> from random import shuffle

>>> l = list(range(10))

>>> l

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

>>> shuffle(l)

>>> l

[4, 6, 8, 0, 3, 5, 1, 2, 7, 9]

示例11-5 random.shuffle函数不能打乱FrenchDeck实例

1
2
3
4
5
6
7
8
9
10
arduino复制代码from frenchdeck import FrenchDeck
from random import shuffle
deck = FrenchDeck()
shuffle(deck)
Traceback (most recent call last):
File "<input>", line 1, in <module>
File
...
x[i], x[j] = x[j], x[i]
TypeError: 'FrenchDeck' object does not support item assignment

这个报错的原因是,shuffle函数要调换collection中元素的位置,而FrenchDeck只实现了不可变序列的协议。可变的序列还必须提供__setitem__方法。

示例11-6 为FrenchDeck打猴子补丁,把它变成可变的

1
2
3
4
5
6
7
scss复制代码def set_card(deck, position, card):
deck._cards[position] = card

FrenchDeck.__setitem__ = set_card
shuffle(deck)
deck[:5]
[Card(rank='10', suit='diamonds'), Card(rank='8', suit='clubs'), Card(rank='3', suit='spades'), Card(rank='Q', suit='diamonds'), Card(rank='6', suit='diamonds')]

猴子补丁:在运行时修改类或模块,而不改动源码。

11.4 Alex Martelli的水禽

11.5 定义抽象基类的子类

示例11-8 FrenchDeck2,collections.MutableSequence的子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
python复制代码import collections

Card = collections.namedtuple('Card', ['rank', 'suit'])


class FrenchDeck2(collections.MutableSequence):
ranks = [str(n) for n in (2, 11)] + list('JQKA')
suits = 'spades diamonds clubs hearts'.split()

def __init__(self):
self._cards = [Card(rank, suit) for suit in self.suits
for rank in self.ranks]

def __len__(self):
return len(self._cards)

def __getitem__(self, position):
return self._cards[position]

def __setitem__(self, position, value):
self._cards[position] = value

def __delitem__(self, position):
del self._cards[position]

def insert(self, index: int, value) -> None:
self._cards[index] = value

insert这个抽象方法是必须实现的,导入时,Python不会检查抽象方法的实现,在运行时实例化FrenchDeck2类时才会真正检查。

1
2
3
4
5
scala复制代码from frenchdeck2 import FrenchDeck2
deck = FrenchDeck2()
Traceback (most recent call last):
...
TypeError: Can't instantiate abstract class FrenchDeck2 with abstract methods insert

image.png
图11-2 MutableSequence抽象基类

11.6 标准库中的抽象基类

11.6.1 collections.abc模块中的抽象基类

image.png

11.6.2 抽象基类的数字塔

numbers包定义的是“数字塔”(即各个抽象基类的层次结构是线性的),其中Number是位于最顶端的超类,随后是Complex子类,依次往下,最底端是Integral类:

  • Number
  • Complex
  • Real
  • Rational
  • Integral

如果想检查一个数是不是整数,可以使用isinstance(x, numbers.Ingegral),这样的代码就能接受int、bool。

如果一个值可能是浮点数类型,可以使用isinstance(x, numbers.Real)检查。这样的代码能接受bool、int、float、fractions.Fraction。

11.7 定义并使用一个抽象基类

image.png

示例11-9 tombola.py: Tombola是抽象基类,有两个抽象方法和两个具体方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
python复制代码import abc

class Tombola(abc.ABC):

@abc.abstractmethod
def load(self, iterable):
"""Add items from an iterable."""

@abc.abstractmethod
def pick(self):
"""Remove item at random, returning it.
This method should raise `LookupError` when the instance is empty.
"""

def loaded(self):
"""Return `True` if there's at least 1 item, `False` otherwise."""
return bool(self.inspect)


def inspect(self):
"""Return a sorted tuple with the items currently inside."""
items = []
while True:
try:
items.append(self.pick())
except LookupError:
break
self.load(items)
return tuple(sorted(items))

示例11-11 不符合Tombola要求的子类无法蒙混过关

1
2
3
4
5
6
7
8
9
10
11
scala复制代码from tombola import Tombola
class Fake(Tombola):
def pick(self):
return 13

Fake
<class '__main__.Fake'>
f = Fake()
Traceback (most recent call last):
...
TypeError: Can't instantiate abstract class Fake with abstract methods load

尝试实例化Fake时抛出了TypeError。Python热舞Fake是抽象类,因为它没有实现load方法。

11.7.1 抽象基类语法详解

11.7.2 定义Tombola抽象基类的子类

示例11-12 bingo.py:BingoCage是Tombola的具体子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
python复制代码import random

from tombola import Tombola


class BingoCage(Tombola):

def __init__(self, items):
self._randomizer = random.SystemRandom()
self._items = []
self.load(items)

def load(self, items):
self._items.extend(items)
self._randomizer.shuffle(self._items)

def pick(self):
try:
return self._items.pop()
except IndexError:
raise LookupError('pick from empty BingoCage')

def __call__(self):
self.pick()

示例11-13 lotto.py:LotteryBlower是Tombola的具体子类,覆盖了继承的inspect和loaded方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
python复制代码import random

from tombola import Tombola


class LotteryBlower(Tombola):

def __init__(self, iterable):
self._balls = list(iterable)

def load(self, iterable):
self._balls.extend(iterable)

def pick(self):
try:
position = random.randrange(len(self._balls))
except ValueError:
raise LookupError('pick from empty LotteryBlower')
return self._balls.pop(position)

def loaded(self):
return bool(self._balls)

def inspect(self):
return tuple(sorted(self._balls))

11.7.3 Tombola的虚拟子类

即便不继承,也有办法把一个类注册为抽象基类的虚拟子类。这样做时,我保证注册的类忠实地实现了抽象基类定义的接口,而Python会相信我们,从而不做检查。如果我们说谎了,那么常规的运行时异常会被我们捕获。

注册虚拟子类的方式是在抽象基类上调用register方法。这样做之后,注册的类会变成抽象基类的虚拟子类,而且issubclass和isinstance等函数都能识别,但是注册的类不会从抽象基类中继承任何方法和属性。

image.png

示例11-14 tombolist.py:TomboList是Tombola的虚拟子类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码from random import randrange
from tombola import Tombola


@Tombola.register
class TomboList(list):

def pick(self):
if self: # 从list中继承\_\_bool__方法,列表不为空时返回True
position = randrange(len(self))
return self.pop(position)
else:
raise LookupError('pop from empty TomboList')

load = list.extend

def loaded(self):
return bool(self)

def inspect(self):
return tuple(sorted(self))
1
2
3
4
5
6
7
8
python复制代码
>>> from tombola import Tombola
>>> from tombolist import TomboList
>>> issubclass(TomboList, Tombola)
True
>>> t = TomboList(range(100))
>>> isinstance(t, Tombola)
True

类的继承关系在一个特殊的类属性中指定————__mro__,即方法解析顺序(Method Resolution Order).这个属性的作用很简单,按顺序列出类及超类,Python会按照这个顺序搜索方法。查看TomboList类的__mro__属性,会发现它只列出了“真实的”超类。

1
2
python复制代码>>> TomboList.__mro__
(<class 'tombolist.TomboList'>, <class 'list'>, <class 'object'>)

TomboList.__mro中没有Tombola,因此TomboList没有从Tombola中继承任何方法。

11.8 Tombola子类的测试方法

__subclasses__()

这个方法返回类的直接子类列表,不包含虚拟子类。

__abc_register

只有抽象基类有这个数据属性,其值是一个WeakSet对象,即抽象类注册的虚拟子类的弱引用。

11.9 Python使用register的方式

虽然现在可以把register当作装饰器使用了,但更常见的做法还是把它当作函数使用,用于注册其他地方定义的类。

把内置类型tuple、str、range和memoryview注册为Sequence的虚拟子类:

1
2
3
4
python复制代码Sequence.register(tuple)
Sequence.register(str)
Sequence.register(range)
Sequence.register(memoryview)

11.10 鹅的行为有可能像鸭子

即便不注册,抽象基类也能把一个类识别为虚拟子类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码>>> class Struggle:

...     def __len__(self): return 23

... 

>>> from collections import abc

>>> isinstance(Struggle(), abc.Sized)

True

>>> issubclass(Struggle, abc.Sized)

True

Struggle是abc.Sized的子类,这是因为abc.Sized实现了一个特殊的类方法,名为__subclasshook__.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpringCloud升级之路20200x版-43为何

发表于 2021-11-28

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

本系列代码地址:github.com/JoJoTec/spr…

在开始编写我们自己的日志 Filter 之前,还有一个问题我想在这里和大家分享,即在 Spring Cloud Gateway 中可能发生链路信息丢失的问题。

主要冲突 - Project Reactor 与 Java Logger MDC 之间的设计冲突

Poject Reactor 是基于异步响应式设计的编程模式的实现,它的主要实现思路是先编写执行链路,最后 sub 执行整个链路。但是链路的每一部分,究竟是哪个线程执行的,是不确定的。

Java 的日志框架设计,其上下文 MDC(Mapped Diagnostic Context)信息,是基于线程设计的,其实可以简单理解为一个 ThreadLocal 的 Map。日志的链路信息,是保存在这个 MDC 中的。

这样其实可以看出 Project Reactor 与日志框架的 MDC 默认是不兼容的,只要发生异步线程切换,这个 MDC 就变了。Spring Cloud Sleuth 为此加了很多粘合代码,但是智者千虑必有一失,Project Reactor 应用场景和库也在不断发展和壮大,Spring Cloud Sleuth 也可能会漏掉一些场景导致链路信息丢失。

一种 Spring Cloud Gateway 常见的链路信息丢失的场景

我们编写一个简单的测试项目(项目地址):

引入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
xml复制代码<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.6</version>
</parent>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-sleuth</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!--log4j2异步日志需要的依赖,所有项目都必须用log4j2和异步日志配置-->
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>${disruptor.version}</version>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.3</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

对所有路径开启 AdaptCachedBodyGlobalFilter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
typescript复制代码@Configuration(proxyBeanMethods = false)
public class ApiGatewayConfiguration {
@Autowired
private AdaptCachedBodyGlobalFilter adaptCachedBodyGlobalFilter;
@Autowired
private GatewayProperties gatewayProperties;

@PostConstruct
public void init() {
gatewayProperties.getRoutes().forEach(routeDefinition -> {
//对 spring cloud gateway 路由配置中的每个路由都启用 AdaptCachedBodyGlobalFilter
EnableBodyCachingEvent enableBodyCachingEvent = new EnableBodyCachingEvent(new Object(), routeDefinition.getId());
adaptCachedBodyGlobalFilter.onApplicationEvent(enableBodyCachingEvent);
});
}
}

配置(我们只有一个路由,将请求转发到 httpbin.org 这个 http 请求测试网站):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码server:
port: 8181
spring:
application:
name: apiGateway
cloud:
gateway:
httpclient:
connect-timeout: 500
response-timeout: 60000
routes:
- id: first_route
uri: http://httpbin.org
predicates:
- Path=/httpbin/**
filters:
- StripPrefix=1

添加两个全局 Filter,一个在 AdaptCachedBodyGlobalFilter 之前,一个在 AdaptCachedBodyGlobalFilter 之后。这两个 Filter 非常简单,只是打一行日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码@Log4j2
@Component
public class PreLogFilter implements GlobalFilter, Ordered {

public static final int ORDER = new AdaptCachedBodyGlobalFilter().getOrder() - 1;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("before AdaptCachedBodyGlobalFilter");
return chain.filter(exchange);
}

@Override
public int getOrder() {
return ORDER;
}
}

@Log4j2
@Component
public class PostLogFilter implements GlobalFilter, Ordered {

public static final int ORDER = new AdaptCachedBodyGlobalFilter().getOrder() + 1;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("after AdaptCachedBodyGlobalFilter");
return chain.filter(exchange);
}

@Override
public int getOrder() {
return ORDER;
}
}

最后指定 Log4j2 的输出格式中包含链路信息,就像系列文章开头中指定的那样。

启动这个应用,之后访问 http://127.0.0.1:8181/httpbin/anything,查看日志,发现 PostLogFilter 中的日志,没有链路信息了:

1
2
ini复制代码2021-09-08 06:32:35.457  INFO [service-apiGateway,51063d6f1fe264d0,51063d6f1fe264d0] [30600] [reactor-http-nio-2][?:]: before AdaptCachedBodyGlobalFilter
2021-09-08 06:32:35.474 INFO [service-apiGateway,,] [30600] [reactor-http-nio-2][?:]: after AdaptCachedBodyGlobalFilter

Spring Cloud Sleuth 是如何增加链路信息

通过系列之前的源码分析,我们知道,在最开始的 TraceWebFilter,我们将 Mono 封装成了一个 MonoWebFilterTrace,它的核心源码是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Override
public void subscribe(CoreSubscriber<? super Void> subscriber) {
Context context = contextWithoutInitialSpan(subscriber.currentContext());
Span span = findOrCreateSpan(context);
//将 Span 放入执行上下文中,对于日志其实就是将链路信息放入 org.slf4j.MDC
//日志的 MDC 一般都是 ThreadLocal 的 Map,对于 Log4j2 的实现类就是 org.apache.logging.log4j.ThreadContext,其核心 contextMap 就是一个基于 ThreadLocal 实现的 Map
//简单理解就是将链路信息放入一个 ThreadLocal 的 Map 中,每个线程访问自己的 Map 获取链路信息
try (CurrentTraceContext.Scope scope = this.currentTraceContext.maybeScope(span.context())) {
//将实际的 subscribe 用 Span 所在的 Context 包裹住,结束时关闭 Span
this.source.subscribe(new WebFilterTraceSubscriber(subscriber, context, span, this));
}
//在 scope.close() 之后,会将链路信息从 ThreadLocal 的 Map 中剔除
}

@Override
public Object scanUnsafe(Attr key) {
if (key == Attr.RUN_STYLE) {
//执行的方式必须是不能切换线程,也就是同步的
//因为,日志的链路信息是放在 ThreadLocal 对象中,切换线程,链路信息就没了
return Attr.RunStyle.SYNC;
}
return super.scanUnsafe(key);
}

WebFilterTraceSubscriber 干了些什么呢?出现异常,以及 http 请求结束的时候,我们可能想将响应信息,异常信息记录进入 Span 中,就是通过这个类封装实现的。

经过 MonoWebFilterTrace 的封装,由于 Spring-WebFlux 处理请求,其实就是封装成我们上面得出的 Mono 之后进行 subscribe 处理的请求,所以这样,整个内部 Mono 的 publish 链路以及 subscribe 链路,就被 WebFilterTraceSubscriber 中的 scope 包裹起来了。只要我们自己不在 GatewayFilter 中转换成某些强制异步的 Mono 或者 Flux 导致切换线程,链路信息是不会丢失的。

为何上面的测试项目中链路信息会丢失

我们来看经过 AdaptCachedBodyGlobalFilter 之后,我们前面拼的 Mono 链路会变成什么样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
less复制代码return Mono.defer(() ->
new MonoWebFilterTrace(source,
RoutePredicateHandlerMapping.this.lookupRoute(exchange) //根据请求寻找路由
.flatMap((Function<Route, Mono<?>>) r -> {
exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); //将路由放入 Attributes 中,后面我们还会用到
return Mono.just(RoutePredicateHandlerMapping.this.webHandler); //返回 RoutePredicateHandlerMapping 的 FilteringWebHandler
}).switchIfEmpty( //如果为 Mono.empty(),也就是没找到路由
Mono.empty()
.then(Mono.fromRunnable(() -> { //返回 Mono.empty() 之后,记录日志
if (logger.isTraceEnabled()) {
logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");
}
})))
.switchIfEmpty(DispatcherHandler.this.createNotFoundError()) //如果没有返回不为 Mono.empty() 的 handlerMapping,则直接返回 404
.then(
Mono.defer(() -> {
//省略在 AdaptCachedBodyGlobalFilter 前面的链路嵌套
//读取 Body,由于 TCP 拆包,所以需要他们拼接到一起
DataBufferUtils.join(exchange.getRequest().getBody())
//如果没有 Body,则直接返回空 DataBuffer
.defaultIfEmpty(factory.wrap(new EmptyByteBuf(factory.getByteBufAllocator())))
//decorate方法中将 dataBuffer 放入 exchange 的 Attributes 列表,只是为了防止重复进入这个 `AdaptCachedBodyGlobalFilter` 的情况导致重复缓存请求 Body
//之后,使用新的 body 以及原始请求封装成新的请求,继续 GatewayFilters 链路
.map(dataBuffer -> decorate(exchange, dataBuffer, cacheDecoratedRequest))
.switchIfEmpty(Mono.just(exchange.getRequest())).flatMap(function);
})
.then(Mono.empty()))
), //调用对应的 Handler
TraceWebFilter.this.isTracePresent(), TraceWebFilter.this, TraceWebFilter.this.spanFromContextRetriever()).transformDeferred((call) -> {
//MetricsWebFilter 相关的处理,在前面的代码中给出了,这里省略
});
);

其中 DataBufferUtils.join(exchange.getRequest().getBody()) 其实是一个 FluxReceive,这里我们可以理解为:提交一个尝试读取请求 Body 的任务,将之后的 GatewayFilter 的链路处理加到在读取完 Body 之后的回调当中,提交这个任务后,立刻返回。这么看可能比较复杂,我们用一个类似的例子类比下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
scala复制代码//首先我们创建一个新的 Span
Span span = tracer.newTrace();
//声明一个类似于 TraceWebFilter 中封装的 MonoWebFilterTrace 的 MonoOperator
class MonoWebFilterTrace<T> extends MonoOperator<T, T> {
protected MonoWebFilterTrace(Mono<? extends T> source) {
super(source);
}

@Override
public void subscribe(CoreSubscriber<? super T> actual) {
//将 subscribe 用 span 包裹
try (Tracer.SpanInScope spanInScope = tracer.withSpanInScope(span)) {
source.subscribe(actual);
//在将要关闭 spanInScope 的时候(即从 ThreadLocal 的 Map 中移除链路信息),打印日志
log.info("stopped");
}
}
}

Mono.defer(() -> new MonoWebFilterTrace(
Mono.fromRunnable(() -> {
log.info("first");
})
//模拟 FluxReceive
.then(Mono.delay(Duration.ofSeconds(1))
.doOnSuccess(longSignal -> log.info(longSignal))))
).subscribe(aLong -> log.info(aLong));

Mono.delay 和 FluxReceive 表现类似,都是异步切换线程池执行。执行上面的代码,我们可以从日志上面就能看出来:

1
2
3
4
5
ini复制代码2021-09-08 07:12:45.236  INFO [service-apiGateway,7b2f5c190e1406cb,7b2f5c190e1406cb] [31868] [reactor-http-nio-2][?:]: first
2021-09-08 07:12:45.240 INFO [service-apiGateway,7b2f5c190e1406cb,7b2f5c190e1406cb] [31868] [reactor-http-nio-2][?:]: stopped
2021-09-08 07:12:46.241 INFO [service-apiGateway,,] [31868] [parallel-1][?:]: doOnEach_onNext(0)
2021-09-08 07:12:46.242 INFO [service-apiGateway,,] [31868] [parallel-1][?:]: onComplete()
2021-09-08 07:12:46.242 INFO [service-apiGateway,,] [31868] [parallel-1][?:]: 0

在 Spring Cloud Gateway 中,Request Body 的 FluxReceive 使用的线程池和调用 GatewayFilter 的是同一个线程池,所以可能线程还是同一个,但是由于 Span 已经结束,从 ThreadLocal 的 Map 中已经移除了链路信息,所以日志中还是没有链路信息。

微信搜索“我的编程喵”关注公众号,每日一刷,轻松提升技术,斩获各种offer:

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文带你掌握Mybatis设计模式之Builder Myba

发表于 2021-11-28

这是我参与11月更文挑战的第26天,活动详情查看:2021最后一次更文挑战

Mybatis设计模式

前言

虽然设计模式有3类23种设计模式,但是⼤多停留在概念层⾯,Mybatis源码中使⽤了⼤量的设计模式,观察设计模式在其中的应⽤,能够更深⼊的理解设计模式。

Mybatis⾄少⽤到了以下的设计模式的使⽤:

模式 mybatis体现
Builder模式 例如SqlSessionFactoryBuilder、Environment;
⼯⼚⽅法模式 例如SqlSessionFactory、TransactionFactory、LogFactory
单例模式 例如 ErrorContext 和 LogFactory;
代理模式 Mybatis实现的核⼼,⽐如MapperProxy、ConnectionLogger,⽤的jdk的动态代理还有executor.loader包使⽤了 cglib或者javassist达到延迟加载的效果
组合模式 例如SqlNode和各个⼦类ChooseSqlNode等;
模板⽅法模式 例如 BaseExecutor 和 SimpleExecutor,还有 BaseTypeHandler 和所有的⼦类例如 IntegerTypeHandler;
适配器模式 例如Log的Mybatis接⼝和它对jdbc、log4j等各种⽇志框架的适配实现;
装饰者模式 例如Cache包中的cache.decorators⼦包中等各个装饰者的实现;
迭代器模式 例如迭代器模式PropertyTokenizer;

1 Builder构建者模式

Builder模式的定义是”将⼀个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。”,它属于创建类模式,⼀般来说,如果⼀个对象的构建⽐较复杂,超出了构造函数所能包含的范围,就可以使⽤⼯⼚模式和Builder模式,相对于⼯⼚模式会产出⼀个完整的产品,Builder应⽤于更加 复杂的对象的构建,甚⾄只会构建产品的⼀个部分,直⽩来说,就是使⽤多个简单的对象⼀步⼀步构建成⼀个复杂的对象

例⼦:使⽤构建者设计模式来⽣产computer 主要步骤:

  • 1、将需要构建的⽬标类分成多个部件(电脑可以分为主机、显示器、键盘、⾳箱等部件);
  • 2、 创建构建类;
  • 3、 依次创建部件;
  • 4、 将部件组装成⽬标对象

1. 定义computer

image.png

2. ComputerBuilder

image.png

调⽤

image.png

2. Mybatis中的体现

SqlSessionFactory 的构建过程:

Mybatis的初始化⼯作⾮常复杂,不是只⽤⼀个构造函数就能搞定的。所以使⽤了建造者模式,使⽤了⼤量的Builder,进⾏分层构造,核⼼对象Configuration使⽤了 XmlConfigBuilder来进⾏构造。

image.png

在Mybatis环境的初始化过程中,SqlSessionFactoryBuilder会调⽤XMLConfigBuilder读取所有的MybatisMapConfig.xml 和所有的 *Mapper.xml ⽂件,构建 Mybatis 运⾏的核⼼对象Configuration对象,然后将该Configuration对象作为参数构建⼀个SqlSessionFactory对象。

image.png

其中 XMLConfigBuilder 在构建 Configuration 对象时,也会调⽤ XMLMapperBuilder ⽤于读取Mapper⽂件,⽽XMLMapperBuilder会使⽤XMLStatementBuilder来读取和build所有的SQL语句。

1
2
java复制代码//解析<mappers />标签
mapperElement(root.evalNode("mappers"));

在这个过程中,有⼀个相似的特点,就是这些Builder会读取⽂件或者配置,然后做⼤量的XpathParser 解析、配置或语法的解析、反射⽣成对象、存⼊结果缓存等步骤,这么多的⼯作都不是⼀个构造函数所能包括的,因此⼤量采⽤了 Builder模式来解决

image.png

SqlSessionFactoryBuilder类根据不同的输⼊参数来构建SqlSessionFactory这个⼯⼚对象。因此Mybatis使用builder建造者模式就差不多啦,后续还会更新其他设计模式在mybatis的使用

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…143144145…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%