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

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


  • 首页

  • 归档

  • 搜索

stream流中map和foreach的使用与区别

发表于 2021-11-25

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

前言

在jdk1.8的时候主要引进了Lambda表达式、Date Time API(针对于时间计算,很方便很香)、Optional类(可以很好避免空指针异常)和Stream API

Stream API可以简化很多事情,比如使用filter配合Lambda表达式进行筛选满足的数据,再比如可以快速的使数组内数据进行排序,省略了去很多遍历数组然后各种判断操作。

但是对于不熟悉Stream API特性的同学就很不友好,第一眼看到会比较懵,所以使用Stream API需要一定的门槛,但对于有工作经验的同学熟悉下就可以了上手很快,非常推荐使用真的香。

foreach使用

通过名字就会有疑问这不就是平常用的增强for循环吗?

是的没错,完全可以把这个当作增强for循环使用,但是使用的方式又有一定的区别。

image.png

首先它是需要传入一个Consumer并且没有返回值,但是这个Consumer是个啥,点进去看一下。

image.png

看到了 @FunctionalInterface 那么我们是不是就可以使用函数式编程,直接在foreach中写Lambda表达式,尝试一下果然可以。

1
2
3
4
5
6
7
8
9
10
11
js复制代码public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("测试1");
list.add("测试2");
list.add("测试3");
list.add("测试4");
list.add("测试5");
list.stream().forEach(li ->{
System.out.println(li);
});
}

foreach和之前用的增强for很相似,对集合进行遍历还可以改变遍历时的数据并作用在集合上。

map的使用

在没有接触Stream API之前听到这个map第一想到的就是hashMap,但是此map非彼map。

照样我们看下方法的源码是什么样子的,需要传入一个Function返回Stream。

image.png

image.png

又看到了 @FunctionalInterface 那么就可以理解为我们传入一个方法,然后返回一个新的流给我们。验证一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
js复制代码public static void main(String[] args) {
List<String> list = new ArrayList<>();
list.add("测试1");
list.add("测试2");
list.add("测试3");
list.add("测试4");
list.add("测试5");
List<String> collect = list.stream()
.map(a -> a = a + "测试")
.collect(Collectors.toList());
collect.stream().forEach(li ->{
System.out.println(li);
});
list.stream().forEach(li ->{
System.out.println(li);
});
}

image.png

map不会改变当前数组,但是如果用了map需要得到一个新的数组就要使用 .collect(Collectors.toList())

使用场景与区别

使用场景:

如果我们需要改变数组中的元素时使用foreach

如果只是需要数组中的某些元素时使用map
区别:
foreach没有返回值,并且会对数组中所有的元素进行处理,同时还会改变数组中元素

map 可以返回一个全新的Stream对象,也可以使用 .collect(Collectors.toList()) 返回一个数组,同时不会对当前数组进行任何改变。

本文转载自: 掘金

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

数据结构—二叉树(BinaryTree)的入门原理以及Jav

发表于 2021-11-25

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

本文详细介绍了二叉树的基本概念,以及各种二叉树,以及二叉树的Java实现方式,包括顺序结果和链式结构的实现。

二叉树是一种特殊的树,其定义为:二叉树是n(n>=0)个节点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树组成。

1 二叉树的定义

二叉树是一种特殊的树,其定义为:二叉树是n(n>=0)个节点的有限集合,该集合或者为空集(称为空二叉树),或者由一个根节点和两棵互不相交的、分别称为根节点的左子树和右子树组成。 如果不是太清楚树的的概念的,可以看这篇文章:数据结构—树(Tree)的入门原理以及Java实现案例。

如下图,就是一颗二叉树:
在这里插入图片描述

2 二叉树的特性

三个特性:

  1. 每个节点最多有两棵子树,所以二叉树中不存在度大于2的节点。注意不是只有两棵子树,而是最多有。没有子树或者有一棵子树都是可以的。
  2. 左子树和右子树是有顺序的,次序不能任意颠倒。
  3. 即使树中某节点只有一棵子树,也要区分它是左子树还是右子树。

如下案例:一个3个节点树,对于普通的树和二叉树,分别有几种形态?
在这里插入图片描述
普通树,首先有树1形态,而后续四种情况对于普通树是没有区分的,因此只有两种情况;而对于二叉树,则以五种情况都有。

3 特殊的二叉树

3.1 斜二叉树

所有的节点都只有左子树的二叉树叫左斜树。所有节点都是只有右子树的二叉树叫右斜树。这两者统称为斜树。

左斜树:

在这里插入图片描述

右斜树:

在这里插入图片描述

3.2 满二叉树

在一棵二叉树中,如果所有分支节点都存在左子树和右子树,并且所有叶子都在同一层上,这样的二叉树称为满二叉树,又称完美二叉树。
在这里插入图片描述

满二叉树的特点:

  1. 叶子只能出现在最下一层。出现在其他层就不可能达成平衡。
  2. 非叶子节点的度一定是2。否则就是“缺胳膊少腿”了。
  3. 在同样深度的二叉树中,满二叉树的节点个数最多,叶子数最多。

3.3 完全二叉树

完全二叉树:除去最后一层叶子节点,就是一颗满二叉树,并且最后一层的节点只能集中在左侧,满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
在这里插入图片描述
完全二叉树的特点:

  1. 叶子节点只能出现在最下两层。
  2. 最下层的叶子一定集中在左部连续位置。
  3. 倒数二层,若有叶子节点,一定都集中在右部连续位置。
  4. 如果节点度为1,则该节点只有左孩子,即不存在只有右子树的情况。
  5. 同样节点数的二叉树,完全二叉树的深度最小。

3.4 平衡二叉树

平衡二叉树又被称为AVL树(区别于AVL算法),它是具有如下性质:

  1. 它一定是一棵二叉排序树;
  2. 它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。

平衡二叉树作为重点和难点,此处不多赘述,后面的文章会单独讲。

4 二叉树的性质

共有大概六条性质,这些性质可以被直接用来实现二叉树:

  1. 在二叉树的第i层上至多有2^(i-1)个节点(i≥1);
  2. 深度为k的二叉树至多有2^k-1个节点(k≥1),最少有k个节点;
  3. 对于任意一棵二叉树,如果其叶子节点数为N0,且度数为2的节点总数为N2,则N0=N2+1;
  4. 具有n个节点的完全二叉树的深度为|log2n+1|(|x|表示不大于x的最大整数)。
  5. 有N个节点的完全二叉树各节点如果用顺序方式存储,若I为节点编号(从1开始),则节点之间有如下关系:
1. 如果 I = 1,则节点I是二叉树的根;I > 1,则其父节点的编号为 I/2,左子节点编号为 2 \* I (如果存在),右子节点编号为 2 \* I + 1(如果存在);
2. 如果 2 \* I <= N,则其左孩子(即左子树的根节点)的编号为 2 \* I ;若 2 \* I > N,则无左右孩子;
3. 如果 2 \* I + 1 <= N,则其右孩子的节点编号为 2 \* I + 1;若 2 \* I + 1 > N,则无右孩子。
  1. 有N个节点的完全二叉树各节点如果用顺序方式存储,若I为节点编号(从0开始),则节点之间有如下关系:
1. 如果 I = 0,则节点 I 是二叉树的根;I > 0,则其父节点的编号为 (I-1)/2,左子节点编号为 2 \* I + 1(如果存在),右子节点编号为 2 \* I + 2(如果存在);
2. 如果 2 \* I + 1 <= N,则其左孩子(即左子树的根节点)的编号为 2 \* I +1 ;若 2 \* I + 1 > N,则无左右孩子;
3. 如果 2 \* I + 2 <= N,则其右孩子的节点编号为 2 \* I + 2;若 2 \* I + 2 > N,则无右孩子。

在这里插入图片描述

上图是已经编号了的完全二叉树,具有N=10个节点,设I从1开始,下面来验证各个特性:

  1. I = 1 的节点,确实是根节点;如果 I = 5 > 0,那么父节点编号为5/2,即2。
  2. 如果I=5,2 * 5 = 10,5编号的节点的左孩子为2 * 5 = 10编号;如果I=6,2 * 6 > 10,6编号的节点的无左孩子。
  3. 如果 I = 4,2 * 4 + 1 < 10,4编号的节点的右孩子为2 * 4 + 1 = 9 编号;如果I=5,2 * 5 + 1 > 10,5编号的节点的无右孩子。

最后一个特性:给定n个节点,能构成f(n)种不同的二叉树。f(n)为卡特兰数的第N项:

在这里插入图片描述

5 二叉树的存储结构

5.1 顺序存储结构

5.1.1 顺序存储结构的概述

顺序存储结构对树这种一对多的关系结构实现起来是比较困难的。但是二叉树是一种特殊的树,由于它的特殊性,使得用顺序存储结构也可以实现。

二叉树的顺序存储结构就是用一维数组存储二叉树中的节点,并且节点的存储位置,也就是数组的下标要能体现节点之间的逻辑关系,比如双亲与孩子的关系,左右兄弟的关系等。

此时,完全二叉树的规律性和优越性就显现了出来:

在这里插入图片描述

由于完全二叉树的特性,可以将上图的完全二叉树从上到下,从左到右的遍历,然后顺序存放进数组对应索引的位置中:

在这里插入图片描述

对于一般的二叉树,则可以 “借用” 完全二叉树的的思路,将空出来的节点位置置空:

在这里插入图片描述

如上图的普通二叉树,存储时,将其“转换”为完全二叉树,不存在的节点使用null填充:

在这里插入图片描述

极端情况下,一棵深度为k的右斜树,它只有k个结点,却需要分配2^k-1个存储单元空间,这明显会浪费很多空间。

在这里插入图片描述

因此,顺序存储结构只适用于完全二叉树或者满二叉树。

5.1.2 顺序存储结构的简单实现

提供一个二叉树的顺序存储结构的简单实现,节点不允许为null。

可以看到,子节点和父节点的添加、获取,都是依靠的二叉树性质的第五条的公式,这里把要被实现的二叉树看成了完全二叉树,还是比较简单的,但是可能会浪费内存空间。

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
java复制代码/**
* 二叉树的顺序存储结构的简单实现
*/
public class ArrayBinaryTree<E> {

/**
* 深度
*/
private int deep;
/**
* 容量,也是节点数量
*/
private int capacity;
/**
* 底层数组
*/
private Object[] elements;

/**
* 节点真正数量
*/
private int size;


/**
* 指定树的深度,初始化数组
*
* @param deep 树深度
*/
public ArrayBinaryTree(int deep) {
this.deep = deep;
this.elements = new Object[capacity = (int) Math.pow(2, deep) - 1];
}

/**
* 指定树的深度和根节点
*
* @param deep
* @param root
*/
public ArrayBinaryTree(int deep, E root) {
this(deep);
addRoot(root);
}


/**
* 添加根节点
*
* @param root 根节点数据
*/
public void addRoot(E root) {
checkNullData(root);
elements[0] = root;
size++;
}


/**
*
* 添加子节点
*
* @param parentIndex 父节点索引
* @param data 节点数据
* @param left 是否是左子节点,true 是;false 否
* @return 添加成功后子节点的索引
*/
public int addChild(int parentIndex, E data, boolean left) {
checkParentIndex(parentIndex);
checkNullData(data);
int childIndex;
if (left) {
childIndex = parentIndex * 2 + 1;
} else {
childIndex = parentIndex * 2 + 2;
}
addChild(childIndex, data);
size++;
return childIndex;
}

/**
* 添加子节点
*
* @param childIndex 子节点索引
* @param data 子节点数据
*/
private void addChild(int childIndex, E data) {
if (elements[childIndex] != null) {
throw new IllegalStateException("该父节点已经存在该子节点");
}
elements[childIndex] = data;
}


/**
* 是否是空树
*
* @return true 是 ;false 否
*/
public boolean isEmpty() {
return elements[0] == null;
}


/**
* 返回节点数
*
* @return 节点数
*/
public int size() {
return size;
}


/**
* 获取索引为index的节点的父节点
*
* @param index 索引
* @return 父节点数据
*/
public E getParent(int index) {
if (index == 0) {
return null;
}
return (E) elements[(index - 1) / 2];
}

/**
* 获取索引为index的节点的右子节点
*
* @param index 索引
* @return 右子节点数据
*/
public E getRight(int index) {
if (2 * index + 1 >= capacity) {
return null;
}
return (E) elements[index * 2 + 2];
}

/**
* 获取索引为index的节点的左子节点
*
* @param index 索引
* @return 左子节点
*/
public E getLeft(int index) {
if (2 * index + 1 >= capacity) {
return null;
}
return (E) elements[2 * index + 1];
}


/**
* 获取根节点
*
* @return 根节点数据
*/
public E getRoot() {
return (E) elements[0];
}

/**
* 获取节点出现的首个索引位置
*
* @param data 节点数据
* @return 节点索引, 或者-1--不存在该节点
*/
public int indexOf(E data) {
for (int i = 0; i < capacity; i++) {
if (elements[i].equals(data)) {
return i;
}
}
return -1;
}

/**
* 检查子节点是否已经存在
*
* @param message 消息
*/
private void checkChild(int childIndex, String message) {
if (elements[childIndex] == null) {
throw new IllegalStateException(message);
}
}


/**
* 数据判null
*
* @param data 添加的数据
*/
private void checkNullData(E data) {
if (data == null) {
throw new NullPointerException("数据不允许为null");
}
}

/**
* 检查父节点是否存在
*
* @param parentIndex 父节点索引
*/
private void checkParentIndex(int parentIndex) {
if (elements[parentIndex] == null) {
throw new NoSuchElementException("父节点不存在");
}
}
}

5.2 链式存储结构

采用链式存储结构更加的灵活,为树节点设计一个数据域和两个引用变量,一个保存左子结点的引用,另一个保存右子节点的引用,我们称这样的链表叫做二叉链表。如果有需要还可在加在一个保存父节点引用变量。

5.2.1 链式存储结构的简单实现

下面是一个没有保存父节点引用的二叉树链式存储结构的简单实现,查找其父节点是比较困难的,需要遍历整个树,因此建议加上父节点的引用。

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
java复制代码/**
* 二叉树的链式存储结构的简单实现
*/
public class LinkedBinaryTree<E> {

/**
* 外部保存根节点的引用
*/
private BinaryTreeNode<E> root;

/**
* 树节点的数量
*/
private int size;

/**
* 内部节点对象
*
* @param <E> 数据类型
*/
public static class BinaryTreeNode<E> {

//数据域
E data;
//左子节点
BinaryTreeNode<E> left;
//右子节点
BinaryTreeNode<E> right;

public BinaryTreeNode(E data) {
this.data = data;
}

public BinaryTreeNode(E data, BinaryTreeNode<E> left, BinaryTreeNode<E> right) {
this.data = data;
this.left = left;
this.right = right;
}

@Override
public String toString() {
return data.toString();
}
}

/**
* 空构造器
*/
public LinkedBinaryTree() {
}

/**
* 构造器,初始化root节点
*
* @param root 根节点数据
*/
public LinkedBinaryTree(E root) {
checkNullData(root);
this.root = new BinaryTreeNode<>(root);
size++;
}

/**
* 添加子节点
*
* @param parent 父节点的引用
* @param data 节点数据
* @param left 是否是左子节点,true 是;false 否
*/
public BinaryTreeNode<E> addChild(BinaryTreeNode<E> parent, E data, boolean left) {
checkNullParent(parent);
checkNullData(data);
BinaryTreeNode<E> node = new BinaryTreeNode<>(data);
if (left) {
if (parent.left != null) {
throw new IllegalStateException("该父节点已经存在左子节点,添加失败");
}
parent.left = node;
} else {
if (parent.right != null) {
throw new IllegalStateException("该父节点已经存在右子节点,添加失败");
}
parent.right = node;
}
size++;
return node;
}

/**
* 是否是空树
*
* @return true 是 ;false 否
*/
public boolean isEmpty() {
return size == 0;
}


/**
* 返回节点数
*
* @return 节点数
*/
public int size() {
return size;
}

/**
* 获取根节点
*
* @return 根节点 ;或者null--表示空树
*/
public BinaryTreeNode<E> getRoot() {
return root;
}

/**
* 获取左子节点
*
* @param parent 父节点引用
* @return 左子节点或者null--表示没有左子节点
*/
public BinaryTreeNode<E> getLeft(BinaryTreeNode<E> parent) {
return parent == null ? null : parent.left;
}

/**
* 获取右子节点
*
* @param parent 父节点引用
* @return 右子节点或者null--表示没有右子节点
*/
public BinaryTreeNode<E> getRight(BinaryTreeNode<E> parent) {
return parent == null ? null : parent.right;
}


/**
* 数据判null
*
* @param data 添加的数据
*/
private void checkNullData(E data) {
if (data == null) {
throw new NullPointerException("数据不允许为null");
}
}


/**
* 检查父节点是否为null
*
* @param parent 父节点引用
*/
private void checkNullParent(BinaryTreeNode<E> parent) {
if (parent == null) {
throw new NoSuchElementException("父节点不能为null");
}
}
}

6 总结

本文为大家介绍了二叉树的入门知识,比如二叉树的概念,特性,性质等,这些东西很多是死的,但是需要我们理解记忆,最后介绍了二叉树的存储结构,以及Java语言的简单实现,对于更加特殊的二叉树,比如红黑树,它们还有自己的独特实现,在后续文章中会介绍。

另外,在实现案例中,并没有树的遍历,以及整颗树的创建等操作,这部分内容较多,将在后续的文章中单独介绍,大家可以关注文章更新。最后,如果大家还不是太清楚树的的概念的,可以看这篇文章:数据结构—树(Tree)的入门原理以及Java实现案例。

相关文章:

  1. 《大话数据结构》
  2. 《算法》
  3. 《算法图解》

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

本文转载自: 掘金

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

学习kafka的第二天

发表于 2021-11-25

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

Kafka的集群图

cluster_architecture.png

Broker

Kafka 集群通常由多个代理组成以保持负载平衡。Kafka brokers 是无状态的,所以他们使用 ZooKeeper 来维护他们的集群状态。一个 Kafka broker 实例每秒可以处理数十万次读取和写入,每个 broker 可以处理 TB 级的消息而不会影响性能。Kafka broker leader 选举可以通过 ZooKeeper 来完成。

ZooKeeper

ZooKeeper 用于管理和协调 Kafka 代理。ZooKeeper 服务主要用于通知生产者和消费者有关 Kafka 系统中任何新代理的存在或 Kafka 系统中代理的故障。根据 Zookeeper 收到的关于代理存在或失败的通知,然后生产者和消费者做出决定并开始与其他一些代理协调他们的任务。

Producers

生产者将数据推送给经纪人。当新代理启动时,所有生产者都会搜索它并自动向该新代理发送消息。Kafka 生产者不会等待来自代理的确认,而是以代理可以处理的速度发送消息。

Consumers

由于 Kafka brokers 是无状态的,这意味着消费者必须通过使用分区偏移量来维护已经消费了多少消息。如果消费者确认特定的消息偏移,则意味着消费者已经消费了所有先前的消息。消费者向代理发出异步拉取请求,以准备好使用字节缓冲区。消费者只需提供一个偏移值就可以倒带或跳到分区中的任何点。消费者偏移值由 ZooKeeper 通知。

发布-订阅消息的工作流程

以下是 Pub-Sub 消息传递的逐步工作流程 -

  • 生产者定期向主题发送消息。
  • Kafka 代理将所有消息存储在为该特定主题配置的分区中。它确保消息在分区之间平等共享。如果生产者发送两条消息并且有两个分区,则Kafka将在第一个分区中存储一条消息,在第二个分区中存储第二条消息。
  • 消费者订阅特定主题。
  • 一旦消费者订阅了一个主题,Kafka 将向消费者提供该主题的当前偏移量,并将该偏移量保存在 Zookeeper 集合中。
  • 消费者将定期(如 100 毫秒)向 Kafka 请求新消息。
  • 一旦 Kafka 收到来自生产者的消息,它就会将这些消息转发给消费者。
  • 消费者将收到消息并对其进行处理。
  • 处理完消息后,消费者将向 Kafka 代理发送确认。
  • 一旦 Kafka 收到确认,它会将偏移量更改为新值并在 Zookeeper 中更新它。由于在 Zookeeper 中维护了偏移量,因此即使在服务器异常期间,消费者也可以正确读取下一条消息。
  • 上述流程将重复,直到消费者停止请求。
  • 消费者可以选择随时回退/跳到主题的所需偏移量并阅读所有后续消息。

队列消息/消费者组的工作流程

在队列消息系统而不是单个消费者中,具有相同”Group ID 的”一组消费者将订阅一个主题。简单来说,订阅具有相同”Group ID”的主题的消费者被视为一个组,消息在它们之间共享。让我们检查一下这个系统的实际工作流程。

  • 生产者定期向主题发送消息。
  • Kafka 将所有消息存储在为该特定主题配置的分区中,类似于之前的场景。
  • 单个消费者订阅特定主题,假设”Topic-01”和”Group ID”为”Group-1”。
  • 卡夫卡用相同的方式,发布-订阅消息的消费者交互,直到新的消费订阅同一主题,”主题-01”与同”组ID”为”第1组”。
  • 一旦新的消费者到达,Kafka 将其操作切换到共享模式并在两个消费者之间共享数据。这种共享将一直持续到消费者数量达到为该特定主题配置的分区数量。
  • 一旦消费者数量超过分区数量,新消费者将不会收到任何进一步的消息,直到现有消费者中的任何一个退订。出现这种情况是因为 Kafka 中的每个消费者至少会分配一个分区,一旦所有分区都分配给现有消费者,新消费者将不得不等待。
  • 此功能也称为”消费者组”。同样,Kafka 将以非常简单和高效的方式提供两个系统中最好的。

本文转载自: 掘金

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

(五)Gateway开发教程之为什么选择JWT

发表于 2021-11-25

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

前情回顾

我们已经说到了Gateway中如何利用全局过滤器来做权限认证token处理的功能,但是我们还没有说到如何集成权限认证相关组件功能呢。

今天就说到了,如何实现统一权限认证功能,请接着往下看吧。

微服务中的权限认证

微服务中的权限认证,一般是有着几种常用的解决方案,比如JWT(Json web token)、分布式Session、OAuth2 Token等等方案。

JWT:全称JSON WEB TOKEN,是一种基于JSON的开放标准。该Token的生成,一般都包含着用户的基本信息、角色信息等等,将这些信息通过密钥来进行加密,然后将这个token用于登录认证,同时也可以进行信息的传递。

分布式Session:在单体服务的认证中,早期使用session会话来进行登录认证的一种,而分布式session,则是结合了session原本的特性,与cookie、redis缓存来解决一致性的问题,相对比较复杂。

OAuth2 Token:OAuth2 Token的核心就是为第三方应用颁发令牌,提供授予认证权限的功能,其中提供了四种获得认证令牌的方式,分别是授权码、隐藏式、密码式、客户端凭证等方式来支持开发者进行使用的。

为什么要选择JWT

为什么要选择JWT,那当然是因为其简单易集成,而且可以将用户的基本信息加密到token中,此信息在前端也可以去获取到一定的信息。

在项目初期使用JWT,综合来说还说非常香的。

总结

今天我们来说了为什么要选择JWT来做授权认证逻辑,接下来就是要去集成JWT,来实现统一权限授权认证功能了,敬请期待吧。喜欢可以关注一下专栏。

本文转载自: 掘金

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

Go语言学习查缺补漏ing Day7

发表于 2021-11-25

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

Go语言学习查缺补漏ing Day7

本文收录于我的专栏:《让我们一起Golang》

一、再谈defer的执行顺序

大家来看一看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码package main
​
import "fmt"
​
type Person struct {
age int
}
func main() {
person := &Person{28}
​
//A
defer func(p *Person) {
fmt.Println(p.age)
}(person)
   //B
defer fmt.Println(person.age)
//C
defer func() {
fmt.Println(person.age)
}()
person.age = 21
}

前面我们介绍过defer的执行顺序,但是我今天又遇到新问题,于是这里又补充介绍这个defer的顺序问题。

这个程序运行结果是:

1
2
3
复制代码21
28
21

我们都知道defer的执行顺序是先进后出,所以执行顺序是C、B、A。

B中defer fmt.Println(person.age)输出28,为什么呢?

因为这里是将28作为defer()函数的参数,会把28推入栈中进行缓存,得到执行这条defer语句时就把它拿出来。所以输出28.

而A中:

1
2
3
go复制代码defer func(p *Person) {
fmt.Println(p.age)
}(person)

defer()函数是将结构体Person的地址进行缓存,当后续改变这个地址的内值时,后续输出时这里就会输出那个地址内改变后的值。所以B defer语句执行时从地址中取出的值是29.

而C defer语句理由很简单:

1
2
3
go复制代码defer func() {
fmt.Println(person.age)
}()

就是无参匿名函数的一种情形。闭包引用,person.age改变就会改变。

二、哪种切片的声明比较好?为什么?

1
2
css复制代码var a []int
a := []int{}

这里第一种声明的是nil切片,而第二种声明是创建一个长度以及容量为零的空切片。

第一种切片声明方法比较好,因为它这种声明方式不占用空间,而第二种声明后会占用一部分空间。

三、取得结构体成员的几种方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码package main
​
import "fmt"
​
type S struct {
m string
}
​
func f() *S {
return &S{"ReganYue"}
}
func main() {
p := f()
p2 := *f()
fmt.Println(p.m, p2.m)
}
​

我们运行能够发现:

p、p2都能获取结构体的成员变量。

为什么呢?f()函数的返回值是指针类型,所以p2获取*f()时,p2是S类型,p2.m可以获取其成员变量。

而f()的结果是指针,不过我们前面说过,一级指针能够自动进行解引用。所以也能够访问成员变量。

四、遍历map的存在顺序变化?为什么?

我们执行下面这段代码多次,看输出结果:

1
2
3
4
5
6
7
8
9
10
11
go复制代码package main
​
import "fmt"
​
func main() {
m := map[int]string{0: "zero", 1: "one", 3: "three", 4: "four", 5: "five"}
for k, v := range m {
fmt.Println(k, v)
}
}
​

第一次执行结果如下:

1
2
3
4
5
6
sql复制代码5 five
0 zero
1 one
3 three
4 four
​

第二次执行结果如下:

1
2
3
4
5
6
sql复制代码0 zero
1 one
3 three
4 four
5 five
​

第三次执行结果如下:

1
2
3
4
5
sql复制代码4 four
5 five
0 zero
1 one
3 three

我们发现每一次执行的顺序都是变化的。这说明遍历map的顺序是无序的。为什么呢?

在runtime.mapiterinit中有这样一段代码:

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
go复制代码// mapiterinit initializes the hiter struct used for ranging over maps.
// The hiter struct pointed to by 'it' is allocated on the stack
// by the compilers order pass or on the heap by reflect_mapiterinit.
// Both need to have zeroed hiter since the struct contains pointers.
func mapiterinit(t *maptype, h *hmap, it *hiter) {
if raceenabled && h != nil {
callerpc := getcallerpc()
racereadpc(unsafe.Pointer(h), callerpc, funcPC(mapiterinit))
}
​
if h == nil || h.count == 0 {
return
}
​
if unsafe.Sizeof(hiter{})/sys.PtrSize != 12 {
throw("hash_iter size incorrect") // see cmd/compile/internal/gc/reflect.go
}
it.t = t
it.h = h
​
// grab snapshot of bucket state
it.B = h.B
it.buckets = h.buckets
if t.bucket.ptrdata == 0 {
// Allocate the current slice and remember pointers to both current and old.
// This preserves all relevant overflow buckets alive even if
// the table grows and/or overflow buckets are added to the table
// while we are iterating.
h.createOverflow()
it.overflow = h.extra.overflow
it.oldoverflow = h.extra.oldoverflow
}
​
// decide where to start
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
​
// iterator state
it.bucket = it.startBucket
​
// Remember we have an iterator.
// Can run concurrently with another mapiterinit().
if old := h.flags; old&(iterator|oldIterator) != iterator|oldIterator {
atomic.Or8(&h.flags, iterator|oldIterator)
}
​
mapiternext(it)
}
1
2
3
4
5
6
7
8
9
10
go复制代码    // decide where to start
r := uintptr(fastrand())
if h.B > 31-bucketCntBits {
r += uintptr(fastrand()) << 31
}
it.startBucket = r & bucketMask(h.B)
it.offset = uint8(r >> h.B & (bucketCnt - 1))
​
// iterator state
it.bucket = it.startBucket

我们可以看到,决定从哪开始是根据fastrand()取随机数决定的,所以每次运行,随机数都不一样,所以输出顺序也不一样。

本文转载自: 掘金

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

JUnit5的注解demo

发表于 2021-11-25

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战
今天这篇说的是我们的单元测试,之使用的测试都是JUnit4,SpringBoot2.0之后就引入了JUnit5作为我们的测试工具,那作为JUnit4和JUnit5之间有什么区别吗?

我们现在使用测试只需要创建一个测试类,在类上添加@SpringBootTest注解,测试方法上添加@Test注解就可以了

1
2
3
4
5
6
7
8
9
10
less复制代码@SpringBootTest
class SpringBoot01ApplicationTests {

@Test
void testRedis(){

}


}

接下来带大家使用一下我们新的测试,主要介绍一下这种标签的使用功能

@DisplayName

1
2
3
4
5
6
7
8
9
less复制代码@DisplayName("junit5功能测试类")
public class JunitTest5 {

@DisplayName("displayName")
@Test
void testDisplayName(){
System.out.println(1);
}
}

这个标签主要是用来标注我们的运行标签,演示结果就如下图所示

@BeforeEach和 @AfterEach

1
2
3
4
csharp复制代码@BeforeEach
void testBeforeEach(){
System.out.println("开始测试");
}
1
2
3
4
csharp复制代码@AfterEach
void testAfterEach(){
System.out.println("测试结束");
}

被标注@BeforeEach的方法会在测试方法开始之前开始运行,被标注@AfterEach会在测试方法结束之后运行

@BeforeAll和@AfterAll

1
2
3
4
5
6
7
8
csharp复制代码	@BeforeAll
static void testBeforeAll()P{
System.out.println("所有测试开始");
}
@AfterAll
static void testAfterAll(){
System.out.println("所有测试结束");
}

我们这个两个方法也是在测试开是之前开始的,但是和上面的有什么区别吗?我们的测试不可能就有一个测试,如果有需要公共运行的程序,我们不可能每一个测试都加上一个环绕,添加这@BeforeAll标签的,在所有测试开始之前运行,在所有测试结束之后

@Timeout

1
2
3
4
5
6
less复制代码	@SneakyThrows
@Timeout(value = 5,unit = TimeUnit.MILLISECONDS)
@Test
void testTimeout(){
Thread.sleep(100);
}

他标注了的类必须要在规定的时间内完成否则他就会报错

@SpringBootTest

我们刚刚的测试代码,如果有细心的同学一下子就看出来,我们并没有写@SpringBootTest这个注解,所有如果没有这个注解,我们SpringBoot是不能够使用的。

1
2
3
4
5
6
7
scss复制代码	@Autowired
UserMapper userMapper;

@Test
void te(){
System.out.println(userMapper);
}

甚至我们的idea会直接给我们的提示

我们添加标签之后,我们的tomcat也启动了

我们的对象也拿到了

@RepeatedTest()

1
2
3
4
5
less复制代码	@RepeatedTest(3)
@Test
void testRepeatedTest(){
System.out.println("3");
}

重复测试注解,我们可以在注解后面标注我们测试类运行的次数,它会自动运行

本文转载自: 掘金

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

Spring嵌套事务是怎么回滚的? 源码解析 内层事务 外层

发表于 2021-11-25

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

  • 事务的传播机制
  • 多数据源的切换问题

更深入理解 Spring 事务。

用户注册完成后,需要给该用户登记一门PUA必修课,并更新该门课的登记用户数。
为此,我添加了两个表。
课程表 course,记录课程名称和注册的用户数。

用户选课表 user_course,记录用户表 user 和课程表 course 之间的多对多关联。

同时为课程表初始化了一条课程信息
接下来我们完成用户的相关操作,主要包括两部分:

  • 新增用户选课记录
  • 课程登记学生数 + 1

新增业务类 CourseService实现相关业务逻辑,分别调用了上述方法保存用户与课程的关联关系,并给课程注册人数+1

为避免注册课程的业务异常导致用户信息无法保存,这里 catch 注册课程方法中抛出的异常。希望当注册课程发生错误时,只回滚注册课程部分,保证用户信息依然正常。

为验证异常是否符合预期,在 regCourse() 里抛一个注册失败异常:
执行代码:
注册失败部分的异常符合预期,但是后面又多了一个这样的错误提示:Transaction rolled back because it has been marked as rollback-only

最后用户和选课的信息都被回滚了,显然这不符预期。
期待结果是即便内部事务regCourse()发生异常,外部事务saveStudent()俘获该异常后,内部事务应自行回滚,不影响外部事务。
这是什么原因造成的呢?

源码解析

伪代码梳理整个事务的结构:

整个业务包含2层事务:

  • 外层 saveUser() 的事务
  • 内层 regCourse() 事务

Spring声明式事务中的propagation属性,表示对这些方法使用怎样的事务,即:
一个带事务的方法调用了另一个带事务的方法,被调用的方法它怎么处理自己事务和调用方法事务之间的关系。

propagation 有7种配置:

  • REQUIRED
  • 默认值*,如果本来有事务,则加入该事务,如果没有事务,则创建新的事务。
  • SUPPORTS
  • MANDATORY
  • REQUIRES_NEW
  • NOT_SUPPORTED
  • NEVER
  • NESTED

因为:

  • 在 saveUser() 上声明了一个外部的事务,就已经存在一个事务了
  • 在propagation值为默认REQUIRED时

regCourse() 就会加入到已有的事务中,两个方法共用一个事务。

Spring 事务处理的核心:

TransactionAspectSupport.invokeWithinTransaction()

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
java复制代码protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {

TransactionAttributeSource tas = getTransactionAttributeSource();
final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
final PlatformTransactionManager tm = determineTransactionManager(txAttr);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) {
// 是否需要创建一个事务
TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification);
Object retVal = null;
try {
// 调用具体的业务方法
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 当发生异常时进行处理
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
cleanupTransactionInfo(txInfo);
}
// 正常返回时提交事务
commitTransactionAfterReturning(txInfo);
return retVal;
}
//......省略非关键代码.....
}

整个方法完成了事务的一整套处理逻辑,如下:

  • 检查是否需要创建事务
  • 调用具体的业务方法进行处理
  • 提交事务
  • 处理异常

当前案例是两个事务嵌套,外层事务 saveUser()和内层事务 regCourse(),每个事务都会调用到这个方法。所以,该方法会被调两次。

内层事务

当捕获了异常,会调用

TransactionAspectSupport.completeTransactionAfterThrowing()

进行异常处理:

对异常类型做了一些检查,当符合声明中的定义后,执行具体的 rollback 操作,这个操作是通过如下方法完成:

AbstractPlatformTransactionManager

rollback()

该回滚实现负责处理正参与到已有事务集的事务。委托执行Rollback和doSetRollbackOnly。

继续调用

processRollback()


该方法里区分了三种场景:

  • 是否有保存点
  • 是否为一个新的事务
  • 是否处于一个更大的事务中

因为默认传播类型REQUIRED,嵌套的事务并未开启一个新事务,所以属于当前事务处于一个更大事务中,所以会走到分支1。

如下的判断条件确定是否设置为仅回滚:

1
2
java复制代码if (status.isLocalRollbackOnly() ||
isGlobalRollbackOnParticipationFailure())

满足任一,都会执行 doSetRollbackOnly():

  • isLocalRollbackOnly

    默认 false,当前场景为 false
  • isGlobalRollbackOnParticipationFailure()

    所以,就只由该方法来确定了,默认值为 true, 即是否回滚交由外层事务统一决定

条件得到满足,执行

DataSourceTransactionManager#doSetRollbackOnly

最终调用

DataSourceTransactionObject#setRollbackOnly()


内层事务操作执行完毕。

外层事务

外层事务中,业务代码就捕获了内层所抛异常,所以该异常不会继续往上抛,最后的事务会在 TransactionAspectSupport.invokeWithinTransaction() 中的

TransactionAspectSupport#commitTransactionAfterReturning()


该方法里执行了commit 操作:

AbstractPlatformTransactionManager#commit

当满足 !shouldCommitOnGlobalRollbackOnly() &&defStatus.isGlobalRollbackOnly(),就会回滚,否则继续提交事务:

  • shouldCommitOnGlobalRollbackOnly()
    若发现事务被标记了全局回滚,且在发生全局回滚时,判断是否应该提交事务,这个方法的默认返回 false,这里无需关注
  • isGlobalRollbackOnly()

    该方法最终进入

DataSourceTransactionObject#isRollbackOnly()

之前内部事务处理最终调用到DataSourceTransactionObject#setRollbackOnly()

1
2
3
java复制代码public void setRollbackOnly() {
getConnectionHolder().setRollbackOnly();
}
  • isRollbackOnly()
  • setRollbackOnly()

两个方法本质都是对ConnectionHolder.rollbackOnly属性标志位的存取
但ConnectionHolder则存在于DefaultTransactionStatus#transaction属性。

综上:外层事务是否回滚的关键,最终取决于DataSourceTransactionObject#isRollbackOnly(),该方法返回值正是在内层异常时设置的。
所以最终外层事务也被回滚,从而在控制台中打印上述日志。

这就明白了,Spring默认事务传播属性为REQUIRED:若已有事务,则加入该事务,若无事务,则创建新事务,因而内外两层事务都处于同一事务。
在 regCourse()中抛异常,并触发回滚操作时,这个回滚会继续传播,从而把 saveUser() 也回滚,最终整个事务都被回滚!

修正

Spring事务默认传播属性 REQUIRED,在整个事务的调用链上,任一环节抛异常都会导致全局回滚。

所以只需将传播属性改成 REQUIRES_NEW :

运行:

异常正常抛出,注册课程部分的数据没有保存,但用户还是正常注册成功。这意味着此时Spring 只对注册课程这部分的数据进行了回滚,并没有传播到外层:

  • 当子事务声明为 Propagation.REQUIRES_NEW 时,在 TransactionAspectSupport.invokeWithinTransaction() 中调用 createTransactionIfNecessary() 就会创建一个新的事务,独立于外层事务
  • 而在 AbstractPlatformTransactionManager.processRollback() 进行 rollback 处理时,因为 status.isNewTransaction() 会因为它处于一个新的事务中而返回 true,所以它走入到了另一个分支,执行了 doRollback() 操作,让这个子事务单独回滚,不会影响到主事务。

本文转载自: 掘金

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

PL/SQL 连接远程 Oracle 数据库配置

发表于 2021-11-25

刚参加工作到一家新公司入职,一开始肯定要给新的工作电脑安装开发环境了,还记得当初初出茅庐,会的东西不是很多,单是连接公司远程数据库数据库服务器都搞了半天。

公司一般都会有开发环境的数据库服务器,如果是 Oracle 数据库,那我们就需要使用 PL/SQL 远程连接数据库进行开发,接下来就看看如何配置吧!

配置

1. 下载 Instant Client 和 PL/SQL:

a. Instant Client

可以在官网下载 Instant Client for Microsoft Windows (x64) 64-bit ,

但是注意要下载与服务器端Oracle版本匹配的版本,官网下载可能要登陆,没有账号的话百度搜一下 就有共享的。

b. PL/SQL

下载地址 [Registered download PL/SQL Developer - Allround Automations](https://www.allroundautomations.com/registered/plsqldev.html)

c. 配置

下载好 instantclient 和 PL/SQL 后需要做一些配置:

(1). 将 instantclient 解压在任意文件夹,然后在解压过的文件夹下创建network目录,

(2). 在network目录下创建admin目录,然后在admin目录下创建 tnsnames.ora文件。

(3). 在 tnsnames.ora 文件下编辑,做如下配置:`

1
2
3
4
5
6
7
8
9
java复制代码ORCL =
(DESCRIPTION =
(ADDRESS_LIST =
(ADDRESS = (PROTOCOL = TCP)(HOST = 10.168.178.xxx)(PORT = 1521))
)
(CONNECT_DATA =
(SERVICE_NAME = orcl )
)
)

其中 HOST 是远程服务器地址还有端口号,SERVICE_NAME 地方是远程数据库名称,自己根据实际情况修改,编辑完之后保存。此处一定要注意格式,多一个空格就会连接不上

2. 添加环境变量

接下来再添加一下instantclient 的环境变量:

)

除此之外,你可能会出现 PL/SQL 的查询结果中文乱码的情况,那么此时还需要配置另一个环境变量:NLS_LANG

你可以通过下面的语句查询出来:

1
java复制代码select userenv('language') from dual;

image.png

然后复制查询出来的结果配置到 NLS_LANG 变量下

image.png

配置完成后,重启一下 PL/SQL 就不会有中文乱码了。

3. 配置 PL/SQL

最后再进入PL/SQL中配置一下:

登陆界面点击取消,进入未登录的界面。

点击Tools–》Preferences–》Connection。填写相应的自己的盘符的目录值。其他默认即可

这个时候就已经配置好了,直接重启PLSQL,登陆界面会显示相应的Database等下拉框信息,输入远程数据库的用户名和密码,登陆成功。

本文转载自: 掘金

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

Python matplotlib 绘制箱型图 复习回顾 1

发表于 2021-11-25

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

复习回顾

matplotlib 是非常强大的模块,包含绘制图形类、制作动画类、图形处理类等,我们前面也已经对pyplot类折线、柱状等常见图的绘制方法的学习,继上一节我们也学习了应用在物理比较多的量场图的绘制方法,往列举几篇期文章如下。

  • pyplot.quiver()绘制量场图:matplotlib 绘制量场图
  • pyplot.ion()交互模式绘制动态图:matplotlib 绘制动态图
  • animation类绘制动态图:matplotlib Animation类
  • 对图像处理的image类:matplotlib 图像处理

在matplotlib模块中,量场图是常应用在物理学上,而箱型图更多在统计数据上应用。

箱型图.png

本期,我们将学习matplotlib.pyplot.boxplot()相关方法属性的学习,let’s go~

  1. 箱型图概述

  • 什么是箱型图?

+ 箱型图外观如箱子,又名为盒须图、盒式图或者箱线图。
+ 箱线图是一种用作于显示一组数据分散情况资料的统计图
+ 箱型图能显示一组数据的最大值、最小值、中位数,及上下四分位数六个数据点![image.png](https://gitee.com/songjianzaina/juejin_p7/raw/master/img/f5dbc592af4690ab97b579aa71e1413a1e7038ea5ca7a0c860e469097dc6a948)
  • 箱型图应用场景

+ 箱型图由于能显示一组数据分散情况,常用于品质管理
+ 箱型图有利于数据的清洗,能快速知道数据分别情况
+ 箱型图有助于分析一直数据的偏向如分析公司员工收入水平
  • 使用箱型图方法

1
2
python复制代码import matplotlib.pyplot as plt 
plt.boxplot(x)
  1. 箱型图属性

  • 设置箱型凹凸

+ 关键字:notch
+ 默认值为:False,显示非凹凸型
  • 设置箱型位置

+ 关键字:vert
+ 默认值为:True
+ 当vert设置为True时,绘制垂直框
+ 当vert设置为False时,绘制水平框
  • 设置箱体颜色填充

+ 关键字:patch\_artist
+ 默认位置为:False
+ 当patch\_artist为False,为Line2D artist
+ 当patch\_artist为True,为Patch artist,可填充颜色
  • 设置箱型均值样式

+ 关键字:meanline
+ 默认值为:False
+ 当meanline为False,不显示均值线
+ 当meanline为True,且showmeans=True 均值线会以虚线的形式显示
  • 设置箱型边框显示

+ 设置箱型末端显示关键字:showcaps
+ 设置箱型箱体显示关键字:showbox
+ 默认值为:True
  1. 绘制箱型图步骤

  • 导入matplotlib.pyplot类
1
python复制代码import matplotlib.pyplot as plt
  • 使用numpy库里的arange(),random()等方法准备x数组向量序列
1
python复制代码x = np.arange(10,20,5)
  • 调用pyplot.boxplot()方法绘制箱型图,显示中位数(红色)和均值线(虚线)
1
python复制代码plt.boxplot(x,meanline=True,showmeans=True,labels=["A"])
  • 最后调用pyplot.show()渲染打印出箱型图

image.png

  1. 小试牛刀

学习以上箱型相关属性和绘制步骤,我们来绘制一组数据箱型图并填充不同的颜色

  • 调用numpy.random.randint()准备5组数据
  • 使用字典形式定义箱型样式boxprops属性
  • 使用字典形式定义箱型均值样式meanprops属性
1
2
3
4
5
6
7
8
9
python复制代码x = np.random.randint(10,100,size=(5,5))

box = {"linestyle":'--',"linewidth":3,"color":'blue'}

mean = {"marker":'o','markerfacecolor':'pink','markersize':12}

plt.boxplot(x,meanline=True,showmeans=True,labels=["A","B","C","D","E"],boxprops=box,meanprops=mean)

plt.show()

image.png

  • 如果箱型要填充颜色,则需要使用patch_artist 属性设置为True
1
2
3
4
5
python复制代码b = plt.boxplot(x,meanline=True,showmeans=True,labels=["A","B","C","D","E"],boxprops=box,meanprops=mean,patch_artist=True)

for c in b["boxes"] :

c.set(color="lightblue")

image.png

总结

本期,我们对matplotlib.pyplot 提供boxplot()绘制箱型图相关属性进行学习。对于箱型图更有利于快速知道一组数据的分布情况。

以上是本期内容,欢迎大佬们点赞评论,下期见~

本文转载自: 掘金

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

Git 冲突时的常用操作

发表于 2021-11-25

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

场景例子

更新代码时,遇到代码冲突的时候

  1. 没有使用git add 将代码加到暂存区,即代码还在工作区时,使用 git checkout命令丢弃修改。

git checkout [file path]

1
2
3
4
shell复制代码## 丢弃app.java文件的修改
git checkout app.java
## 丢弃所有工作区的修改
git checkout .
  1. 在使用了git add将代码加到暂存区了,使用 git reset HEAD 命令
  • 保留工作区、清除暂存区代码
    git reset [--mixed] HEAD^
  • 保留工作目录的内容,并将工作区的内容所带来的新文件差异放进暂存区
    git reset --soft HEAD^
  • 清除工作区、暂存区的代码
    git reset --hard HEAD^
  1. 本地有暂时不方便提交的代码时,例如突然有bug要修复,本地的代码还在开发中,那么就适合使用 git stash命令将代码储存起来。
1
2
3
4
5
6
7
8
9
10
perl复制代码git stash save 'msg' # 保存当前工作区修改的内容
git stash list # 查看stash栈的所有内容
git stash apply # 恢复stash栈储存的内容到本地,但不删除
git stash drop # 主动删除stash栈储存的内容
git stash pop # stash栈顶出栈

# 当stash 里面有多个存储时,想指定某一个恢复
git stash apply stash@{0}
或
git stash pop stash@{0}
  1. git push时提示推送失败
  • 不同人修改了不同文件;
  • 不同人修改了同文件的不同区域;
  • 不同人修改了同文件的同一区域;

前两种情况,使用git pull 可以实现代码自动合并;
也可以使用git fetch 和 git merge 命令去分开手动执行。

1
2
3
4
bash复制代码git fetch # 将远程分支更新到本地
git merge xxx # 合并远程分支

xxx 可以是远程分支上的某个提交,也可以是远程分支的名字,例如origin/master

第三种情况,使用git pull命令的话,git 在自动合并时可能会报错。这种情况是需要手动修改冲突提交的,因为git无法选择应该保留冲突的哪一方。

找到冲突的文件,使用vi 或者ide 将冲突修复,看保留哪些修改。
修改完后,使用git add + commit 命令将文件提交,后推送即可。

​
ghFtNn.png

参考文章

[Git Reset 三种模式] www.jianshu.com/p/c2ec5f06c…

本文转载自: 掘金

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

1…189190191…956

开发者博客

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