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

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


  • 首页

  • 归档

  • 搜索

ArrayList 重拳出击,把 LinkedList 干翻

发表于 2021-10-10

大家好,我是二哥呀。

这是《Java 程序员进阶之路》专栏的第 60 篇,我们来聊聊 ArrayList 和 LinkedList 之间的区别。大家可以到 GitHub 上给二哥一个 star,冲 500 星标了。

github.com/itwanger/to…

如果再有人给你说 “ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快”,你可以让他滚了!

这是一个极其不负责任的总结,关键是你会在很多地方看到这样的结论。

害,我一开始学 Java 的时候,也问过一个大佬,“ArrayList 和 LinkedList 有什么区别?”他就把“ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快”甩给我了,当时觉得,大佬好牛逼啊!

后来我研究了 ArrayList 和 LinkedList 的源码,发现还真的是,前者是数组,后者是 LinkedList,于是我对大佬更加佩服了!

直到后来,我亲自跑程序验证了一遍,才发现大佬的结论太草率了!根本就不是这么回事!

先来给大家普及一个概念——时间复杂度。

在计算机科学中,算法的时间复杂度(Time complexity)是一个函数,它定性描述该算法的运行时间。这是一个代表算法输入值的字符串的长度的函数。时间复杂度常用大 O 符号表述,不包括这个函数的低阶项和首项系数。使用这种方式时,时间复杂度可被称为是渐近的,亦即考察输入值大小趋近无穷时的情况。例如,如果一个算法对于任何大小为 n (必须比 n0n_0n0​ 大)的输入,它至多需要 5n3 +3n5n^3 + 3n5n3 +3n 的时间运行完毕,那么它的渐近时间复杂度是 O(n3)O(n3^)O(n3)。

增删改查,对应到 ArrayList 和 LinkedList,就是 add(E e)、remove(int index)、add(int index, E element)、get(int index),我来给大家一一分析下,它们对应的时间复杂度,也就明白了“ArrayList 底层是数组,查询快、增删慢;LinkedList 底层是链表,查询慢、增删快”这个结论很荒唐的原因

对于 ArrayList 来说:

1)get(int index) 方法的时间复杂度为 O(1)O(1)O(1),因为是直接从底层数组根据下标获取的,和数组长度无关。

1
2
3
4
java复制代码public E get(int index) {
Objects.checkIndex(index, size);
return elementData(index);
}

这也是 ArrayList 的最大优点。

2)add(E e) 方法会默认将元素添加到数组末尾,但需要考虑到数组扩容的情况,如果不需要扩容,时间复杂度为 O(1)O(1)O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public boolean add(E e) {
modCount++;
add(e, elementData, size);
return true;
}

private void add(E e, Object[] elementData, int s) {
if (s == elementData.length)
elementData = grow();
elementData[s] = e;
size = s + 1;
}

如果需要扩容的话,并且不是第一次(oldCapacity > 0)扩容的时候,内部执行的 Arrays.copyOf() 方法是耗时的关键,需要把原有数组中的元素复制到扩容后的新数组当中。

1
2
3
4
5
6
7
8
9
10
11
java复制代码private Object[] grow(int minCapacity) {
int oldCapacity = elementData.length;
if (oldCapacity > 0 || elementData != DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
int newCapacity = ArraysSupport.newLength(oldCapacity,
minCapacity - oldCapacity, /* minimum growth */
oldCapacity >> 1 /* preferred growth */);
return elementData = Arrays.copyOf(elementData, newCapacity);
} else {
return elementData = new Object[Math.max(DEFAULT_CAPACITY, minCapacity)];
}
}

3)add(int index, E element) 方法将新的元素插入到指定的位置,考虑到需要复制底层数组(根据之前的判断,扩容的话,数组可能要复制一次),根据最坏的打算(不管需要不需要扩容,System.arraycopy() 肯定要执行),所以时间复杂度为 O(n)O(n)O(n)。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public void add(int index, E element) {
rangeCheckForAdd(index);
modCount++;
final int s;
Object[] elementData;
if ((s = size) == (elementData = this.elementData).length)
elementData = grow();
System.arraycopy(elementData, index,
elementData, index + 1,
s - index);
elementData[index] = element;
size = s + 1;
}

来执行以下代码,把沉默王八插入到下标为 2 的位置上。

1
2
3
4
5
6
7
8
java复制代码ArrayList<String> list = new ArrayList<>();
list.add("沉默王二");
list.add("沉默王三");
list.add("沉默王四");
list.add("沉默王五");
list.add("沉默王六");
list.add("沉默王七");
list.add(2, "沉默王八");

System.arraycopy() 执行完成后,下标为 2 的元素为沉默王四,这一点需要注意。也就是说,在数组中插入元素的时候,会把插入位置以后的元素依次往后复制,所以下标为 2 和下标为 3 的元素都为沉默王四。

image.png

之后再通过 elementData[index] = element 将下标为 2 的元素赋值为沉默王八;随后执行 size = s + 1,数组的长度变为 7。

image.png

4) remove(int index) 方法将指定位置上的元素删除,考虑到需要复制底层数组,所以时间复杂度为 O(n)O(n)O(n)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public E remove(int index) {
Objects.checkIndex(index, size);
final Object[] es = elementData;

@SuppressWarnings("unchecked") E oldValue = (E) es[index];
fastRemove(es, index);

return oldValue;
}
private void fastRemove(Object[] es, int i) {
modCount++;
final int newSize;
if ((newSize = size - 1) > i)
System.arraycopy(es, i + 1, es, i, newSize - i);
es[size = newSize] = null;
}

对于 LinkedList 来说:

1)get(int index) 方法的时间复杂度为 O(n)O(n)O(n),因为需要循环遍历整个链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public E get(int index) {
checkElementIndex(index);
return node(index).item;
}

LinkedList.Node<E> node(int index) {
// assert isElementIndex(index);

if (index < (size >> 1)) {
LinkedList.Node<E> x = first;
for (int i = 0; i < index; i++)
x = x.next;
return x;
} else {
LinkedList.Node<E> x = last;
for (int i = size - 1; i > index; i--)
x = x.prev;
return x;
}
}

下标小于链表长度的一半时,从前往后遍历;否则从后往前遍历,这样从理论上说,就节省了一半的时间。

如果下标为 0 或者 list.size() - 1 的话,时间复杂度为 O(1)O(1)O(1)。这种情况下,可以使用 getFirst() 和 getLast() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public E getFirst() {
final LinkedList.Node<E> f = first;
if (f == null)
throw new NoSuchElementException();
return f.item;
}

public E getLast() {
final LinkedList.Node<E> l = last;
if (l == null)
throw new NoSuchElementException();
return l.item;
}

first 和 last 在链表中是直接存储的,所以时间复杂度为 O(1)O(1)O(1)。

2)add(E e) 方法默认将元素添加到链表末尾,所以时间复杂度为 O(1)O(1)O(1)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public boolean add(E e) {
linkLast(e);
return true;
}
void linkLast(E e) {
final LinkedList.Node<E> l = last;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

3)add(int index, E element) 方法将新的元素插入到指定的位置,需要先通过遍历查找这个元素,然后再进行插入,所以时间复杂度为 O(n)O(n)O(n)。

1
2
3
4
5
6
7
8
java复制代码public void add(int index, E element) {
checkPositionIndex(index);

if (index == size)
linkLast(element);
else
linkBefore(element, node(index));
}

如果下标为 0 或者 list.size() - 1 的话,时间复杂度为 O(1)O(1)O(1)。这种情况下,可以使用 addFirst() 和 addLast() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public void addFirst(E e) {
linkFirst(e);
}
private void linkFirst(E e) {
final LinkedList.Node<E> f = first;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(null, e, f);
first = newNode;
if (f == null)
last = newNode;
else
f.prev = newNode;
size++;
modCount++;
}

linkFirst() 只需要对 first 进行更新即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public void addLast(E e) {
linkLast(e);
}

void linkLast(E e) {
final LinkedList.Node<E> l = last;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(l, e, null);
last = newNode;
if (l == null)
first = newNode;
else
l.next = newNode;
size++;
modCount++;
}

linkLast() 只需要对 last 进行更新即可。

需要注意的是,有些文章里面说,LinkedList 插入元素的时间复杂度近似 O(1)O(1)O(1),其实是有问题的,因为 add(int index, E element) 方法在插入元素的时候会调用 node(index) 查找元素,该方法之前我们之间已经确认过了,时间复杂度为 O(n)O(n)O(n),即便随后调用 linkBefore() 方法进行插入的时间复杂度为 O(1)O(1)O(1),总体上的时间复杂度仍然为 O(n)O(n)O(n) 才对。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码void linkBefore(E e, LinkedList.Node<E> succ) {
// assert succ != null;
final LinkedList.Node<E> pred = succ.prev;
final LinkedList.Node<E> newNode = new LinkedList.Node<>(pred, e, succ);
succ.prev = newNode;
if (pred == null)
first = newNode;
else
pred.next = newNode;
size++;
modCount++;
}

4) remove(int index) 方法将指定位置上的元素删除,考虑到需要调用 node(index) 方法查找元素,所以时间复杂度为 O(n)O(n)O(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
java复制代码public E remove(int index) {
checkElementIndex(index);
return unlink(node(index));
}

E unlink(LinkedList.Node<E> x) {
// assert x != null;
final E element = x.item;
final LinkedList.Node<E> next = x.next;
final LinkedList.Node<E> prev = x.prev;

if (prev == null) {
first = next;
} else {
prev.next = next;
x.prev = null;
}

if (next == null) {
last = prev;
} else {
next.prev = prev;
x.next = null;
}

x.item = null;
size--;
modCount++;
return element;
}

通过时间复杂度的比较,以及源码的分析,我相信大家在选择的时候就有了主意,对吧?

需要注意的是,如果列表很大很大,ArrayList 和 LinkedList 在内存的使用上也有所不同。LinkedList 的每个元素都有更多开销,因为要存储上一个和下一个元素的地址。ArrayList 没有这样的开销。

查询的时候,ArrayList 比 LinkedList 快,这是毋庸置疑的;插入和删除的时候,LinkedList 因为要遍历列表,所以并不比 ArrayList 更快。反而 ArrayList 更轻量级,不需要在每个元素上维护上一个和下一个元素的地址。

但是,请注意,如果 ArrayList 在增删改的时候涉及到大量的数组复制,效率就另当别论了,因为这个过程相当的耗时。

对于初学者来说,一般不会涉及到百万级别的数据操作,如果真的不知道该用 ArrayList 还是 LinkedList,就无脑选择 ArrayList 吧!


这是《Java 程序员进阶之路》专栏的第 60 篇。Java 程序员进阶之路,风趣幽默、通俗易懂,对 Java 初学者极度友好和舒适😘,内容包括但不限于 Java 语法、Java 集合框架、Java IO、Java 并发编程、Java 虚拟机等核心知识点,目前已连载 60 篇。

github.com/itwanger/to…

大家觉得文章里有哪一点有问题的话,也可以在留言区给出自己的观点和思考。

本文转载自: 掘金

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

💖10分钟阿里云搭建个人网站(保姆级新手向) 掘金官方周边

发表于 2021-10-10

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

image.png
最近不少XiaoLin的粉丝找到我说:

林哥,我是应届生,最近要面试了,有没有什么可以加分的?

林哥,我准备找实习,最近要写简历了,有没有什么可以加分的?

林哥,我最近准备毕业设计,有没有什么可以让导师眼前一亮的操作?

对于这些问题的答案,我通常都会说自己搭建一个网站,利用阿里云/腾讯云/华为云/七牛云等等各种云服务器来快速搭建一个属于自己的装x小网站,虽然不是很牛x,但是对于说找实习、做毕业设计加分,这种纯粹是绰绰有余,别人会觉得你很有动手能力,话不多说,马上开始,给我十分钟,带你学会一个装x的小知识点。

image.png

🎁购买服务器

购买服务器的步骤我就不多说了,打开某60/某度,随便搜索一个云服务器都行,今天我使用的是阿里云的服务器,仅仅只是因为上次随手购买的一个服务器。


无论是什么服务器都行,**重点是必须是Linux系统,不然后面步骤将变得毫无意义!** 一般学生用1核2G即可,一年也不贵,大概也就80-100一个月。

🧨安装环境

今天我们就安装最简单的基础环境用来测试即可,仅仅只安装JDK和MySQL,如果有不会安装的可以看我的往期博客:
  • 测试N次,阿里云centos7安装mysql5.7,看完这篇一次过!
  • Linux实践踩坑之5分钟安装JDK

准备项目

接下来就到了我们最激动人心的时刻了,我们首先要准备一个测试的项目,今天我拿的一个测试项目是一个小型的简历管理系统,还没有完全写完,是一个基于SpringBoot+Freemarker的项目,是一个很简单的项目。


我们首先需要修改一下数据库的相关信息。

image.png

我们接下来就开始打包项目啦!找到IDEA的mvane->项目名称->Lifecycle->package

image.png

看到这个,说明我们的项目打包完毕了,已经可以开始进行部署了!Nice!

image.png

我们去到Idea打包好的默认的jar包的位置,就可以看到我们的jar包啦!

image.png

最后一步是将数据库同步到远程服务器的数据库!

部署

我们首先把刚才的jar包放在服务器上任意的目录,虽然说是任意目录,但是最好还是有规范的目录,便于后期维护和拓展。我的目录是在`/usr/project`下,所以我们cd到那里去

image.png
接着拖拽上传我们的项目。

image.png
最后一步是运行我们的项目。

1
shell复制代码 java -jar ResumeManager-1.0.0.jar

来了来了,他来了!

image.png
如果没有任何报错的话,说明我们的项目就是搭建部署起来了,但是呢,如果这个黑窗窗关了,我们的jar包也会停止运行,那么如何让他在后台运行呢?我们只需在前面加一个单词。

1
shell复制代码nohup java -jar ResumeManager-1.0.0.jar

开放端口

如果我们是使用云服务器,一把都还有最后一步,那就是开放端口,云服务器为了安全起见,很多的端口是关闭的,无法访问的,这也就造成了我们会无法访问我们的网站,以阿里云服务器为例。

image.png

image.png

image.png

测试

最后ip+端口就可以访问啦!大功告成

image.png

如果各位观众老爷们有什么发布的时候的BUG或者是有建议,欢迎在评论区提出,我会一个一个查看和接实,给个一键三连是最最好的了,掘金官方将在评论区抽100位幸运小伙伴送出官方周边大礼包,概率很大,你评一下!

本文转载自: 掘金

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

❤️ MySQL 8 新特性:全局参数持久化! 前言 全局参

发表于 2021-10-10

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

前言

自从 2018 年发布第一版 MySQL 8.0.11 正式版至今,MySQL 版本已经更新迭代到 8.0.26,相对于稳定的 5.7 版本来说,8.0 在性能上的提升是毋庸置疑的!

随着越来越多的企业开始使用 MySQL 8.0 版本,对于 DBA 来说是一个挑战,也是一个机遇!💪🏻

本文主要讨论下 MySQL 8.0 版本的新特性:全局参数持久化

全局参数持久化

MySQL 8.0 版本支持在线修改全局参数并持久化,通过加上 PERSIST 关键字,可以将修改的参数持久化到新的配置文件(mysqld-auto.cnf)中,重启 MySQL 时,可以从该配置文件获取到最新的配置参数!

对应的Worklog [WL#8688]:dev.mysql.com/worklog/tas…

启用这个功能,使用特定的语法 SET PERSIST 来设定任意可动态修改的全局变量!

  • SET PERSIST
    语句可以修改内存中变量的值,并且将修改后的值写⼊数据⽬录中的 mysqld-auto.cnf 中。
  • SET PERSIST_ONLY
    语句不会修改内存中变量的值,只是将修改后的值写⼊数据⽬录中的 mysqld-auto.cnf 中。

以 max_connections 参数为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
sql复制代码mysql> select * from performance_schema.persisted_variables;
Empty set (0.00 sec)

mysql> show variables like '%max_connections%';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| max_connections | 151 |
| mysqlx_max_connections | 100 |
+------------------------+-------+
2 rows in set (0.00 sec)

mysql> set persist max_connections=300;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from performance_schema.persisted_variables;
+-----------------+----------------+
| VARIABLE_NAME | VARIABLE_VALUE |
+-----------------+----------------+
| max_connections | 300 |
+-----------------+----------------+
1 row in set (0.00 sec)

系统会在数据目录下生成一个包含 json 格式的 mysqld-auto.cnf 的文件,格式化后如下所示,当 my.cnf 和mysqld-auto.cnf 同时存在时,后者具有更高优先级。

1
2
3
4
5
6
7
8
9
10
11
12
13
json复制代码{
"Version": 1,
"mysql_server": {
"max_connections": {
"Value": "300",
"Metadata": {
"Timestamp": 1632575065787609,
"User": "root",
"Host": "localhost"
}
}
}
}

📢 注意: 即使你通过 SET PERSIST 修改配置的值并没有任何变化,也会写入到 mysqld-auto.cnf 文件中。但你可以通过设置成 DEFAULT 值的方式来恢复初始默认值!

如果想要恢复 max_connections 参数为初始默认值,只需要执行:

1
2
3
4
5
6
7
8
9
10
sql复制代码mysql> set persist max_connections=DEFAULT;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from performance_schema.persisted_variables;
+-----------------+----------------+
| VARIABLE_NAME | VARIABLE_VALUE |
+-----------------+----------------+
| max_connections | 151 |
+-----------------+----------------+
1 row in set (0.00 sec)

如果想要移除所有的全局持久化参数,则只需执行:

1
2
3
4
5
sql复制代码mysql> RESET PERSIST;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from performance_schema.persisted_variables;
Empty set (0.00 sec)

当然,删除 mysqld-auto.cnf 文件后,重启 MySQL 也可!

写在最后

主要代码:
Commit f2bc0f89b7f94cc8fe963d08157413a01d14d994

主要入口函数(8.0.0):

1
2
3
4
5
6
arduino复制代码接口函数大多定义在sql/persisted_variable.cc文件中:
启动时载入mysqld-auto.cnf的内容: Persisted_variables_cache::load_persist_file(); 通过json解析合法性,并存入内存
将文件中读取的配置进行设置: Persisted_variables_cache::set_persist_options

运行SET PERSIST命令时,调用Persisted_variables_cache::set_variable 更新内存中存储的值
写入mysqld-auto.cnf文件中: Persisted_variables_cache::flush_to_file

参考文档:

  • mysqlserverteam.com/mysql-8-0-p…
  • dev.mysql.com/worklog/tas…
  • bugs.mysql.com/bug.php?id=…

本次分享到此结束啦~

如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是我创作最大的动力。

❤️ 技术交流可以 关注公众号:Lucifer三思而后行 ❤️

本文转载自: 掘金

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

花式玩转二叉树遍历,递归迭代应有尽有,绝对满足您的需求!!!

发表于 2021-10-10

小知识,大挑战!本文正在参与“ 程序员必备小知识 ”创作活动

本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金

Code皮皮虾 一个沙雕而又有趣的憨憨少年,和大多数小伙伴们一样喜欢听歌、游戏,当然除此之外还有写作的兴趣,emm…,日子还很长,让我们一起加油努力叭🌈

如果觉得写得不错的话,球球一个关注哦😉


前言

二叉树遍历对应地址:

力扣:二叉树前序遍历地址

力扣:二叉树中序遍历地址

力扣:二叉树后序遍历地址

力扣:二叉树层序遍历地址


二叉树层序遍历

使用广度优先搜索(BFS)

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 Main {



public List<List<Integer>> levelOrder(TreeNode root) {

List<List<Integer>> res = new ArrayList<>();
if(root == null)
return res;

Deque<TreeNode> queue = new LinkedList<>();
queue.add(root);

while(!queue.isEmpty()) {
int size = queue.size();
List<Integer> list = new ArrayList<>();
while (size-- > 0) {
TreeNode node = queue.poll();
list.add(node.val);
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
res.add(list);
}

return res;
}

}

class TreeNode {
int val;
TreeNode left;
TreeNode right;

TreeNode() {
}

TreeNode(int val) {
this.val = val;
}

TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}

在这里插入图片描述


递归法


二叉树前序遍历

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
java复制代码
public class Main {


List<Integer> res;
public List<Integer> preorderTraversal(TreeNode root) {
res = new ArrayList<>();
if(root == null)
return res;
preTreeNode(root);
return res;
}

private void preTreeNode(TreeNode root) {
if(root == null) return;

res.add(root.val);

preTreeNode(root.left);
preTreeNode(root.right);
}

}

class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode() {}
TreeNode(int val) { this.val = val; }
TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}

在这里插入图片描述
复杂度分析

  • 时间复杂度:O(n),其中 n 是二叉树的节点数。每一个节点恰好被遍历一次。
  • 空间复杂度:O(n),为递归过程中栈的开销,平均情况下为 O(logn),最坏情况下树呈现链状,为 O(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
java复制代码public class Main {


List<Integer> res;

public List<Integer> inorderTraversal(TreeNode root) {
res = new ArrayList<>();
if (root == null)
return res;

midTreeNode(root);
return res;
}

private void midTreeNode(TreeNode root) {
if (root == null) return;

midTreeNode(root.left);

res.add(root.val);

midTreeNode(root.right);
}

}

class TreeNode {
int val;
TreeNode left;
TreeNode right;

TreeNode() {
}

TreeNode(int val) {
this.val = val;
}

TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}

在这里插入图片描述


二叉树后序遍历

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 Main {


List<Integer> res;
public List<Integer> postorderTraversal(TreeNode root) {
res = new ArrayList<>();
if(root == null)
return res;

postTreeNode(root);
return res;
}

private void postTreeNode(TreeNode root) {
if(root == null) return;

postTreeNode(root.left);
postTreeNode(root.right);

res.add(root.val);
}

}

class TreeNode {
int val;
TreeNode left;
TreeNode right;

TreeNode() {
}

TreeNode(int val) {
this.val = val;
}

TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}

在这里插入图片描述


迭代法


二叉树前序遍历

与递归方法相差无几,只是把递归中隐藏的栈显示出来

因为栈的特性,我们需要先把右子节点push到栈中,再push左子节点,这样拿出来的时候就是先拿出左子节点。

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
java复制代码
public class Main {

public List<Integer> preorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) {
return res;
}
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
res.add(node.val);
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
return res;
}

}

class TreeNode {
int val;
TreeNode left;
TreeNode right;

TreeNode() {
}

TreeNode(int val) {
this.val = val;
}

TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}

在这里插入图片描述


二叉树中序遍历

中序遍历式按 左 中 右的顺序输出节点。
所以我们尽可能的把节点的左子树压入栈中,这样栈顶元素为最左侧节点
在pop出来后,将其右子节点push进去,这样的话就是按照中序遍历获取元素啦

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
java复制代码
public class Main {


public List<Integer> inorderTraversal(TreeNode root) {
List<Integer> res = new ArrayList<>();
if (root == null) {
return res;
}
Deque<TreeNode> stack = new LinkedList<>();
TreeNode cur = root;
while (!stack.isEmpty() || cur != null) {
while(cur != null) {
stack.push(cur);
cur = cur.left;
}
TreeNode node = stack.pop();
res.add(node.val);
if(node.right != null) {
cur = node.right;
}
}
return res;
}

}

class TreeNode {
int val;
TreeNode left;
TreeNode right;

TreeNode() {
}

TreeNode(int val) {
this.val = val;
}

TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}

在这里插入图片描述


二叉树后序遍历

  • 首先将根节点压栈
  • 因为先出栈的为根节点,其后先出右子节点,最后出左子节点
  • 将左子节点压栈
  • 将右子节点压栈
  • 因为出栈顺序为“根右左”,所以需要每次将元素插入list开头
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
java复制代码public class Main {


public List<Integer> postorderTraversal(TreeNode root) {
List<Integer> result = new ArrayList<Integer>();
if (root == null)
return result;
Deque<TreeNode> stack = new LinkedList<>();
stack.push(root);
while (!stack.isEmpty()) {
TreeNode node = stack.pop();
if (node.left != null)
stack.push(node.left);
if (node.right != null) {
stack.push(node.right);
}
result.add(0, node.val);
}
return result;

}
}

class TreeNode {
int val;
TreeNode left;
TreeNode right;

TreeNode() {
}

TreeNode(int val) {
this.val = val;
}

TreeNode(int val, TreeNode left, TreeNode right) {
this.val = val;
this.left = left;
this.right = right;
}
}

💖最后

我是 Code皮皮虾,一个热爱分享知识的 皮皮虾爱好者,未来的日子里会不断更新出对大家有益的博文,期待大家的关注!!!

创作不易,如果这篇博文对各位有帮助,希望各位小伙伴可以一键三连哦!,感谢支持,我们下次再见~


一键三连.png

本文转载自: 掘金

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

spring的Autowired报红了,

发表于 2021-10-10

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

摘要:

aa.jpg

我们在使用@Autowired注解的时候,很常见的就会报以上这个,到底是啥原因呢,还有怎么解决呢,那就康康这篇文章吧!!

话不多说,直接上硬菜!!!

可选的自动装配

默认情况下,@Autowired具有强契约性,也就是说其所标注的属性或者参数必须是可装配的,如果没有Bean可以装配到@Autowired所标注的属性或参数中,自动装配就会失败,抛出NoSuchBeanDefinitionException异常,

两种情况:

①:属性不一定非要装配,null值也可以

在这种场景下,可以通过设置@Autowired的required属性为false来配置自动装配是可选的,如下:

1
2
java复制代码@Autowired(required = false)
private Instrument instrument;

在这里,spring姜尝试装配instrument属性,但是如果没有查找到与之匹配的类型为Insrument的bean,应用不会发生任何问题,而instrument属性的值会设置为null。

注意required属性可以用于@Autowired注解所使用的任何地方,但是当好似用构造器装配时,只有一个构造器可以将@Autowired的required属性设置为true,其他使用该注解所标注的构造器只能将required属性设置为false,此外,当时用@Autowired标注多个构造器时,spring就会从所有满足装配条件的构造器中选择入参最多的那个构造器

②spring不缺少适合装配的bean

这种场景的问题在于spring中可能会有多个符合装配条件的Bean(最少2个),并且都可以被装配到属性或者参数中,在这个时候,为了帮助spring认清渣女(bushi),选中善良美丽的老婆(需要的Bean),便有一个新的注解搭配应运而生,那就是@Qualifier注解。

例如:为了确保spring为 《告白气球》 bean的前奏要用吉他(guitar)演奏,即使有其他的乐器bean也可以用来演奏这个前奏,但是我们在这里就可以使用@Qulifier注解来明确指出使用吉他(指定Bean):

1
2
3
java复制代码@Autowired
@Qualifire("guitar")
private Instrument instrument;

如上在注入时将会去尝试注入id为guitar的Bean;

使用@Qualifire意味着把@Autowired的byType自动装备转换为显示的byName装配,而如上的例子就是通过指定Bean的id来缩小自动装配候选Bean的范围,缩小的只剩下了一个;

而除了通过Bean的id来缩小范围之外,我们还可以通过在Bean上直接使用qualifier来缩小范围,例如,假设吉他(guitar)的Bean时使用如下的xml进行声明的

1
2
3
xml复制代码<bean class = "com.springinaction.beanxml.Guitar">
<qualifier value = "stringed"></qualifier>
</bean>

这里的元素限定了吉他是一个Stringed(弦乐器),除了可以在xml中指定qualifier,还可以使用@Qualifire注解来标注guitar类:

1
2
3
4
java复制代码@Qualifier("Stringed")
public class Guitar implements Instrument{
   
}

注:关系@Qualifier注解,我们下一篇文章见吧(手动狗头)

本文转载自: 掘金

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

一番瞎操作,Kubesphere 也能通过gitlab登录了

发表于 2021-10-10

  最近接到了一个需求,技术团队迫切的希望开发过程中用到的林林总总的开源系统能够实现统一登录,免去频繁切换账户或忘记密码之苦。”恩。。就你了”,看着Kubesphere挺顺眼,就先拿你动刀吧。

  翻看kubesphere的官网文档,果然不负所望,kubesphere3.1是支持oauth2登录功能的,并且已经内置了github和阿里云IDaas的插件,只需在ks-apiserver的配置文件中启用相应的oauth配置,即可实现;但是如果需要gitlab登录,就需要开发相应的插件了,这样的话,似乎就违背了低成本方便快捷实现的初衷了

  转念一想,既然gitlab和github”同宗同姓”,那么github的配置是不是也可同样应用在gitlab上?于是乎投机取巧的使用github的配置代替,几番折腾下来,最终还是以失败收场,看来只能老老实实的开发插件了

下载kubesphere源码,认证插件目录位置 /pkg/apiserver/authentication/identityprovider/

接口定义


kubesphere官方已对OAuth2认证逻辑进行了抽象和封装,按照官方指引,provider只需要实现几个约定的接口即可

image.png

接口实现


kubesphere官方本身已经封装好了oauth验证逻辑,所以对于站在巨人肩上的开发者而言,只需要重新定义一个结构体gitlabIdentity,用以反序列化gitlab user api的用户信息

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
golang复制代码package gitlab

import (
    "context"
    "crypto/tls"
    "encoding/json"
    "io/ioutil"
    "net/http"
    "time"

    "github.com/mitchellh/mapstructure"
    "golang.org/x/oauth2"

    "kubesphere.io/kubesphere/pkg/apiserver/authentication/identityprovider"
    "kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
)

// 初始化过程中,注册oauth Provider
func init() {
    identityprovider.RegisterOAuthProvider(&gitlabProviderFactory{})
}

type gitlab struct {
    // ClientID is the application's ID.
    ClientID string `json:"clientID" yaml:"clientID"`

    // ClientSecret is the application's secret.
    ClientSecret string `json:"-" yaml:"clientSecret"`

    // Endpoint contains the resource server's token endpoint
    // URLs. These are constants specific to each server and are
    // often available via site-specific packages, such as
    // google.Endpoint or gitlab.endpoint.
    Endpoint endpoint `json:"endpoint" yaml:"endpoint"`

    // RedirectURL is the URL to redirect users going through
    // the OAuth flow, after the resource owner's URLs.
    RedirectURL string `json:"redirectURL" yaml:"redirectURL"`

    // Used to turn off TLS certificate checks
    InsecureSkipVerify bool `json:"insecureSkipVerify" yaml:"insecureSkipVerify"`

    // Scope specifies optional requested permissions.
    Scopes []string `json:"scopes" yaml:"scopes"`

    Config *oauth2.Config `json:"-" yaml:"-"`
}

// endpoint represents an OAuth 2.0 provider's authorization and token
// endpoint URLs.
type endpoint struct {
    AuthURL     string `json:"authURL" yaml:"authURL"`
    TokenURL    string `json:"tokenURL" yaml:"tokenURL"`
    UserInfoURL string `json:"userInfoURL" yaml:"userInfoURL"`
}

type Identity struct {
    Provider string `json:"provider"`
    ExternUid string `json:"extern_uid"`
}

// 根据gitab user api接口返回字段定义结构体
type gitlabIdentity struct {
    ID                int       `json:"id"`
    UserName       string    `json:"username"`
    Email             string    `json:"email"`
    Name              string    `json:"name"`
    State             string     `json:"state"`
    AvatarURL         string    `json:"avatar_url"`
    WEBURL            string    `json:"web_url"`
    CreatedAt         time.Time `json:"created_at"`
    IsAdmin           bool      `json:"is_admin"`
    Bio               string    `json:"bio"` 
    Location          string    `json:"location"` 
    Skype             string    `json:"skype"` 
    LINKEDIN          string    `json:"linkedin"`
    TWITTER           string    `json:"twitter"`
    WebsiteURL        string    `json:"website_url"`
    ORGANIZATION      string    `json:"organization"`
    LastSignInAt      time.Time `json:"last_sign_in_at"`
    ConfirmedAt       time.Time `json:"confirmed_at"`
    ThemeID           int       `json:"theme_id"`
    ColorSchemeID     int     `json:"color_scheme_id"`
    ProjectsLimits    int       `json:"projects_limit"`
    CurrentSignInAt   time.Time `json:"current_sign_in_at"`
    CanCreateGroup    bool      `json:"can_create_group"`
    CanCreateProject  bool      `json:"can_create_project"`
    TwoFactorEnabled  bool      `json:"two_factor_enabled"`
    External          bool      `json:"external"`
    Identities        []Identity `json:"identities"`
}

type gitlabProviderFactory struct {
}

func (g *gitlabProviderFactory) Type() string {
    return "GitLabIdentityProvider"
}

func (g *gitlabProviderFactory) Create(options oauth.DynamicOptions) (identityprovider.OAuthProvider, error) {
    var gitlab gitlab
    if err := mapstructure.Decode(options, &gitlab); err != nil {
        return nil, err
    }

    // fixed options
    options["endpoint"] = oauth.DynamicOptions{
        "authURL":     gitlab.Endpoint.AuthURL,
        "tokenURL":    gitlab.Endpoint.TokenURL,
        "userInfoURL": gitlab.Endpoint.UserInfoURL,
    }
    gitlab.Config = &oauth2.Config{
        ClientID:     gitlab.ClientID,
        ClientSecret: gitlab.ClientSecret,
        Endpoint: oauth2.Endpoint{
            AuthURL:  gitlab.Endpoint.AuthURL,
            TokenURL: gitlab.Endpoint.TokenURL,
        },
        RedirectURL: gitlab.RedirectURL,
        Scopes:      gitlab.Scopes,
    }
    return &gitlab, nil
}

func (g gitlabIdentity) GetUserID() string {
    return g.UserName
}

func (g gitlabIdentity) GetUsername() string {
    return g.UserName
}

func (g gitlabIdentity) GetEmail() string {
    return g.Email
}

// 请求oauth2服务端,反序列化用户信息
func (g *gitlab) IdentityExchange(code string) (identityprovider.Identity, error) {
    ctx := context.TODO()
    if g.InsecureSkipVerify {
        client := &http.Client{
            Transport: &http.Transport{
                TLSClientConfig: &tls.Config{
                    InsecureSkipVerify: true,
                },
            },
        }
        ctx = context.WithValue(ctx, oauth2.HTTPClient, client)
    }
    token, err := g.Config.Exchange(ctx, code)
    if err != nil {
        return nil, err
    }
    resp, err := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token)).Get(g.Endpoint.UserInfoURL)
    if err != nil {
        return nil, err
    }

    data, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var gitlabIdentity gitlabIdentity
    err = json.Unmarshal(data, &gitlabIdentity)
    if err != nil {
        return nil, err
    }

    return gitlabIdentity, nil
}

改动源码后,按照kubesphere的二次开发文档重新编译生成镜像并部署

配置文件


相对应的,ks-apiserver的配置文件kubesphere-config也要启用gitlab插件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
golang复制代码    kubesphere.yaml: |
authentication:
authenticateRateLimiterMaxTries: 10
authenticateRateLimiterDuration: 10m0s
jwtSecret: "xxxxxxxxxxxxxxxxx"
oauthOptions:
accessTokenMaxAge: 1h
accessTokenInactivityTimeout: 30m
identityProviders:
- name: gitlab
type: GitLabIdentityProvider
mappingMethod: auto
provider:
clientID: 'xxxxxxxxxxxxxxxx'
clientSecret: 'xxxxxxxxxxxx'
endpoint:
# xxx gitlab地址
authURL: 'http://xxx/oauth/authorize'
tokenURL: 'http://xxx/oauth/token'
userInfoURL: 'http://xxx/api/v4/user'
redirectURL: 'http://xxx/oauth/redirect/gitlab'
scopes:
- read_user

最终效果

image.png

真香!再也不需要不胜其烦的为技术人员创建账户、找回密码了,攒下来的时间足够喝几杯咖啡了🤔

文章均为原创,关注公众号云猿生\color{green} {云猿生} 云猿生获取更多知识

image.png

本文转载自: 掘金

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

阿里限流神器Sentinel夺命连环 17 问?

发表于 2021-10-09

作者:bucaichenmou

链接:www.cnblogs.com/cbvlog/p/15…

1、前言

这是《spring Cloud 进阶》专栏的第五篇文章,这篇文章介绍一下阿里开源的流量防卫兵Sentinel,一款非常优秀的开源项目,经过近10年的双十一的考验,非常成熟的一款产品。往期文章如下:

  • 五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?
  • openFeign夺命连环9问,这谁受得了?
  • 阿里面试这样问:Nacos、Apollo、Config配置中心如何选型?这10个维度告诉你!
  • 阿里面试败北:5种微服务注册中心如何选型?这几个维度告诉你!

文章目录如下:

2、什么是sentinel?

sentinel顾名思义:卫兵;在Redis中叫做哨兵,用于监控主从切换,但是在微服务中叫做流量防卫兵。

Sentinel 以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。

Sentinel 具有以下特征:

  • 丰富的应用场景:Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景,例如秒杀(即突发流量控制在系统容量可以承受的范围)、消息削峰填谷、集群流量控制、实时熔断下游不可用应用等。
  • 完备的实时监控:Sentinel 同时提供实时的监控功能。您可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况。
  • 广泛的开源生态:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Apache Dubbo、gRPC、Quarkus 的整合。您只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel。同时 Sentinel 提供 Java/Go/C++ 等多语言的原生实现。
  • 完善的 SPI 扩展机制:Sentinel 提供简单易用、完善的 SPI 扩展接口。您可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等。

Sentinel 的主要特性如下图:

Sentinel 分为两个部分:

  • 核心库(Java 客户端)不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持。
  • 控制台(Dashboard)基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器。

总之一句话:sentinel真牛逼,完爆Hystrix………

3、sentinel和Hystrix有何区别?

不多说了,总之一句话:Hystrix赶紧放弃,用sentinel……

具体区别如下图:

4、sentinel版本如何选择?

由于陈某写的是Spring Cloud 进阶一个系列,使用的聚合项目,因此版本还是保持和之前文章一样,不清楚的可以看这篇:五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?

这里选择的spring-cloud-alibaba-dependencies的版本是2.2.1.RELEASE,因此sentinel版本选择1.7.1,大家可以根据自己的版本选择对应sentinel的版本,版本对应关系如下图:

注意:一定要按照官方推荐的版本适配,否则出现意想不到的BUG追悔莫及………

5、Sentinel 控制台如何安装?

sentinel和nacos一样,都有一个控制台,但是这里不用自己手动搭建一个微服务,官方已经搭建好了,只需要下载对应得jar包运行即可。下载地址:github.com/alibaba/Sen…

选择对应得版本下载即可,我这里选择1.7.1版本,下载的jar包如下图:

当然你可以通过源码构建:mvn clean package

注意:JDK版本必须>=1.8

此时我们只需要运行这个jar包即可,命令如下:

1
ini复制代码java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard-1.7.1.jar

上述参数含义如下:

  • -Dserver.port:指定启动的端口,默认8080
  • -Dproject.name:指定本服务的名称
  • -Dcsp.sentinel.dashboard.server:指定sentinel控制台的地址,用于将自己注册进入实现监控自己

启动成功之后,浏览器访问:http://localhost:8080,登录页面如下图:

默认的用户名和密码:sentinel/sentinel

登录成功之后页面如下:

可以看到目前只有一个服务sentinel-dashboard被监控了,这个服务就是自己。

注意:上述参数都是可选的,没必要可以不填。

那么问题来了:默认的用户名和密码在生产环境上肯定不能用,如何修改呢?

从 Sentinel 1.6.0 起sentinel已经支持自定义用户名和密码了,只需要在执行jar命令时指定即可,命令如下:

1
ini复制代码java -Dsentinel.dashboard.auth.username=admin -Dsentinel.dashboard.auth.password=123 -jar sentinel-dashboard-1.7.1.jar

用户可以通过如下参数进行配置:

  • -Dsentinel.dashboard.auth.username=sentinel 用于指定控制台的登录用户名为 sentinel;
  • -Dsentinel.dashboard.auth.password=123456 用于指定控制台的登录密码为 123456;如果省略这两个参数,默认用户和密码均为 sentinel;
  • -Dserver.servlet.session.timeout=7200 用于指定 Spring Boot 服务端 session 的过期时间,如 7200 表示 7200 秒;60m 表示 60 分钟,默认为 30 分钟;

注意:部署多台控制台时,session 默认不会在各实例之间共享,这一块需要自行改造。

除了用户名密码相关的配置,sentinel控制台还提供了其他的可配置选项,如下图:

6、微服务如何接入sentinel控制台?

微服务为什么要集成sentinel控制台,sentinel不是提供了相关的API吗?

其实Spring Boot 官方一直提倡约定>配置>编码的规则,能够不硬编码何乐而不为呢?

因此本文后续内容主要还是结合sentinel控制台进行讲解,关于API的使用大家可以按照官方文档学习,讲解的非常清楚。

好了,言归正传,微服务如何接入sentinel控制台呢?

1、新建微服务模块注册进入Nacos

这里的注册中心依然使用的是nacos,有不会用的请看专栏第一篇Nacos文章:五十五张图告诉你微服务的灵魂摆渡者Nacos究竟有多强?

新建一个微服务模块:sentinel-service9008,相关代码不贴出了。

相关配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码server:
port: 9008
spring:
application:
## 指定服务名称,在nacos中的名字
name: sentinel-service
cloud:
nacos:
discovery:
# nacos的服务地址,nacos-server中IP地址:端口号
server-addr: 127.0.0.1:8848
management:
endpoints:
web:
exposure:
## yml文件中存在特殊字符,必须用单引号包含,否则启动报错
include: '*'

源码全部会上传,获取方式看文末!

2、添加依赖

除了Nacos的依赖,还需要添加一个sentinel的依赖:

1
2
3
4
5
xml复制代码<!--sentinel的依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

以上只贴出了sentinel相关依赖,nacos依赖不再贴了,见源码!

3、添加配置集成控制台

只需要添加如下配置即可集成sentinel控制台:

1
2
3
4
5
6
yaml复制代码spring:
cloud:
sentinel:
transport:
## 指定控制台的地址,默认端口8080
dashboard: localhost:8080

4、新建一个测试接口

下面新建一个测试接口,用于测试相关规则,如下:

1
2
3
4
5
6
7
8
9
less复制代码@RestController
@RequestMapping("/sentinel")
public class FlowLimitController {

@GetMapping("/test")
public String test(){
return "接收到一条消息--------";
}
}

5、启动微服务

启动9008这个微服务,然后浏览器输入:http://localhost:9008/sentinel/test,此时查看sentinel控制台,将会看见sentinel-service这个服务已经被监控了,如下图:

注意:sentinel是懒加载机制,只有访问过一次的资源才会被监控。

不过可以通过配置关闭懒加载,在项目启动时就连接sentinel控制台,配置如下:

1
2
3
4
yaml复制代码spring:
sentinel:
# 取消控制台懒加载,项目启动即连接Sentinel
eager: true

7、流量控制如何配置?

流量控制(flow control),其原理是监控应用流量的 QPS 或并发线程数等指标,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性。

QPS:每秒请求数,即在不断向服务器发送请求的情况下,服务器每秒能够处理的请求数量。

并发线程数:指的是施压机施加的同时请求的线程数量。

同一个资源可以创建多条限流规则,一条限流规则由以下元素组成:

  • resource:资源名,即限流规则的作用对象。
  • count: 限流阈值
  • grade:限流阈值类型(1:QPS 0:并发线程数),默认值QPS
  • limitApp:流控针对的调用来源,若为 default 则不区分调用来源,默认值default
  • strategy:判断的根据是资源自身**(0),还是根据其它关联资源 **(1),还是根据链路入口*(2)\*,默认值根据资源本身。
  • controlBehavior: 流控效果(直接拒绝(0) / 排队等待(2) / 预热冷启动(1)),默认值直接拒绝。

以上元素限流元素对应的类是com.alibaba.csp.sentinel.slots.block.flow.FlowRule,各元素如下图:

注意:各个元素的取值以及默认值一定要记住,后续配置将会用到。

以上几个元素在sentinel控制台对应规则如下图:

1、三种流控效果

流控效果总共分为三种,对应元素controlBehavior,分别如下:

快速失败

默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。

warm up

即预热/冷启动方式。当系统长期处于低水位的情况下,当流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过”冷启动”,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

注意:这一效果只针对QPS流控,并发线程数流控不支持。

预热底层是根据令牌桶算法实现的,源码对应得类在com.alibaba.csp.sentinel.slots.block.flow.controller.WarmUpController。

算法中有一个冷却因子coldFactor,默认值是3,即请求 QPS 从 threshold(阈值) / 3 开始,经预热时长逐渐升至设定的 QPS 阈值。

比如设定QPS阈值为3,流控效果为warm up,预热时长为5秒,如下图:

这样配置之后有什么效果呢:QPS起初会从(3/3/=1)每秒通过一次请求开始预热直到5秒之后达到每秒通过3次请求。动态效果图如下:

从上述动画可以清楚的看见:前几秒是频繁流控的,直到5秒,QPS阈值达到了3。

具体算法原理请看:github.com/alibaba/Sen…

排队等待

匀速排队方式会严格控制请求通过的间隔时间,也即是让请求以均匀的速度通过,对应的是漏桶算法。源码对应得类:com.alibaba.csp.sentinel.slots.block.flow.controller.RateLimiterController

注意:这一效果只针对QPS流控,并发线程数流控不支持。

简单举个栗子:你去大学食堂吃饭,只有一个阿姨在打饭,那么所有人都要排队打饭,每次只有一个人打到饭,其他人都在排队等待。

不同的是sentinel有个超时等待时间,一旦超过这个预定设置的时间将会被限流。

该方式作用如下图:

这种方式适合用于请求以突刺状来到,这个时候我们不希望一下子把所有的请求都通过,这样可能会把系统压垮;同时我们也期待系统以稳定的速度,逐步处理这些请求,以起到“削峰填谷”的效果,而不是拒绝所有请求。

比如设置QPS阈值为1,超时等待时间为10000毫秒,如下图:

此时的效果如下:

从上图可以看到:连续点击刷新请求,虽然设置了QPS阈值为1,但是并没有被限流,而是在等待,因为设置了超时等待时间为10秒。

具体算法原理请看:github.com/alibaba/Sen…

2、三种流控模式

流控模式总共分为三种,对应元素strategy,分别如下:

  • 直接拒绝:接口达到限流条件时,直接限流
  • 关联:当关联的资源达到阈值时,就限流自己
  • 链路:只记录指定链路上的流量(指定资源从入口资源进来的流量,如果达到阈值,就可以限流)

下面来详细介绍下以上三种流控模式。

直接拒绝

顾名思义:默认的流量控制方式,当QPS超过任意规则的阈值后,新的请求就会被立即拒绝,拒绝方式为抛出FlowException。上面的几个例子都是配置了直接拒绝这个模式,这里不再详细介绍。

关联

典型的使用场景:一个是支付接口,一个是下单接口,此时一旦支付接口达到了阈值,那么订单接口就应该被限流,不然这边还在下单,消费者等待或者直接被拒绝支付将会极大的影响用户体验。

简而言之:A关联B,一旦B达到阈值,则A被限流

演示一下效果,创建以下两个接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
kotlin复制代码@RestController
@RequestMapping("/sentinel")
public class FlowLimitController {

/**
* 下单接口
* @return
*/
@GetMapping("/order")
public String order() {
return "下单成功..........";
}

/**
* 支付接口
* @return
*/
@GetMapping("/pay")
public String pay() {
return "支付成功..........";
}
}

此时的流控规则配置如下图:

注意:关联之后,这里设置的限流规则是对被关联资源,也就是/sentinel/pay这个资源,但是真正被限流则是/sentinel/order。

如何演示效果呢?很简单,只需要不断的请求/sentinel/pay达到阈值,然后在请求/sentinel/order。

利用POSTMAN不断向/sentinel/pay发出请求,然后浏览器请求/sentinel/order,结果如下图:

可以看到订单接口被限流了………….

3、两种统计类型

流控分为两种统计类型,分别是QPS,并发线程数,很多人不太明白这两种统计类型有什么区别?

举个栗子:陈某带了一个亿去银行存钱,但是银行大门保安要查健康码,每秒最多只能同时进入4个人,并且银行中只有两个工作人员工作,如下图:

此时的QPS含义:从保安到银行这一段,即是保安放行进入银行的人数。

此时并发线程数的含义:银行只有两个工作人员在工作,那么最多只能同时处理两个任务,这里并发线程数的阈值就是2。

8、降级规则如何配置?

熔断降级在日常生活中也是比较常见的,场景如下:

  • 股票市场的熔断,当价格触发到了熔点之后,会暂停交易一段时间,或者交易可以继续进行,但是报价会限制在一定的范围。
  • 电压过高导致保险丝触发熔断保护

在大型的分布式系统中,一个请求的依赖如下图:

如果这个时候,某个服务出现一些异常,比如:

  • 服务提供者不可用(硬件故障、程序bug、网络故障、用户请求量较大)
  • 重试导致的流量过大
  • 服务调用者使用同步调用,产生大量的等待线程占用系统资源,一旦线程资源被耗尽,调用者提供的服务也会变成不可用状态

那么将会导致整个服务不可用,用古话来讲就是:千里之堤毁于蚁穴。

所谓编程源于生活,架构师们根据生活的经验设计出了服务的熔断降级策略,很好的解决了这类问题。

熔断降级规则对应sentinel控制台的降级规则这一栏,如下图:

熔断降级涉及到的几个属性如下表:

源码中对应得类为:com.alibaba.csp.sentinel.slots.block.degrade.DegradeRule。

三种熔断策略

Sentinel 提供以下几种熔断策略:

  1. 平均响应时间 (DEGRADE_GRADE_RT):当 1s 内持续进入 5 个请求,对应时刻的平均响应时间(秒级)均超过阈值(count,以 ms 为单位),那么在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地熔断(抛出 DegradeException)。注意 Sentinel 默认统计的 RT 上限是 4900 ms,超出此阈值的都会算作 4900 ms,若需要变更此上限可以通过启动配置项 -Dcsp.sentinel.statistic.max.rt=xxx 来配置。
  2. 异常比例 (DEGRADE_GRADE_EXCEPTION_RATIO):当资源的每秒请求量 >= 5,并且每秒异常总数占通过量的比值超过阈值(DegradeRule 中的 count)之后,资源进入降级状态,即在接下的时间窗口(DegradeRule 中的 timeWindow,以 s 为单位)之内,对这个方法的调用都会自动地返回。异常比率的阈值范围是 [0.0, 1.0],代表 0% - 100%。
  3. 异常数 (DEGRADE_GRADE_EXCEPTION_COUNT):当资源近 1 分钟的异常数目超过阈值之后会进行熔断。注意由于统计时间窗口是分钟级别的,若 timeWindow 小于 60s,则结束熔断状态后仍可能再进入熔断状态。

下面演示一个平均响应时间熔断,创建一个接口,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@RestController
@RequestMapping("/sentinel/provider")
@Slf4j
public class FlowLimitController {

@GetMapping("/test")
public String test() throws InterruptedException {
//休眠3秒钟
Thread.sleep(3000);
log.info("收到一条消息----test");
return "接收到一条消息--------";
}
}

在控台为这个接口设置平均响应时间为200毫秒,时间窗口为1秒,大致意思:平均的响应时间大于200毫秒之后,在接下来的1秒时间内将会直接熔断,如下图:

使用Jmeter开启10个线程循环跑,然后在浏览器中访问这个接口,返回结果如下图:

为什么呢?由于的接口中休眠了3秒,平均响应时间肯定大于200毫秒,因此直接被熔断了。

注意:这里熔断后直接返回默认的信息,后面会介绍如何定制熔断返回信息。

9、热点参数如何限流?

顾名思义:热点就是经常访问的数据,很多时候肯定是希望统计某个访问频次Top K数据并对其进行限流。

比如秒杀系统中的商品ID,对于热点商品那一瞬间的并发量是非常可怕的,因此必须要对其进行限流。

Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。

注意:热点参数限流只针对QPS。

官方文档:github.com/alibaba/Sen…

概念理解了,来看下sentinel控制台如何设置热点参数限流,如下图:

规则对应得源码在com.alibaba.csp.sentinel.slots.block.flow.param.ParamFlowRule这个类中,各种属性含义如下图:

规则都懂了,下面我们通过实战来演示一下热点参数到底是如何限流的。

注意:热点参数限流只作用于八大基本类型。

1、创建一个资源

现在先创建一个service,用@SentinelResource这个注解定义一个资源,这个注解后续将会详细介绍,先忽略,代码如下:

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
less复制代码@Service
@Slf4j
public class FlowServiceImpl implements FlowService {

/**
* @SentinelResource的value属性指定了资源名,一定要唯一
* blockHandler属性指定了兜底方法
*/
@Override
@SentinelResource(value = "OrderQuery",blockHandler = "handlerQuery")
public String query(String p1, String p2) {
log.info("查询商品,p1:{},p2:{}",p1,p2);
return "查询商品:success";
}

/**
* 对应得兜底方法,一旦被限流将会调用这个方法来处理
*/
public String handlerQuery(@RequestParam(value = "p1",required = false) String p1,
@RequestParam(value = "p2",required = false)String p2,
BlockException exception){
log.info("查询商品,p1:{},p2:{}",p1,p2);
return "查询商品:熔断了......";
}
}

上述代码什么意思呢?如下:

  • 如果query这个接口没有被限流则返回:查询商品:success
  • 如果query这个接口被限流了,则进入了兜底方法handlerQuery方法,返回:查询商品:熔断了……

2、创建controller接口

下面创建一个controller进行测试,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码@RestController
@RequestMapping("/sentinel/provider")
@Slf4j
public class FlowLimitController {
@Autowired
private FlowService flowService;

@GetMapping("/order/query")
public String query(@RequestParam(value = "p1",required = false) String p1, @RequestParam(value = "p2",required = false)String p2){
return flowService.query(p1,p2);
}

}

可以看到接口中有两个参数,分别是p1、p2。

3、添加热点参数限流规则

在sentinel控制台点击热点规则->新增热点限流规则,添加如下图规则:

上述配置的具体含义:当OrderQuery这个资源中的第0个参数QPS超过1秒1次将会被限流。这里参数索引是从0开始,第0个就是对应接口中的p1这个参数。

第一个测试:浏览器直接访问:http://localhost:9009/sentinel/provider/order/query?p1=22&p2=1222,连续点击将会看到这个接口被熔断降级了,如下图:

这也正是验证了上述的热点参数限流配置。

第二个测试:浏览器输入:http://localhost:9009/sentinel/provider/order/query?p2=1222,连续点击将会看到这个接口并没有被熔断降级,如下图:

注意:对于热点参数限流,只有包含指定索引的参数请求才会被限流,否则不影响。

此时产品说:ID为100的这个产品点击量太少了,你们赶紧调整下这个商品的限流规则。这个时候该怎么办呢?

别着急,sentinel显然考虑到了这一点,提供了参数例外项这项配置,针对产品需求配置如下:

从上图配置中,我们将参数值p1这个参数值等于100的时候,限流阈值设置成了100,也就是说p1=100这个请求QPS放宽到1秒请求100次以上才会被限流。

验证:浏览器输入地址:http://localhost:9009/sentinel/provider/order/query?p1=100,无论点击多么快,都没有被熔断降级,显然是配置生效了,如下图:

以上源码在sentinel-openfeign-provider9009这个模块中,文末有源码获取方式。

10、系统自适应如何限流?

前面热点参数、普通流量限流都是针对的某个接口,这里系统自适应限流针对是整个系统的入口流量,从单台机器的 load、CPU 使用率、平均 RT、入口 QPS 和并发线程数等几个维度监控应用指标,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。

sentinel控制台对应如下图:

阈值类型有五种,分别如下:

  • Load 自适应(仅对 Linux/Unix-like 机器生效):系统的 load1 作为启发指标,进行自适应系统保护。当系统 load1 超过设定的启发值,且系统当前的并发线程数超过估算的系统容量时才会触发系统保护(BBR 阶段)。系统容量由系统的 maxQps * minRt 估算得出。设定参考值一般是 CPU cores * 2.5。
  • CPU usage(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0),比较灵敏。
  • 平均 RT:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。
  • 并发线程数:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。
  • 入口 QPS:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。

官方文档:github.com/alibaba/Sen…

系统规则的配置比较简单,这里以入口QPS为例进行演示,为了演示真实情况,清掉所有的限流规则,添加系统规则,如下图:

这个QPS系统规则一配置,该微服务中的所有接口都将会被这个规则限制,比如访问:http://localhost:9009/sentinel/provider/pay,连续点击,如下图:

可以看到已经被限流了,不仅是这个接口,所有接口都会生效。

注意:系统规则中的入口QPS这个规则不建议配置,一旦配置上了可能导致整个服务不可用。

11、如何自定义限流返回的异常信息?

在前面的例子中,无论是熔断降级还是被限流返回的异常信息都是Blocked by Sentinel (flow limiting),这个是Sentinel默认的异常信息。

很显然默认的异常信息并不能满足我们的业务需求,因此我们需要根据前后端规则制定自己的异常返回信息。

这里将会用到一个注解@SentinelResource,这个在上文也是提到过,这个注解中有两个关于限流兜底方法的属性,如下:

  • blockHandler: 对应处理 BlockException 的函数名称。blockHandler 函数访问范围需要是 public,返回类型需要与原方法相匹配,参数类型需要和原方法相匹配并且最后加一个额外的参数,类型为 BlockException。blockHandler 函数默认需要和原方法在同一个类中。
  • blockHandlerClass:指定 blockHandlerClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。

官方文档:github.com/alibaba/Sen…

使用@SentinelResource注解自定义一个限流异常返回信息,先自定义一个资源,指定兜底方法为handler,代码如下:

第二步:写个对应得兜底方法,必须在同一个类中,代码如下:

第三步:对资源QueryOrder新增一个限流规则,如下图:

第四步:写个controller,代码就不晒了,自己写吧,哈哈。。。。

第五步:调用接口,疯狂点击,将会出现兜底方法中定义的返回信息,如下图:

到这儿基本算是成功了,但是有个问题:兜底方法必须要和业务方法放在同一个类中,这样代码耦合度不是很高吗?

@SentinelResource提供一个属性blockHandlerClass,完美的解决了这一个问题,能够将兜底方法单独放在一个类中,下面来介绍一下。

第一步:新建一个单独的类CommonHandler来放置兜底方法,代码如下:

第二步:在@SentinelResource注解中指定blockHandlerClass为上面的类,blockHandler指定兜底方法名,代码如下:

好了,至此就完成了,自己照着试试吧…….

上述源码在sentinel-openfeign-provider9009这个模块中,源码获取方式见文末。

12、如何对异常进行降级处理?

程序员每天都在制造BUG,没有完美的代码,也没有完美的程序员,针对代码的运行时异常我们无法避免,但是我们可以当出现异常的时候进行捕获并做出相应的处理,我们称之为降级处理。

异常的降级还是要用到@SentinelResource注解,其中相关的几个属性如下:

  • fallback:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑。fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。fallback 函数签名和位置要求:
    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • fallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象
  • fallbackClass:指定对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • defaultFallback(since 1.6.0):默认的 fallback 函数名称,可选项,通常用于通用的 fallback 逻辑(即可以用于很多服务或方法)。默认 fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理。若同时配置了 fallback 和 defaultFallback,则只有 fallback 会生效。defaultFallback 函数签名要求:
    • 返回值类型必须与原函数返回值类型一致;
    • 方法参数列表需要为空,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常。
    • defaultFallback 函数默认需要和原方法在同一个类中。若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象,注意对应的函数必需为 static 函数,否则无法解析。
  • exceptionsToIgnore(since 1.6.0):用于指定哪些异常被排除掉,不会计入异常统计中,也不会进入 fallback 逻辑中,而是会原样抛出。

1.8.0 版本开始,defaultFallback 支持在类级别进行配置。

注:1.6.0 之前的版本 fallback 函数只针对降级异常(DegradeException)进行处理,不能针对业务异常进行处理。

官方文档:github.com/alibaba/Sen…

下面定义一个创建订单的接口,手动制造一个1/0异常,代码如下:

上述接口并没有进行异常降级处理,因此调用该接口直接返回了异常信息,非常不友好,如下图:

我们可以使用fallback指定异常降级的兜底方法,此时业务方法改造如下:

使用fallbackClass属性指定单独一个类处理异常降级,降低了代码的耦合度,fallback属性指定了降级兜底的方法,代码如下:

此时再次访问接口,虽然有异常,但是返回的确实降级兜底方法中的返回信息,如下图:

到了这里基本满足了异常降级的处理需求,但是仍然有个疑问:能否只用一个方法处理全部的异常?

答案是:能,必须能,此时就要用到defaultFallback 这个属性了,指定默认的降级兜底方法,此时的业务方法变成如下代码:

defaultFallback属性指定了默认的降级兜底方法,这个方法代码如下:

好了,异常降级处理到这儿已经介绍完了,但是仍然有一个问题:若 blockHandler 和 fallback 都进行了配置,那么哪个会生效?

结论:若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 BlockException 时只会进入 blockHandler 处理逻辑。若未配置 blockHandler、fallback 和 defaultFallback,则被限流降级时会将 BlockException 直接抛出。

将createOrder这个业务接口改造一下,同时指定blockHandler和fallback,代码如下:

此时不配置任何规则,直接访问接口,可以看到这里直接进入了异常降级处理,如下图:

我们对createOrder这个资源配置降级规则:60秒内如果出现2个以上的异常直接限流,如下图:

此时我们再次访问这个接口,可以看到前两次直接进入了fallback指定的方法中(并未达到限流的异常数阈值),两次之后就被限流了,进入了blockHandler方法中,效果如下图:

上述源码在sentinel-openfeign-provider9009这个模块中,源码获取方式见文末。

13、sentinel的黑白名单如何设置?

顾名思义,黑名单就是拉黑呗,拉黑就是不能访问了呗,sentinel能够针对请求来源进行是否放行,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。

sentinel控制台对应得规则配置如下图:

该规则对应得源码为com.alibaba.csp.sentinel.slots.block.authority.AuthorityRule,几个属性如下:

  • resource:资源名,即限流规则的作用对象。
  • limitApp:对应的黑名单/白名单,不同 origin 用 , 分隔,如 appA,appB。
  • strategy:限制模式,AUTHORITY_WHITE 为白名单模式,AUTHORITY_BLACK 为黑名单模式,默认为白名单模式。

官方文档:github.com/alibaba/Sen…

这里有个问题:请求来源是什么,怎么获取?

Sentinel提供了一个接口RequestOriginParser,我们可以实现这个接口根据自己业务的规则解析出请求来源名称。

下面我以IP作为区分请求来源,代码如下:

然后将127.0.0.1设置为黑名单,如下图:

直接访问:http://127.0.0.1:9009/sentinel/rate/order/query?id=1002,结果如下图:

可以看到被限流了哦……………..

好了,黑白名单就介绍到这里。

上述源码在sentinel-openfeign-provider9009这个模块中,源码获取方式见文末。

14、限流规则如何持久化?

Sentinel默认限流规则是存储在内存中,只要服务重启之后对应得限流规则也会消失,实际的生产中肯定是不允许这种操作,因此限流规则的持久化迫在眉睫。

sentinel官方文档提供了两种持久化模式,分别如下:

但是官方推荐使用Push模式,下面陈某就Push模式介绍一下持久化限流规则。这里使用Nacos作为配置中心。

盗用官方一张架构图,如下:

1、添加依赖

这里需要添加一个依赖,如下:

1
2
3
4
xml复制代码<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

2、配置文件中配置相关信息

既然使用到了Nacos作为配置中心,肯定是要配置相关的地址、dataId…

在application.yml配置文件中添加如下配置:

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
yaml复制代码spring:
cloud:
sentinel:
## nacos持久化配置
datasource:
## 配置流控规则,名字任意
ds-flow:
nacos:
## nacos的地址
server-addr: 127.0.0.1:8848
## 配置ID
dataId: ${spring.application.name}-flow
## 配置分组,默认是DEFAULT_GROUP
groupId: DEFAULT_GROUP
## 配置存储的格式
data-type: json
## rule-type设置对应得规则类型,总共七大类型,在com.alibaba.cloud.sentinel.datasource.RuleType这个枚举类中有体现
rule-type: flow
## 配置降级规则,名字任意
ds-degrade:
nacos:
## nacos的地址
server-addr: 127.0.0.1:8848
## 配置ID
dataId: ${spring.application.name}-degrade
## 配置分组,默认是DEFAULT_GROUP
groupId: DEFAULT_GROUP
## 配置存储的格式
data-type: json
## rule-type设置对应得规则类型,总共七大类型,在com.alibaba.cloud.sentinel.datasource.RuleType这个枚举类中有体现
rule-type: degrade

上述配置仅仅展示了和持久化相关的一些配置,其他相关的配置代码就不贴了,稍后自己看源码。

spring.cloud.sentinel.datasource下可以配置多个规则,陈某这里只配置了限流和降级规则,其他规则自己尝试配一下,不同规则通过rule-type区分,其取值都在com.alibaba.cloud.sentinel.datasource.RuleType这个枚举类中,对应着sentinel中的几大统计规则。

3、在Nacos添加对应的规则配置

上述配置中对应的限流(flow)规则如下图:

上述配置中对应的降级(degrade)规则如下图:

先不纠结JSON数据里面到底是什么,先看效果,全部发布之后,Nacos中总共有了两个配置,如下图:

上图中可以看到我们的两种规则已经在Nacos配置好了,来看一下sentinel中是否已经生效了,如下图:

哦了,已经生效了,由于是push模式,只要nacos中点击发布配置,相关规则配置就会推送到sentinel中。

上述源码在sentinel-openfeign-provider9009这个模块中,源码获取方式见文末。

伏笔:push模式只能保证Nacos中的修改推送到sentinel控制台,**但是sentinel控制台的限流规则修改如何推送到Nacos呢?**别着急,下面将会介绍…………..

4、JSON中到底怎么写?

很多人好奇JOSN中的配置到底怎么写?其实很简单,陈某在介绍各种规则的时候都明确告诉你每种规则对应源码中的实现类,比如流控规则对应的类就是com.alibaba.csp.sentinel.slots.block.flow.FlowRule,JOSN中各个属性也是来源于这个类。

下面陈某列出各个规则的JSON配置,开发中照着改即可。

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
ruby复制代码[
{
// 资源名
"resource": "/test",
// 针对来源,若为 default 则不区分调用来源
"limitApp": "default",
// 限流阈值类型(1:QPS;0:并发线程数)
"grade": 1,
// 阈值
"count": 1,
// 是否是集群模式
"clusterMode": false,
// 流控效果(0:快速失败;1:Warm Up(预热模式);2:排队等待)
"controlBehavior": 0,
// 流控模式(0:直接;1:关联;2:链路)
"strategy": 0,
// 预热时间(秒,预热模式需要此参数)
"warmUpPeriodSec": 10,
// 超时时间(排队等待模式需要此参数)
"maxQueueingTimeMs": 500,
// 关联资源、入口资源(关联、链路模式)
"refResource": "rrr"
}
]

2、降级规则

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
json复制代码[
{
// 资源名
"resource": "/test1",
"limitApp": "default",
// 熔断策略(0:慢调用比例,1:异常比率,2:异常计数)
"grade": 0,
// 最大RT、比例阈值、异常数
"count": 200,
// 慢调用比例阈值,仅慢调用比例模式有效(1.8.0 引入)
"slowRatioThreshold": 0.2,
// 最小请求数
"minRequestAmount": 5,
// 当单位统计时长(类中默认1000)
"statIntervalMs": 1000,
// 熔断时长
"timeWindow": 10
}
]

3、热点规则

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
ruby复制代码[
{
// 资源名
"resource": "/test1",
// 限流模式(QPS 模式,不可更改)
"grade": 1,
// 参数索引
"paramIdx": 0,
// 单机阈值
"count": 13,
// 统计窗口时长
"durationInSec": 6,
// 是否集群 默认false
"clusterMode": 默认false,
//
"burstCount": 0,
// 集群模式配置
"clusterConfig": {
//
"fallbackToLocalWhenFail": true,
//
"flowId": 2,
//
"sampleCount": 10,
//
"thresholdType": 0,
//
"windowIntervalMs": 1000
},
// 流控效果(支持快速失败和匀速排队模式)
"controlBehavior": 0,
//
"limitApp": "default",
//
"maxQueueingTimeMs": 0,
// 高级选项
"paramFlowItemList": [
{
// 参数类型
"classType": "int",
// 限流阈值
"count": 222,
// 参数值
"object": "2"
}
]
}
]

4、系统规则

负值表示没有阈值检查。不需要删除参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码[
{
// RT
"avgRt": 1,
// CPU 使用率
"highestCpuUsage": -1,
// LOAD
"highestSystemLoad": -1,
// 线程数
"maxThread": -1,
// 入口 QPS
"qps": -1
}
]

5、授权规则

1
2
3
4
5
6
7
8
9
10
json复制代码[
{
// 资源名
"resource": "sentinel_spring_web_context",
// 流控应用
"limitApp": "/test",
// 授权类型(0代表白名单;1代表黑名单。)
"strategy": 0
}
]

注意:对于上述JOSN中的一些可选属性不需要的时候可以删除。

官方文档:github.com/alibaba/Sen…

15、限流规则如何推送到Nacos进行持久化?

sentinel默认的持久化只能从nacos推送到sentinel控制台,但是实际生产中肯定是双向修改都能推送的,这个如何解决呢?

其实sentinel官方文档就有说到解决方法,不过需要自己修改sentinel控制台的源码来实现。

这个还是比较复杂的,sentinel只帮我们实现了流控规则的demo,其他的还是要自己修改,这点不太人性化….

在这之前需要自己下载对应版本的sentinel控制台的源码,地址:github.com/alibaba/Sen…

流控规则源码修改

在源码的test目录下有sentinel提供的demo,分别有apollo、nacos、zookeeper,如下图:

这里我们是Nacos,因此只需要nacos包下面的demo。修改步骤如下:

1、去掉sentinel-datasource-nacos依赖的scop

这个sentinel-datasource-nacos依赖默认是<scope>test</scope>,因此我们需要去掉这个,如下:

1
2
3
4
5
xml复制代码<!-- for Nacos rule publisher sample -->
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-datasource-nacos</artifactId>
</dependency>

如果你集成的zookeeper或者apollo,则把相应的依赖也要修改。

2、复制test环境下的nacos整个包到main下

将这个nacos包复制到com.alibaba.csp.sentinel.dashboard.rule这个包下,如下图:

3、将FlowControllerV2中的代码复制到FlowControllerV1中

com.alibaba.csp.sentinel.dashboard.controller.v2.FlowControllerV2这个是sentinel提供的demo,只需要将其中的代码全部覆盖到com.alibaba.csp.sentinel.dashboard.controller.FlowControllerV1中。

4、修改FlowControllerV1中的代码

直接覆盖掉当然不行,还要做一些修改,如下:

  • 修改RequestMapping中的请求url为/v1/flow
  • 修改ruleProvider、rulePublisher的依赖,修改后的代码如下:
1
2
3
4
5
6
7
8
less复制代码@Autowired
//使用nacos的依赖
@Qualifier("flowRuleNacosProvider")
private DynamicRuleProvider<List<FlowRuleEntity>> ruleProvider;
@Autowired
//使用nacos的依赖
@Qualifier("flowRuleNacosPublisher")
private DynamicRulePublisher<List<FlowRuleEntity>> rulePublisher;

5、注意nacos的相关配置

com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfigUtil这个工具类中对应的是限流规则在nacos中的一些配置项,有groupId、dataId…对应的配置如下:

需要两边统一,可以自己修改。

com.alibaba.csp.sentinel.dashboard.rule.nacos.NacosConfig这个类中有个方法如下图:

默认指定的nacos地址是本地的,这个需要修改。

6、完成

以上步骤已经改造了sentinel控制台的流控规则,打包启动控制台代码,命令如下:

1
ini复制代码mvn clean install -DskipTests=true -pl sentinel-dashboard -am

启动后在控制台添加流控规则,可以看到也会同步推送到nacos,包括增删改。

其他规则修改也很简单,照葫芦画瓢,这里就不再详细说了,后面会单独出一篇文章详细说一下。

16、集群流控如何做?

首先一个简单的问题:**为什么需要集群流控?**单机流控不香吗?原因如下:

  • 对于微服务要想保证高可用,必须是集群,假设有100个集群,那么想要设置流控规则,是不是每个微服务都要设置一遍?维护成本太高了
  • 单体流控还会造成流量不均匀的问题,出现总流控阈值没有达到某些微服务已经被限流了,这个是非常糟糕的问题,因此实际生产中对于集群不推荐单体流控。

那么如何解决上述的问题呢?sentinel为我们提供了集群流控的规则。思想很简单就是提供一个专门的server来统计调用的总量,其他的实例都与server保持通信。

集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。

集群流控中共有两种身份:

  • Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。
  • Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。

sentinel的集群限流有两种模式,分别如下:

  • 独立模式(Alone):即作为独立的 token server 进程启动,独立部署,隔离性好,但是需要额外的部署操作。独立模式适合作为 Global Rate Limiter 给集群提供流控服务。
  • 嵌入模式(Embedded):即作为内置的 token server 与服务在同一进程中启动。在此模式下,集群中各个实例都是对等的,token server 和 client 可以随时进行转变,因此无需单独部署,灵活性比较好。但是隔离性不佳,需要限制 token server 的总 QPS,防止影响应用本身。嵌入模式适合某个应用集群内部的流控。

下面就以嵌入模式为例介绍一下如何配置。

就以sentinel-openfeign-provider9009这个模块作为演示,直接启动三个集群,端口分别为9009、9011、9013,如下图:

启动成功,在sentinel控制台将会看到有三个实例已经被监控了,如下图:

此时只需要在控制台指定一个服务为token server,其他的为token client,集群流控->新增token server,操作如下图:

选取一个作为服务端,另外两个作为客户端,此时就已经配置好了,如下图:

此时就可以添加集群流控规则了,可以在sentinel控制台直接添加,也可以通过Nacos直接配置,下图是通过Nacos配置的,如下图:

Nacos推送成功后将会在sentinel控制台看到这条流控规则的配置,如下图:

OK,至此集群流控到这儿就介绍完了,配置好之后可以自己试一下效果,陈某就不再演示了。

官方文档:github.com/alibaba/Sen…

17、网关限流如何配置?

这一块内容在后续介绍到网关的时候会详细讲,这里就不再细说了,有想要了解的可以看官方文档。

官方文档:github.com/alibaba/Sen…

18、整合openFeign如何实现熔断降级?

这个在上篇openFeign的文章中有详细介绍:openFeign夺命连环9问,这谁受得了?陈某这里就不再重复介绍了,有不知道的可以看上面这篇文章。

本文转载自: 掘金

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

保姆级教程之Redis缓存穿透 缓存击穿 缓存雪崩 缓存预热

发表于 2021-10-09

缓存引发的问题

image-20211009193259982

缓存穿透

缓存穿透:指访问不存在的key,依然被大量用户访问

指大量请求访问缓存中不存在的值,从而大量请求打进DB中,导致DB故障。

缓存击穿

缓存击穿:一个缓存key失效后,依然被大量用户访问

指热点数据缓存过期的一瞬间,大量请求打进DB中,导致DB故障。

缓存雪崩

缓存雪崩:多个缓存key失效,依然被大量用户访问

指大面积的缓存集体失效,导致用户要向数据库发送请求,而引发的故障。

解决方案

  • 接口层增加校验,如用户鉴权校验(参数校验)
  • 从缓存区不到的数据,数据库也取不到的数据可以设置为null
  • 超时时间设置得比较随机,这样可以一定程度上避免雪崩的出现
  • 布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不会存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力
  • 缓存预热 提交把热点key存入缓存中

布隆过滤器

image-20211009211027896

存入缓存的值经过多次哈希函数算出哈希code然后存放到一个数组中,该数组只存放0和1,因此我们可以通过位运算可以快速知道某个缓存是否在redis中。

运用布隆过滤器可以过滤掉redis中不存在的缓存key,但是无法确定某个值是否一定存在里面。

结论:布隆过滤器中不存在的值,现实中一定不存在布隆过滤器中存在的值,现实中未必存在

布隆过滤器的应用场景不止于此,比如:

  • 原本有10亿个号码,现在又来了10万个号码,要快速准确判断这10万个号码是否在10亿个号码库中
  • 垃圾邮箱的过滤
  • 爬虫URL过滤

等等类似这种大数据量集合,快速准确的判断某个数据是否在集合中,并且不占内存,这就是布隆过滤器的应用场景

缓存预热

提前把热点数据塞入redis

因为我们不知道,所以我们要采用 加锁的那一套保护方案

开发逻辑上也要规避差集,会造成击穿,穿透,雪崩

具体操作:

  • 日常例行统计数据访问记录,统计访问频度较高的热点数据
  • 利用LRU数据删除策略,构建数据留存队列

Redis实现分布式锁

image-20211009212435761

客户端的请求首先会去redis请求锁,此时只能有一个线程能拿到锁,其他线程做一个cas的自旋。这个拿到锁的线程才可以去mysql把数据带回redis然后释放锁。其他的client兄弟就能在redis中找到数据。当

双写不一致

当我们向数据库进行更新操作的时候,同时也要希望更新缓存中的顺序。这是两个动作,不具有原子性,那么到底是谁先谁后呢?因此我们有了以下四个方案

  • 先更新数据库 ,在写缓存 (不可取)
  • 先写缓存, 再更新数据库(不可取)
  • 先删除缓存,再更新数据库(不可取)
  • 先更新数据库,再删除缓存(可取,但是有瑕疵)

先更新数据库 ,在写缓存

线程A要去数据库执行一个删除库存的操作,但还未更新缓存。但此时线程B来查缓存,发现还有缓存,进行下单,其实数据库已经没缓存了,因此方案不行

先写缓存,再更新数据库

一开始缓存中没有数据,线程A去数据库写数据,但还没写成。此时线程B到缓存中查数据,缓存中的是新数据,但是数据库还没更新,数据库的是老数据,那么线程B会拿数据库的老数据进行操作。

先删除缓存,再更新数据库

线程A删除缓存,然后去更新数据库。此时线程B来了,B发现缓存中没东西,然后跑去数据库拿,并且B比A更快拿到数据,但是这个数据还是老数据,因为A还没未数据进行一个写入。

先更新数据库,再删除缓存

线程B去查数据库的数据,此时线程A执行更新数据库,并且删除了缓存。那么线程B会按之前查到的老数据的数据并且存入缓存中,这个存放在缓存中的数据是老的数据,

解决办法:

延迟双删除:
线程A在第一次删除完了之后,进行一个sleep,然后再删除一次。就能避免上面的问题。但是这也会引入另外一个问题,这个sleep的值该设为多少不好把握。在此背景下,Canal横空出世!

Canal

image-20211007110139509

主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费

早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。

Canal 伪装成mysql的一个从机slave,然后向Mysql master,发送dump请求,去获得binlog。等到master返回binlog之后,会把binlog中的数据,解析到内存,不进行持久化,提供给客户进行查询。

本文转载自: 掘金

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

性能测试-jmeter工具

发表于 2021-10-09

注意:本篇对基本组件的介绍较少,更多是介绍jemter工具的进阶使用,若为初学者可以借鉴其他参考文档或官方网站:jmeter.apache.org/usermanual/…

1. Jmeter的常用组件、执行顺序、作用域

  1. 测试计划 TestPlan
  1. 线程组Thread Group
  1. 配置元件Config ELement
  1. 前置处理器 Preprocessor
  1. 定时器Timer
  1. 采样器Samper
  1. 后置处理器Postprocessor
  1. 断言组件Assertions
  1. 监听器Listener
  1. 逻辑控制器Logical Controller
  1. 测试片段 TestFragment
  1. 非测试组件Non-Test Element

组件间执行顺序:配置元件→前置处理器→定时器→采样器→后置处理器→断言→监听器

相同组件执行顺序:顺序执行,谁在最上,谁先执行

作用域:组件影响的范围为同级节点和子节点

2. Jmeter 参数化配置

  1. 通过User Parameters组件

image.png

image.png
如上图:添加了两个参数,username和password;配置了三个用户;引用变量时,使用usernaem、{usernaem}、usernaem、{password}方式引用;注意此处用户数轮询取值的,什么意思呢,如果你有三个线程,依次为线程1取admin,线程2取tom,线程3取jack;如果你有4个线程,则线程4取admin(下一个轮回)

  1. 通过CSV Data Set Config组件

通过读取CSV文件的方式与参数原理上一样的,引用的方式也是通过${变量名}引用定义的参数,简单的举例如下:

Demo.CSV文件内容

1
2
3
4
复制代码Username,password
Admin,123
Tom,123
Jack,123

读取文件的设置和定义变量如下:其中因我的csv文件的有表头和使用英文的逗号分隔;故配置如图

image.png

  1. 通过函数助手添加参数

image.png

选择红框的函数,点击绿框生成,复制蓝框的内容到需要引用的地方

3. 逻辑控制器-事务控制器

image.png

事务控制器可以将多个请求组合为一个事务,已事务为单位进行聚合报告的分析。

注意:如果事务的请求数据和其下面的http请求数据相加的和差距不大,此时的数据可以作为正确的数据使用,如果事务的总数与其下相加的总和差距较大,则测试的数据价值性很低;原因可能是:

  1. Jmeter的运行内容设置不够,需要修改配置文件调整内存
  2. 单机运行请求的线程太多,CPU和内存饱和了;需要使用分布式请求(联机负载)
  3. 本机的配置较低或有其他的程序在运行,资源不足;使用命令行运行

4. 后置处理器-正则提取器

image.png

正则表达式

详情www.runoob.com/regexp/rege…

QQ图片20211009204457.png

5. 后置处理器-json提取器

image.png

6. 集合点(真正的并发使用)

利用同步定时器实现

image.png

7.jmeter联机负载和命令行

联机负载:主控机(存放脚本)→负载机(运行脚本)操作步骤:A. 首先在各负载机启动jmeter-server.bat 或jmeter-server.shB. 修改主控机上的jmeter.properties文件主控机的配置

1
2
ini复制代码# 请找remote_hosts配置
remote_hosts=127.0.0.1,192.168.6.166:1099

负载机的配置

1
2
3
4
5
6
ini复制代码# 寻找server_port参数去掉注释
server_port=1099
# 寻找server.rmi.localport参数去掉注释,修改端口与上面一致
server.rmi.localport=1099
# 寻找server.rmi.ssi.disable=true去掉注释
server.rmi.ssi.disable=true

C. 在主控机启动jmeterD. 在控制机运行的时候选择远程启动或远程启动即可命令行运行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
diff复制代码Jmeter  -h  查看所有的命令
-n 非 GUI 模式 -> 在非 GUI 模式下运行 JMeter
-t 测试文件 -> 要运行的 JMeter 测试脚本文件-l 
日志文件 -> 记录结果的文件
-r 远程执行 -> 在Jmter.properties文件中指定的所有远程服务器
-H 代理主机 -> 设置 JMeter 使用的代理主机
-P 代理端口 -> 设置 JMeter 使用的代理主机的端口号
例如:jmeter -n -t test1.jmx -l logfile1.jtl -H 192.168.1.1 -P 8080
运行命令格式:
jmeter -n -t [jmx脚本路径] -l [测试输出文件路径]
参数解释:
-n: 表示 non gui mode,就是非图形化模式
-t: 表示jmeter脚本的路
-l:表示输出结果路径,如果没有该文件就自动创建,可以生成csv或者jtl文

8.Beanshell

是一个小型的嵌入式java源代码解释器,具有对象脚本语言的特性,能够动态的执行标准的java语法。

  1. Jmeter的beanshell

l 定时器 beanshell

l 前置处理器 beanshell

l 采样器beanshell

l 后置处理器 beanshell

l 断言 beanshell

l 监听器 beanshell

2 jmeter的beanshell的常用内置变量

log→写入信息到jmeter.log文件 log.info(“这是日志”)

ctx→引用当前线程的上下文

vars.get(string key)→从jmeter获取变量

vars.put(string key, string key)→把数据写入变量中

Props.get(string key)→从jmeter.properties中读取

Props.put(string key, string key)→定义属性

Prev.getResponseDataString()→获取响应信息

Prev.getResponseCode()→获取响应代码

最后: 可以通过在 点击蓝色字体后面 免费领取一份216页软件测试工程师面试宝典文档资料。以及相对应的视频学习教程免费分享!,其中包括了有基础知识、Linux必备、Shell、互联网程序原理、Mysql数据库、抓包工具专题、接口测试工具、测试进阶-Python编程、Web自动化测试、APP自动化测试、接口自动化测试、测试高级持续集成、测试架构开发测试框架、性能测试、安全测试等。

如果我的博客对你有帮助、如果你喜欢我的博客内容,请 “点赞” “评论” “收藏” 一键三连哦!

本文转载自: 掘金

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

Spring Boot自动配置原理

发表于 2021-10-09

本文正在参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金。

我们知道,Spring Boot 项目创建完成后,即使不进行任何的配置,也能够顺利地运行,这都要归功于 Spring Boot 的自动化配置。

Spring Boot 默认使用 application.properties 或 application.yml 作为其全局配置文件,我们可以在该配置文件中对各种自动配置属性(server.port、logging.level.* 、spring.config.active.no-profile 等等)进行修改,并使之生效,那么您有没有想过这些属性是否有据可依呢?答案是肯定的。

Spring Boot 官方文档:常见应用属性中对所有的配置属性都进行了列举和解释,我们可以根据官方文档对 Spring Boot 进行配置,但 Spring Boot 中的配置属性数量庞大,仅仅依靠官方文档进行配置也十分麻烦。我们只有了解了 Spring Boot 自动配置的原理,才能更加轻松熟练地对 Spirng Boot 进行配置。本节为你揭开 SpringBoot 自动配置的神秘面纱。

Spring Factories 机制

Spring Boot 的自动配置是基于 Spring Factories 机制实现的。

Spring Factories 机制是 Spring Boot 中的一种服务发现机制,这种扩展机制与 Java SPI 机制十分相似。Spring Boot 会自动扫描所有 Jar 包类路径下 META-INF/spring.factories 文件,并读取其中的内容,进行实例化,这种机制也是 Spring Boot Starter 的基础。

spring.factories

spring.factories 文件本质上与 properties 文件相似,其中包含一组或多组键值对(key=vlaue),其中,key 的取值为接口的完全限定名;value 的取值为接口实现类的完全限定名,一个接口可以设置多个实现类,不同实现类之间使用“,”隔开,例如:\

1
2
3
4
java复制代码org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\
org.springframework.boot.autoconfigure.condition.OnBeanCondition,\
org.springframework.boot.autoconfigure.condition.OnClassCondition,\
org.springframework.boot.autoconfigure.condition.OnWebApplicationCondition

Spring Factories 实现原理

spring-core 包里定义了 SpringFactoriesLoader 类,这个类会扫描所有 Jar 包类路径下的 META-INF/spring.factories 文件,并获取指定接口的配置。在 SpringFactoriesLoader 类中定义了两个对外的方法,如下表。

返回值 方法 描述
List loadFactories(Class factoryType, @Nullable ClassLoader classLoader) 静态方法; 根据接口获取其实现类的实例; 该方法返回的是实现类对象列表。
List loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) 公共静态方法; 根据接口l获取其实现类的名称; 该方法返回的是实现类的类名的列表

以上两个方法的关键都是从指定的 ClassLoader中获取spring.factories 文件,并解析得到类名列表,具体代码如下。
loadFactories() 方法能够获取指定接口的实现类对象,具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
Assert.notNull(factoryType, "'factoryType' must not be null");
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
// 调用loadFactoryNames获取接口的实现类
List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
}
// 遍历 factoryNames 数组,创建实现类的对象
List<T> result = new ArrayList(factoryImplementationNames.size());
Iterator var5 = factoryImplementationNames.iterator();
//排序
while(var5.hasNext()) {
String factoryImplementationName = (String)var5.next();
result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
}

AnnotationAwareOrderComparator.sort(result);
return result;
}

loadFactoryNames() 方法能够根据接口获取其实现类类名的集合,具体代码如下。

1
2
3
4
5
6
7
8
9
10
java复制代码public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoader == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}

String factoryTypeName = factoryType.getName();
//获取自动配置类
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}

loadSpringFactories() 方法能够读取该项目中所有 Jar 包类路径下 META-INF/spring.factories 文件的配置内容,并以 Map 集合的形式返回,具体代码如下。

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
java复制代码private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
HashMap result = new HashMap();

try {
//扫描所有 Jar 包类路径下的 META-INF/spring.factories 文件
Enumeration urls = classLoader.getResources("META-INF/spring.factories");

while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
//将扫描到的 META-INF/spring.factories 文件中内容包装成 properties 对象
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();

while(var6.hasNext()) {
Map.Entry<?, ?> entry = (Map.Entry)var6.next();
//提取 properties 对象中的 key 值
String factoryTypeName = ((String)entry.getKey()).trim();
//提取 proper 对象中的 value 值(多个类的完全限定名使用逗号连接的字符串)
// 使用逗号为分隔符转换为数组,数组内每个元素都是配置类的完全限定名
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
//遍历配置类数组,并将数组转换为 list 集合
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
//将 propertise 对象的 key 与由配置类组成的 List 集合一一对应存入名为 result 的 Map 中
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
//返回 result
return result;
} catch (IOException var14) {
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
}
}
}

自动配置的加载

Spring Boot 自动化配置也是基于 Spring Factories 机制实现的,在 spring-boot-autoconfigure-xxx.jar 类路径下的 META-INF/spring.factories 中设置了 Spring Boot 自动配置的内容 ,如下。

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
java复制代码# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration,\
org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration,\
org.springframework.boot.autoconfigure.context.LifecycleAutoConfiguration,\
org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration,\
org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration,\
org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.cassandra.CassandraRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.couchbase.CouchbaseRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.elasticsearch.ReactiveElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jdbc.JdbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.jpa.JpaRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.ldap.LdapRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jReactiveRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.neo4j.Neo4jRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.solr.SolrRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcDataAutoConfiguration,\
org.springframework.boot.autoconfigure.data.r2dbc.R2dbcRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.data.redis.RedisRepositoriesAutoConfiguration,\
org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration,\
org.springframework.boot.autoconfigure.elasticsearch.ElasticsearchRestClientAutoConfiguration,\
org.springframework.boot.autoconfigure.flyway.FlywayAutoConfiguration,\
org.springframework.boot.autoconfigure.freemarker.FreeMarkerAutoConfiguration,\
org.springframework.boot.autoconfigure.groovy.template.GroovyTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.gson.GsonAutoConfiguration,\
org.springframework.boot.autoconfigure.h2.H2ConsoleAutoConfiguration,\
org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastAutoConfiguration,\
org.springframework.boot.autoconfigure.hazelcast.HazelcastJpaDependencyAutoConfiguration,\
org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration,\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.influx.InfluxDbAutoConfiguration,\
org.springframework.boot.autoconfigure.info.ProjectInfoAutoConfiguration,\
org.springframework.boot.autoconfigure.integration.IntegrationAutoConfiguration,\
org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JdbcTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.JndiDataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.XADataSourceAutoConfiguration,\
org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JmsAutoConfiguration,\
org.springframework.boot.autoconfigure.jmx.JmxAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.JndiConnectionFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.activemq.ActiveMQAutoConfiguration,\
org.springframework.boot.autoconfigure.jms.artemis.ArtemisAutoConfiguration,\
org.springframework.boot.autoconfigure.jersey.JerseyAutoConfiguration,\
org.springframework.boot.autoconfigure.jooq.JooqAutoConfiguration,\
org.springframework.boot.autoconfigure.jsonb.JsonbAutoConfiguration,\
org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration,\
org.springframework.boot.autoconfigure.availability.ApplicationAvailabilityAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.embedded.EmbeddedLdapAutoConfiguration,\
org.springframework.boot.autoconfigure.ldap.LdapAutoConfiguration,\
org.springframework.boot.autoconfigure.liquibase.LiquibaseAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderAutoConfiguration,\
org.springframework.boot.autoconfigure.mail.MailSenderValidatorAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration,\
org.springframework.boot.autoconfigure.mongo.MongoReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.mustache.MustacheAutoConfiguration,\
org.springframework.boot.autoconfigure.neo4j.Neo4jAutoConfiguration,\
org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration,\
org.springframework.boot.autoconfigure.r2dbc.R2dbcAutoConfiguration,\
org.springframework.boot.autoconfigure.r2dbc.R2dbcTransactionManagerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketRequesterAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketServerAutoConfiguration,\
org.springframework.boot.autoconfigure.rsocket.RSocketStrategiesAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration,\
org.springframework.boot.autoconfigure.security.rsocket.RSocketSecurityAutoConfiguration,\
org.springframework.boot.autoconfigure.security.saml2.Saml2RelyingPartyAutoConfiguration,\
org.springframework.boot.autoconfigure.sendgrid.SendGridAutoConfiguration,\
org.springframework.boot.autoconfigure.session.SessionAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.servlet.OAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.client.reactive.ReactiveOAuth2ClientAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.servlet.OAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.security.oauth2.resource.reactive.ReactiveOAuth2ResourceServerAutoConfiguration,\
org.springframework.boot.autoconfigure.solr.SolrAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskExecutionAutoConfiguration,\
org.springframework.boot.autoconfigure.task.TaskSchedulingAutoConfiguration,\
org.springframework.boot.autoconfigure.thymeleaf.ThymeleafAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.TransactionAutoConfiguration,\
org.springframework.boot.autoconfigure.transaction.jta.JtaAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration,\
org.springframework.boot.autoconfigure.web.embedded.EmbeddedWebServerFactoryCustomizerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.HttpHandlerAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.ReactiveWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.error.ErrorWebFluxAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.ClientHttpConnectorAutoConfiguration,\
org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.HttpEncodingAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration,\
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.reactive.WebSocketReactiveAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketServletAutoConfiguration,\
org.springframework.boot.autoconfigure.websocket.servlet.WebSocketMessagingAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.WebServicesAutoConfiguration,\
org.springframework.boot.autoconfigure.webservices.client.WebServiceTemplateAutoConfiguration

以上配置中,value 取值是由多个 xxxAutoConfiguration (使用逗号分隔)组成,每个 xxxAutoConfiguration 都是一个自动配置类。Spring Boot 启动时,会利用 Spring-Factories 机制,将这些 xxxAutoConfiguration 实例化并作为组件加入到容器中,以实现 Spring Boot 的自动配置。

@SpringBootApplication 注解

所有 Spring Boot 项目的主启动程序类上都使用了一个 @SpringBootApplication 注解,该注解是 Spring Boot 中最重要的注解之一 ,也是 Spring Boot 实现自动化配置的关键。
@SpringBootApplication 是一个组合元注解,其主要包含两个注解:@SpringBootConfiguration 和 @EnableAutoConfiguration,其中 @EnableAutoConfiguration 注解是 SpringBoot 自动化配置的核心所在。

image.png

@EnableAutoConfiguration 注解

@EnableAutoConfiguration 注解用于开启 Spring Boot 的自动配置功能, 它使用 Spring 框架提供的 @Import 注解通过 AutoConfigurationImportSelector类(选择器)给容器中导入自动配置组件。

AutoConfigurationImportSelector 类
图2:@EnableAutoConfiguration 注解

AutoConfigurationImportSelector 类

AutoConfigurationImportSelector 类实现了 DeferredImportSelector 接口,AutoConfigurationImportSelector 中还包含一个静态内部类 AutoConfigurationGroup,它实现了 DeferredImportSelector 接口的内部接口 Group(Spring 5 新增)。
AutoConfigurationImportSelector 类中包含 3 个方法,如下表

返回值 方法声明 描述 内部类方法 内部类
Class<? extends Group> getImportGroup() 该方法获取实现了 Group 接口的类,并实例化 否
void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) 该方法用于引入自动配置的集合 是 AutoConfigurationGroup
Iterable selectImports() 遍历自动配置类集合(Entry 类型的集合),并逐个解析集合中的配置类 是

AutoConfigurationGroup
AutoConfigurationImportSelector 内各方法执行顺序如下。

  1. getImportGroup() 方法
  2. process() 方法
  3. selectImports() 方法

下面我们将分别对以上 3 个方法及其调用过程进行介绍。

1. getImportGroup() 方法

AutoConfigurationImportSelector 类中 getImportGroup() 方法主要用于获取实现了 DeferredImportSelector.Group 接口的类,代码如下。

1
2
3
4
java复制代码    public Class<? extends Group> getImportGroup() {
//获取实现了 DeferredImportSelector.Gorup 接口的 AutoConfigurationImportSelector.AutoConfigurationGroup 类
return AutoConfigurationImportSelector.AutoConfigurationGroup.class;
}

2. process() 方法

静态内部类 AutoConfigurationGroup 中的核心方法是 process(),该方法通过调用 getAutoConfigurationEntry() 方法读取 spring.factories 文件中的内容,获得自动配置类的集合,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector, () -> {
return String.format("Only %s implementations are supported, got %s", AutoConfigurationImportSelector.class.getSimpleName(), deferredImportSelector.getClass().getName());
});
//拿到 META-INF/spring.factories中的EnableAutoConfiguration,并做排除、过滤处理
//AutoConfigurationEntry里有需要引入配置类和排除掉的配置类,最终只要返回需要配置的配置类
AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector)deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);
//加入缓存,List<AutoConfigurationEntry>类型
this.autoConfigurationEntries.add(autoConfigurationEntry);
Iterator var4 = autoConfigurationEntry.getConfigurations().iterator();

while(var4.hasNext()) {
String importClassName = (String)var4.next();
//加入缓存,Map<String, AnnotationMetadata>类型
this.entries.putIfAbsent(importClassName, annotationMetadata);
}
}

getAutoConfigurationEntry() 方法通过调用 getCandidateConfigurations() 方法来获取自动配置类的完全限定名,并在经过排除、过滤等处理后,将其缓存到成员变量中,具体代码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码protected AutoConfigurationImportSelector.AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
if (!this.isEnabled(annotationMetadata)) {
return EMPTY_ENTRY;
} else {
//获取注解元数据中的属性设置
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
//获取自动配置类
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
//删除list 集合中重复的配置类
configurations = this.removeDuplicates(configurations);
//获取飘出导入的配置类
Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
//检查是否还存在排除配置类
this.checkExcludedClasses(configurations, exclusions);
//删除排除的配置类
configurations.removeAll(exclusions);
//获取过滤器,过滤配置类
configurations = this.getConfigurationClassFilter().filter(configurations);
//出发自动化配置导入事件
this.fireAutoConfigurationImportEvents(configurations, exclusions);
return new AutoConfigurationImportSelector.AutoConfigurationEntry(configurations, exclusions);
}
}

在 getCandidateConfigurations() 方法中,根据 Spring Factories 机制调用 SpringFactoriesLoader 的 loadFactoryNames() 方法,根据 EnableAutoConfiguration.class (自动配置接口)获取其实现类(自动配置类)的类名的集合,如下图。

getCandidateConfigurations 方法

图3:getCandidateConfigurations 方法

3. process() 方法 以上所有方法执行完成后,AutoConfigurationImportSelector.AutoConfigurationGroup#selectImports() 会将 process() 方法处理后得到的自动配置类,进行过滤、排除,最后将所有自动配置类添加到容器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public Iterable<DeferredImportSelector.Group.Entry> selectImports() {
if (this.autoConfigurationEntries.isEmpty()) {
return Collections.emptyList();
} else {
//获取所有需要排除的配置类
Set<String> allExclusions = (Set)this.autoConfigurationEntries.stream().
map(AutoConfigurationImportSelector.AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
//获取所有经过自动化配置过滤器的配置类
Set<String> processedConfigurations = (Set)this.autoConfigurationEntries.stream().map(AutoConfigurationImportSelector.
AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream).collect(Collectors.toCollection(LinkedHashSet::new));
//排除过滤后配置类中需要排除的类
processedConfigurations.removeAll(allExclusions);
return (Iterable)this.sortAutoConfigurations(processedConfigurations,
this.getAutoConfigurationMetadata()).stream().map((importClassName) -> {
return new DeferredImportSelector.Group.Entry((AnnotationMetadata)this.entries.get(importClassName), importClassName);
}).collect(Collectors.toList());
}
}

自动配置的生效和修改

spring.factories 文件中的所有自动配置类(xxxAutoConfiguration),都是必须在一定的条件下才会作为组件添加到容器中,配置的内容才会生效。这些限制条件在 Spring Boot 中以 @Conditional 派生注解的形式体现,如下表。

注解 生效条件
@ConditionalOnJava 应用使用指定的 Java 版本时生效
@ConditionalOnBean 容器中存在指定的 Bean 时生效
@ConditionalOnMissingBean 容器中不存在指定的 Bean 时生效
@ConditionalOnExpression 满足指定的 SpEL 表达式时生效
@ConditionalOnClass 存在指定的类时生效
@ConditionalOnMissingClass 不存在指定的类时生效
@ConditionalOnSingleCandidate 容器中只存在一个指定的 Bean 或这个 Bean 为首选 Bean 时生效
@ConditionalOnProperty 系统中指定属性存在指定的值时生效
@ConditionalOnResource 类路径下存在指定的资源文件时生效
@ConditionalOnWebApplication 当前应用是 web 应用时生效
@ConditionalOnNotWebApplication 当前应用不是 web 应用生效

下面我们以 ServletWebServerFactoryAutoConfiguration 为例,介绍 Spring Boot 自动配置是如何生效的。

ServletWebServerFactoryAutoConfiguration

ServletWebServerFactoryAutoConfiguration 代码如下。

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
java复制代码@Configuration(   //表示这是一个配置类,与 xml 配置文件等价,也可以给容器中添加组件

proxyBeanMethods = false
)
@AutoConfigureOrder(-2147483648)
@ConditionalOnClass({ServletRequest.class})//判断当前项目有没有 ServletRequest 这个类
@ConditionalOnWebApplication(// 判断当前应用是否是 web 应用,如果是,当前配置类生效
type = Type.SERVLET
)
@EnableConfigurationProperties({ServerProperties.class})
//启动指定类的属性配置(ConfigurationProperties)功能;将配置文件中对应的值和 ServerProperties 绑定起来;并把 ServerProperties 加入到ioc容器中
@Import({ServletWebServerFactoryAutoConfiguration.BeanPostProcessorsRegistrar.class, EmbeddedTomcat.class, EmbeddedJetty.class, EmbeddedUndertow.class})
public class ServletWebServerFactoryAutoConfiguration {
public ServletWebServerFactoryAutoConfiguration() {
}

@Bean //给容器中添加一个组件,这个组件的某些值需要从properties中获取
public ServletWebServerFactoryCustomizer servletWebServerFactoryCustomizer(ServerProperties serverProperties, ObjectProvider<WebListenerRegistrar> webListenerRegistrars) {
return new ServletWebServerFactoryCustomizer(serverProperties, (List) webListenerRegistrars.orderedStream().collect(Collectors.toList()));
}

@Bean
@ConditionalOnClass(
name = {"org.apache.catalina.startup.Tomcat"}
)
public TomcatServletWebServerFactoryCustomizer tomcatServletWebServerFactoryCustomizer(ServerProperties serverProperties) {
return new TomcatServletWebServerFactoryCustomizer(serverProperties);
}

@Bean
@ConditionalOnMissingFilterBean({ForwardedHeaderFilter.class})
@ConditionalOnProperty(
value = {"server.forward-headers-strategy"},
havingValue = "framework"
)
public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean(filter, new ServletRegistrationBean[0]);
registration.setDispatcherTypes(DispatcherType.REQUEST, new DispatcherType[]{DispatcherType.ASYNC, DispatcherType.ERROR});
registration.setOrder(-2147483648);
return registration;
}

public static class BeanPostProcessorsRegistrar implements ImportBeanDefinitionRegistrar, BeanFactoryAware {
private ConfigurableListableBeanFactory beanFactory;

public BeanPostProcessorsRegistrar() {
}

public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ConfigurableListableBeanFactory) {
this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
}

}

public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (this.beanFactory != null) {
this.registerSyntheticBeanIfMissing(registry, "webServerFactoryCustomizerBeanPostProcessor", WebServerFactoryCustomizerBeanPostProcessor.class, WebServerFactoryCustomizerBeanPostProcessor::new);
this.registerSyntheticBeanIfMissing(registry, "errorPageRegistrarBeanPostProcessor", ErrorPageRegistrarBeanPostProcessor.class, ErrorPageRegistrarBeanPostProcessor::new);
}
}

private <T> void registerSyntheticBeanIfMissing(BeanDefinitionRegistry registry, String name, Class<T> beanClass, Supplier<T> instanceSupplier) {
if (ObjectUtils.isEmpty(this.beanFactory.getBeanNamesForType(beanClass, true, false))) {
RootBeanDefinition beanDefinition = new RootBeanDefinition(beanClass, instanceSupplier);
beanDefinition.setSynthetic(true);
registry.registerBeanDefinition(name, beanDefinition);
}

}
}
}

该类使用了以下注解:

  • @Configuration:用于定义一个配置类,可用于替换 Spring 中的 xml 配置文件;
  • @Bean:被 @Configuration 注解的类内部,可以包含有一个或多个被 @Bean 注解的方法,用于构建一个 Bean,并添加到 Spring 容器中;该注解与 spring 配置文件中 等价,方法名与 的 id 或 name 属性等价,方法返回值与 class 属性等价;

除了 @Configuration 和 @Bean 注解外,该类还使用 5 个 @Conditional 衍生注解:

  • @ConditionalOnClass({ServletRequest.class}):判断当前项目是否存在 ServletRequest 这个类,若存在,则该配置类生效。
  • @ConditionalOnWebApplication(type = Type.SERVLET):判断当前应用是否是 Web 应用,如果是的话,当前配置类生效。
  • @ConditionalOnClass(name = {“org.apache.catalina.startup.Tomcat”}):判断是否存在 Tomcat 类,若存在则该方法生效。
  • @ConditionalOnMissingFilterBean({ForwardedHeaderFilter.class}):判断容器中是否有 ForwardedHeaderFilter 这个过滤器,若不存在则该方法生效。
  • @ConditionalOnProperty(value = {“server.forward-headers-strategy”},havingValue = “framework”):判断配置文件中是否存在 server.forward-headers-strategy = framework,若不存在则该方法生效。

ServerProperties

ServletWebServerFactoryAutoConfiguration 类还使用了一个 @EnableConfigurationProperties 注解,通过该注解导入了一个 ServerProperties 类,其部分源码如下。

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
java复制代码@ConfigurationProperties(
prefix = "server",
ignoreUnknownFields = true
)
public class ServerProperties {
private Integer port;
private InetAddress address;
@NestedConfigurationProperty
private final ErrorProperties error = new ErrorProperties();
private ServerProperties.ForwardHeadersStrategy forwardHeadersStrategy;
private String serverHeader;
private DataSize maxHttpHeaderSize = DataSize.ofKilobytes(8L);
private Shutdown shutdown;
@NestedConfigurationProperty
private Ssl ssl;
@NestedConfigurationProperty
private final Compression compression;
@NestedConfigurationProperty
private final Http2 http2;
private final ServerProperties.Servlet servlet;
private final ServerProperties.Tomcat tomcat;
private final ServerProperties.Jetty jetty;
private final ServerProperties.Netty netty;
private final ServerProperties.Undertow undertow;

public ServerProperties() {
this.shutdown = Shutdown.IMMEDIATE;
this.compression = new Compression();
this.http2 = new Http2();
this.servlet = new ServerProperties.Servlet();
this.tomcat = new ServerProperties.Tomcat();
this.jetty = new ServerProperties.Jetty();
this.netty = new ServerProperties.Netty();
this.undertow = new ServerProperties.Undertow();
}
....
}

我们看到,ServletWebServerFactoryAutoConfiguration 使用了一个 @EnableConfigurationProperties 注解,而 ServerProperties 类上则使用了一个 @ConfigurationProperties 注解。这其实是 Spring Boot 自动配置机制中的通用用法。
Spring Boot 中为我们提供了大量的自动配置类 XxxAutoConfiguration 以及 XxxProperties,每个自动配置类 XxxAutoConfiguration 都使用了 @EnableConfigurationProperties 注解,而每个 XxxProperties 上都使用 @ConfigurationProperties 注解。
@ConfigurationProperties 注解的作用,是将这个类的所有属性与配置文件中相关的配置进行绑定,以便于获取或修改配置,但是 @ConfigurationProperties 功能是由容器提供的,被它注解的类必须是容器中的一个组件,否则该功能就无法使用。而 @EnableConfigurationProperties 注解的作用正是将指定的类以组件的形式注入到 IOC 容器中,并开启其 @ConfigurationProperties 功能。因此,@ConfigurationProperties + @EnableConfigurationProperties 组合使用,便可以为 XxxProperties 类实现配置绑定功能。
自动配置类 XxxAutoConfiguration 负责使用 XxxProperties 中属性进行自动配置,而 XxxProperties 则负责将自动配置属性与配置文件的相关配置进行绑定,以便于用户通过配置文件修改默认的自动配置。也就是说,真正“限制”我们可以在配置文件中配置哪些属性的类就是这些 XxxxProperties 类,它与配置文件中定义的 prefix 关键字开头的一组属性是唯一对应的。

注意:XxxAutoConfiguration 与 XxxProperties 并不是一一对应的,大多数情况都是多对多的关系,即一个 XxxAutoConfiguration 可以同时使用多个 XxxProperties 中的属性,一个 XxxProperties 类中属性也可以被多个 XxxAutoConfiguration 使用。

本文转载自: 掘金

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

1…502503504…956

开发者博客

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