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

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


  • 首页

  • 归档

  • 搜索

参考这个PHP代码规范后,同事都夸你的代码写的棒极了

发表于 2021-05-25

为什么需要编码规范

一个好的编码习惯靠平时的习惯养成,团队里有好的编码规范,并且每个人都按照制定的编码规范来执行,那么我相信代码的质量以及可维护性会上升一个档次。而目前PHP要求都要遵守PSR标准规范,我只列举一部分编码中常用的写法进行说明,更多的规范大家可以搜索PSR标准。

PHP文件开始标签

对于纯PHP的代码文件,必须以<?php 或者<?标签开始,并且独占一行,同时不要加?>的结束标签,至于为啥,感兴趣的自己去了解哈。

1
2
3
4
5
php复制代码<?php

class TestDemo
{
}

Class类的声明

类名必须以大写开头的驼峰式命名(TestDemo),两个花括号换行且独占一行,同时需要用多行注释说明类的创建人以及作用等。

1
2
3
4
5
6
7
8
9
php复制代码/**
* 这是一个测试Demo类
* Author: gxcuizy
* Date: 2021-05-25 13:57:16
* Class TestClass
*/
class TestDemo
{
}

Class类的常量

类中的常量命名必须全部字母都要大写(HELLO_WORLD),同时单词间以下划线_分隔,而且最好增加注释说明,单行注释最好在//后先加一个空格再接注释说明。

1
2
php复制代码// 声明一个常量
const HELLO_WORLD = 'best';

Class类的成员属性

类中的成员属性(也叫成员变量),成员属性的命名可以遵循三个规则:大写开头的驼峰式(UserName)、小写开头的驼峰式(UserName)、小写开头的驼峰式(UserName)、小写开头的驼峰式(userName)、下划线分隔式($user_name);这三种命名规则都可以,我个人习惯用下划线分隔式,大家也可以根据自己的习惯选择,但是最好与团队编码规则保持一致即可,同时三个修饰符(public、protected、private)不能少。

1
2
3
4
php复制代码// 声明一个公共变量
public $user_name = '';
// 声明一个静态变量
public static $user_age = 18;

Class类的成员方法

类中的成员方法的命名必须采用小写开头的驼峰式命名(testAction)规则,方法的三个修饰符(public、protected、private)不能少,两个花括号换行且独占一行,参数的等号两边各一个空格,方法的注释不要漏了哈,包括方法的作用、参数说明以及返回值说明等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
php复制代码/**
* 这是一个测试方法
* @param string $msg 参数说明
* @return array
*/
public function testAction($msg = '')
{
// 返回数据格式
$return = array('code' => 200, 'msg' => '');
return $return;
}

/**
* 这是私有方法,方法命名以单下划线开始
* @param string $arg 参数说明
* @return string
*/
private function privateAction($arg = '')
{
return $arg;
}

运算符和表达式

相对于运算符或者不同类型的表达式,不管在任何地方使用,我们都需要在其符号两边有一个空格,例如$a = 1;、1 + 2以及1 && 0等。

1
2
3
4
5
6
7
8
9
10
11
php复制代码/**
* 获取两个数相加的和
* @param int $one 第一个数
* @param int $two 第二个数
* @return int
*/
public function getUserAge($one = 0, $two = 0)
{
$sum = $one + $two;
return $sum;
}

控制结构的规范写法

类似if …… else、while、switch ……case、foreach、for等流程控制结构,基本都需要与括号()和花括号{}配合使用,要求括号()两边均有一个空格,而左花括号{需要与右括号)同行且有一个空格,而作为结束的右花括号}则需要单独一行,而主体内容都是包含在花括号{}中。

1
2
3
4
5
6
7
8
9
10
11
12
php复制代码/**
* 判断用户是否成年
* @param int $age 年龄
*/
public function logicAction($age = 18)
{
if ($age >= 18) {
echo '已成年';
} else {
echo '未成年';
}
}

快速一键格式化

现在很多IDE都支持一键格式化代码,比如PhpStorm等,一般快捷键都是Ctrl + Alt + L,你也可以根据你自己的习惯进行修改快捷键以及代码格式的标准,既可以一键格式化整个文档,也可以只选中某一部分代码进行格式化。PS:如果这个文件不是你一个人创建和修改,请不要轻易一键格式化整个文件,因为会把人家的代码也给格式化了,推荐只格式化你自己的代码即可,每个人都不喜欢别人随意动自己的代码,所以不要搞事情!不要搞事情!不要搞事情!重要的事情说三遍,你懂的。

最后总结

先给出个人的几点建议吧,希望对你有所帮助:

  • 别加多余的空格,也就是该加一个空格就加一个就行(按照代码规范来就行)
  • 多余的换行删除掉(影响代码视觉美观)
  • 调试代码记得及时删除,而不仅仅是注释(怕你久而久之就忘了)
  • 代码注释,代码注释,代码注释(再忙也要适当的写注释,不要说后面再加)
  • 你可以看不爽别人的代码,但是不要随意修改别人的代码(因为别人看你的代码也不爽)
  • 欢迎补充……

我只是抽取了一小部分常用到的代码规范说一下,如果有写的不对的地方请大家指出,我会及时修改,谢谢。如果你还有其他好的编码技巧,也欢迎和大家一起分享。

本文转载自: 掘金

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

王者并发课-青铜4:宝刀屠龙-如何使用synchronize

发表于 2021-05-25

欢迎来到《王者并发课》,本文是该系列文章中的第4篇。

在前面的文章《双刃剑-理解多线程带来的安全问题》中,我们提到了多线程情况下存在的线程安全问题。本文将以这个问题为背景,介绍如何通过使用synchronized关键字解这一问题。当然,在青铜阶段,我们仍不会过多地描述其背后的原理,重点还是先体验并理解它的用法。

一、从场景中体验synchronized

是谁击败了主宰

在峡谷中,击败主宰可以获得高额的经济收益。因此,在条件允许的情况下,大家都会争相击败主宰。于是,哪吒和敌方的兰陵王开始争夺主宰。按规矩,谁是击败主宰的最后一击,谁便是胜利的一方。

假设主宰的初始血量是100,我们通过代码来模拟下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Java复制代码public class Master {
//主宰的初始血量
private int blood = 100;

//每次被击打后血量减5
public int decreaseBlood() {
blood = blood - 5;
return blood;
}

//通过血量判断主宰是否还存活
public boolean isAlive() {
return blood > 0;
}
}

我们定义了哪吒和兰陵王两个线程,让他们同时攻击主宰:

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
Java复制代码 public static void main(String[] args) {
final Master master = new Master();
Thread neZhaAttachThread = new Thread() {
public void run() {
while (master.isAlive()) {
try {
int remainBlood = master.decreaseBlood();
if (remainBlood == 0) {
System.out.println("哪吒击败了主宰!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};

Thread lanLingWangThread = new Thread() {
public void run() {
while (master.isAlive()) {
try {
int remainBlood = master.decreaseBlood();
if (remainBlood == 0) {
System.out.println("兰陵王击败了主宰!");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
neZhaAttachThread.start();
lanLingWangThread.start();
}

下面是运行的结果:

1
2
3
4
vbnet复制代码兰陵王击败了主宰!
哪吒击败了主宰!

Process finished with exit code 0

两人竟然都获得了主宰!很显然,我们不可能接受这样的结果。然而,细看代码,你会发现这个神奇的结果其实一点也不意外,两个线程在对blood做并发减法时出了错误,因为代码中压根没有必要的并发安全控制。

当然,解决办法也比较简单,在decreaseBlood方法上添加synchronized关键字即可:

1
2
3
4
Java复制代码public synchronized int decreaseBlood() {
blood = blood - 5;
return blood;
}

为什么加上synchronized关键字就可以了呢?这就需要往下看了解Java中的锁和同步了。

二、认识synchronized

1. 理解Java对象中的锁

在理解synchronized之前,我们先简单理解下锁的概念。在Java中,每个对象都会有一把锁。当多个线程都需要访问对象时,那么就需要通过获得锁来获得许可,只有获得锁的线程才能访问对象,并且其他线程将进入等待状态,等待其他线程释放锁。如下图所示:

2. 理解synchronized关键字

根据Sun官文文档的描述,synchronized关键字提供了一种预防线程干扰和内存一致性错误的简单策略,即如果一个对象对多个线程可见,那么该对象变量(final修饰的除外)的读写都需要通过synchronized来完成。

你可能已经注意到其中的两个关键名词:

  • 线程干扰(Thread Interference):不同线程中运行但作用于相同数据的两个操作交错时,就会发生干扰。这意味着这两个操作由多个步骤组成,并且步骤顺序重叠;
  • 内存一致性错误(Memory Consistency Errors):当不同的线程对应为相同数据的视图不一致时,将发生内存一致性错误。内存一致性错误的原因很复杂,幸运的是,我们不需要详细了解这些原因,所需要的只是避免它们的策略。

从竞态的角度讲,线程干扰对应的是Read-modify-write,而内存一致性错误对应的则是Check-then-act。

结合锁和synchronized的概念可以理解为,锁是多线程安全的基础机制,而synchronized是锁机制的一种实现。

三、synchronized的四种用法

1. 在实例方法中使用synchronized

1
2
3
4
Java复制代码public synchronized int decreaseBlood() {
blood = blood - 5;
return blood;
}

注意这段代码中的synchronized字段,它表示当前方法每次能且仅能有一个线程访问。另外,由于当前方法是实例方法,所以如果该对象存在多个实例的话,不同的实例可以由不同的线程访问,它们之间并无协作关系。

然而,你可能已经想到了,如果当前线程中有两个synchronized方法,不同的线程是否可以访问不同的synchronized方法呢?

答案是:不能。

这是因为每个实例内的同步方法,能且仅能有一个线程访问。

2. 在静态方法中使用synchronized

1
2
3
4
Java复制代码public static synchronized int decreaseBlood() {
blood = blood - 5;
return blood;
}

与实例方法的synchronized不同,静态方法的synchronized是基于当前方法所属的类,即Master.class,而每个类在虚拟机上有且只有一个类对象。所以,对于同一类而言,每次有且只能有一个线程能访问静态synchronized方法。

当类中包含有多个静态的synchronized方法时,每次也仍然有且只能有一个线程可以访问其中的方法。

注意: 从synchronized在实例方法和静态方法中的应用可以看出,synchronized方法是否能允许其他线程的进入,取决于synchronized的参数。每个不同的参数,在同一时刻都只允许一个线程访问。基于这样的认知,下面的两种用法就很容易理解了。

3. 在实例方法的代码块中使用synchronized

1
2
3
4
5
6
Java复制代码public int decreaseBlood() {
synchronized(this) {
blood = blood - 5;
return blood;
}
}

在某些情况下,你不需要在整个方法层面使用synchronized,毕竟这样的方式粒度较大,容易产生阻塞。此时,在代码块中使用synchronized就是非常不错的选择,如上面代码所示。

刚才已经提到,synchronized的并发限制取决于其参数,在上面这段代码中的参数是this,即当前类的实例对象。而在前面的public synchronized int decreaseBlood()中,synchronized的参数也是当前类的实例对象。因此,下面这两段代码是等同的:

1
2
3
4
5
6
7
8
9
10
11
Java复制代码public int decreaseBlood() {
synchronized(this) {
blood = blood - 5;
return blood;
}
}

public synchronized int decreaseBlood() {
blood = blood - 5;
return blood;
}

4. 在静态方法的代码块中使用synchronized

同理,下面这两个方法的效果也是等同的。

1
2
3
4
5
6
7
8
9
10
11
Java复制代码public static int decreaseBlood() {
synchronized(Master.class) {
blood = blood - 5;
return blood;
}
}

public static synchronized int decreaseBlood() {
blood = blood - 5;
return blood;
}

四、synchronized小结

前面,我们已经介绍了synchronized的几种常见用法,不必死记硬背,你只要记住synchronized可以接受任何非null对象作为参数,而每个参数在同一时刻能且只能允许一个线程访问即可。此外,还有一些具有实际指导意义的Tips你可以注意下:

  1. Java中的synchronized关键字用于解决多线程访问共享资源时的同步,以解决线程干扰和内存一致性问题;
  2. 你可以通过 代码块(code block) 或者 方法(method) 来使用synchronized关键字;
  3. synchronized的原理基于对象中的锁,当线程需要进入synchronized修饰的方法或代码块时,它需要先获得锁并在执行结束后释放它;
  4. 当线程进入非静态(non-static)同步方法时,它获得的是对象实例(Object level)的锁。而线程进入静态同步方法时,它所获得的是类实例(Class level)的锁,两者没有必然关系;
  5. 如果synchronized中使用的对象是null,将会抛出NullPointerException错误;
  6. synchronized对方法的性能有一定影响,因为线程要等待获取锁;
  7. 使用synchronized时尽量使用代码块,而不是整个方法,以免阻塞整个方法;
  8. 尽量不要使用String类型和原始类型作为参数。这是因为,JVM在处理字符串、原始类型时会对它们进行优化。比如,你原本是想对不同的字符串进行加锁,然而JVM认为它们是同一个,很显然这不是你想要的结果。

关于synchronized的可见性、指令排序等底层原理,我们会在后面的阶段中详细介绍。

以上就是文本的全部内容,恭喜你又上了一颗星!✨

夫子的试炼

  • 手写代码体验synchronized的不同用法。

延伸阅读与参考资料

  • 《王者并发课》大纲与更新进度总览
  • docs.oracle.com/javase/tuto…
  • javagoal.com/synchroniza…
  • 《王者并发课》专栏文集下载:github.com/ThoughtsBet…

关于作者

专注高并发领域创作。姊妹篇小册《高并发秒杀的设计精要与实现》作者,关注公众号【MetaThoughts】,及时获取文章更新和文稿。


如果本文对你有帮助,欢迎点赞、关注、监督,我们一起从青铜到王者。

本文转载自: 掘金

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

B 站收藏 138W+,GitHub 标星 71K+,肝

发表于 2021-05-25

大家好,我是 JackTian。

说起 B 站,它是国内知名的视频弹幕网站,现为年轻人在互联网上最主要的活跃站点之一,B 站上其实存在着很多人文科技、影视音乐等相关领域的兴趣爱好者。

作为一名互联网人的我们,在 B 站上关注的多数也都是编程科技领域的 UP 主或根据个人所需通过搜索的方式查阅自己所需要的视频及相关课程,来帮助自己解决目前所遇到的困难以及个人学习计划的参考文献,从而提高你对相关知识的进一步了解与学习。

之前我在公众号上也分享了一些优质的编程学习资料,供大家学习参考。例如:学生党学编程,有这个开源项目就够了!、100天从 Python 小白到大神最良心的学习资源!、GitHub 上帮助人们学习编码的 12 个资源 。

今天,跟大家分享一个由 Crash Course 出品的《计算机科学速成课》中译版教程,在介绍这个教程之前,先跟大家普及一下 Crash Course 是什么?

什么是 Crash Course?

Crash Course 是 YouTube 所创造的频道之一,由 John Green 和 Hank Green 创立。截至目前,已经制作了 15 多个课程,拥有数亿次观看,作为一个知名教育频道,不仅涵盖了多个领域。

在 Crash Course 中,所有高品质的教育视频免费提供给所有人,从而通过以快节奏的格式呈现信息来改变传统教科书的方式,增强学习体验。

图片

Computer-Science-Chinese

图片

Computer Science 也是 Crash Course 下的系列课程之一,讲师是:Carrie Anne,这门 Computer Science 课程介绍了计算机科学相关的基础知识,一共 40 集。

对于英文水平不是很好的读者来说,看起来会比较吃力。不用慌,在 B 站上的一些国内计算机爱好者已经把 Computer Science 翻译成了中文,也就是:计算机速成课。

这门计算机速成课在 B 站上截至目前,收获了点赞:2.4W+、收藏:10W+、转发:1.4W+、投币:2.0W+,果然是受到了各地编程科技人员的称赞!

图片

Computer-Science-Chinese 内容包括:

  • 早期的计算
  • 电子计算
  • 布尔逻辑与逻辑电路
  • 二进制
  • 算术逻辑单元
  • 寄存器 & 内存
  • 中央处理器
  • 指令和程序
  • 高级 CPU 设计
  • 编程史话
  • 编程语言
  • 编程原理:语句和函数
  • 算法初步
  • 数据结构
  • 阿兰·图灵
  • 软件工程
  • 集成电路、摩尔定律
  • 操作系统
  • 内存 & 储存介质
  • 文件系统
  • 压缩
  • 命令行界面
  • 屏幕 & 2D 图形显示
  • 冷战和消费主义
  • 个人计算机革命
  • 图形用户界面
  • 3D 图形
  • 计算机网络
  • 互联网
  • 万维网
  • 网络安全
  • 黑客与攻击
  • 加密
  • 机器学习与人工智能
  • 计算机视觉
  • 自然语言处理
  • 机器人
  • 计算机中的心理学
  • 教育型科技
  • 奇点,天网,计算机的未来

图片

对于英语不是特别友好的同学,也不用担心,有位热心网友为其 Computer-Science 课程制作了中文版字幕,上传到了 GitHub,也能学习到这门计算机速成课。在这里非常感谢 Crash Course 字幕组,也感谢他们的辛苦付出!

这个计算机速成课对于一些刚入门学习编程的同学来说,是非常值得一读的。当然了,对于一些基础知识不扎实的同学,也可以看看。

CrashCourse 字幕组还非常贴心的将视频每一集开头放上了片头相关总结,让你了解这一集中所要讲的内容、专业名词的中英文、什么时间段讲哪些部分的内容以及下一集所要讲的内容,非常赞!

图片

这么好的一份学习课程、这么好的字幕组,强烈建议大家看一看这门课程,相信你能从中有不一样的收获,也希望能够对大家有所帮助。

如果觉得这篇文章对你有点用的话,就请为本文留个言,点个在看,或者转发一下,让更多的朋友可以看到这么好的课程,因为这将是我持续输出更多优质文章的最强动力!感谢大家!

传送门:

1、Crash Course:www.thecrashcourse.com/

2、计算机速成课:www.bilibili.com/video/av213…

3、计算机速成课中文版:github.com/1c7/Crash-C…

4、10 分钟速成课:space.bilibili.com/1950746

5、Youtube 原视频:www.youtube.com/playlist?li…

本文转载自: 掘金

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

MGR最优化配置推荐

发表于 2021-05-25

写本篇文章的动机有三点:

  1. 希望给想用MGR的朋友提供一个参数配置参考
  2. 知数堂的学生更加需要一份MGR的参数手册
  3. 在3306π社区广州站听了万里DB CTO娄帅关于《MGR Best Practice》的分享觉得也是时候总结一下了。

本次内容整理参考了3306π社区广州站《MGR Best Practice》万里DB CTO娄帅分享和一些个人的理解认识。本次总结内容如下:

  • 一、MySQL InnoDB Cluster架构
  • 二、MGR配置
  • 功能需求配置
  • MGR核心配置
  • 性能优化类参数
  • 配合使用的配置
  • InnoDB配置优化
  • 三、MGR监控注意事项
  • 四、MGR使用上的限制

一、MySQL InnoDB Cluster架构

上图引用于官方手册。该架构中包含:MySQL router, MySQL Servers, MySQL Shell。

MySQL router: 属于一个TCP模型的Proxy主要用于流量中转及基于端口号的读写分离。在3306π社区广州站听官方人员透漏基于shared方式的router也快要出来了,敬请期待。

MySQL Servers: 该部分即为MySQL Group Replication即MGR,这个MGR属于Single primary模式。

MySQL Shell: 用于搭建和管理MGR集群的一个客户端工具集。该工具基本是整合了原来官方的mysql utilis工具集。

二、MGR配置

目前对于使用MGR还是优先建议使用Single Primary结合mysql router使用。MGR本身的是使用InnoDB,所以对于性能方面优化更多还是要关注InnoDB优化,从另一方面讲MGR也是基于GTID复制的另一种实现,另外使用上也有一些限制,我们可以也可以通过明确相应的约束来避免不支持的功能。

2.1 功能需求配置

以下参数是MGR集群限制的必备参数:

  • default_storage_engine=InnoDB

默认引擎使用InnoDB

  • server-id

如果没使用mysqlsh搭建MGR,server_id是auto_increment_offset起始值,所以默认推荐使用1-9的数字,如果使用mysqlsh搭建,该值可以随意处理。

  • binlog_format=row

使用row格式的binlog,这个基本是新功能的标配

  • gtid_mode=on

启用GTID

  • enforce_gtid_consistency=on

配合启用GTID使用

  • sql_require_primary_key=on

每个表必须有主建

  • binlog_cache_size=8M

通过binlog cache size大小把事务限制在8M以内,减少因为写大事务把MGR搞挂的问题。基于这个配置如果遇到大事务应用端直接会收到报错。

  • binlog_checksum=none

低于8.0.21版本以前只能使用binlog_checksum=none,高于等于8.0.21可以启用binlog_checksum=crc32

  • log_error_verbosity=3

MGR还属于新一代的架构,所以运行中建议把log_error_verbosity设置为3,把运行中的INFO也打印到error log中。

2.2 MGR核心配置

  • group_replication_single_primary_mode=ON

推荐使用single primary节点,如果使用mysql router可以透明的实现把写传到mysql上。为了减少BUG,应用上失误建议使用single-primary

  • group_replication_consistency=before

在MGR中如果对于single primary又是大部分时间主节点处理业务,该参数可以配置成: EVENTUAL从而获取最佳的性能;配置成before,是为了保证secondary节点在读写操作时,也不会读以延迟的数据; after是必须保证数据在其它节点应用完毕,这个目前不推荐使用;BEFORE_ON_PRIMARY_FAILOVER这个参数只是保证在故障接管时按BEFORE的逻辑走。目前这块比较完善的处理推荐:eventual 和 before,不推荐使用after。

  • group_replication_group_seeds=node1_IP:PORT;node2_IP:PORT;node3_IP:port

MGR节点成员通信的IP及端口后,所以有成员都要写里面。

  • group_replication_bootstrap_group=off

只有在搭建新集群的过程中打开,新集群搭建完成后立刻关闭,避免节点重启后,搭建新的集群

  • group_replication_transaction_size_limit=150000000

单个事务大小上限,尽量避免大事务出现,只会在commit时报错,事务执行过程中即使超过阈值也无感知。结合着我们上面提供的binlog_cache_size是8M,则我们在开启binlog的情况,可以把事务限制在8M以内。

  • group_replication_communication_max_message_size=10M

大事务拆成多个包传输,拆包的大小为10M。将大事务切分成小包,进行paxos传递。

  • group_replication_flow_control_mode=ON

流控开关,触发流控后,只会延迟等待1s。

  • group_replication_flow_control_certifier_threshold=25000

触发流控的待认证的队列长度。

  • group_replication_flow_control_applier_threshold = 25000

触发流控的待执行的队列长度。

2.3 性能优化类参数

MGR在数据应用实质上还用的sql_thread,所以这块的优化主要是针对复制优化相关的配置。

  • binlog_transaction_dependency_tracking=writeset
  • transaction_write_set_extraction=XXHASH64

启用sql_thread的writeset功能

  • slave_parallel_worker = 8

并行复制中sql_thread线程数,推荐使用CPU core数的2倍即可

  • slave_parallel_type=LOGICAL_CLOCK

配置复制基于事务的并行复制。

  • slave_preserve_commit_order=ON

保证事务并行提交按主库上执行的顺序进行。

  • slave_checkpoint_period=2
  • slave_checkpoint_group=256

加快sql_thread执行过的信息更新到相关的表里,方便看到show slave status及相关监控的数据是更加准确的。

2.4 配合使用的配置

  • group_replication_exit_state_action=READ_ONLY

集群成员退出或是少数派变为read_only不能提供写服务

  • group_replication_unreachable_majority_timeout=10

网络分区时,少数派状等待此时长后,状态变为Error,回滚pending事务

  • group_replication_autorejoin_tries=3

自动尝试连入集群的次数,尝试间隔5s(group_replication_member_expel_timeout),如果设置为0,表示禁用尝试。

  • group_replication_member_expel_timeout=5

将suspicious节点踢出集群的等待时长,如果网络环境一般,可以适当调大30-60,不要太大。如果设置为0,表示不用等待直接进行ERROR状态

  • group_replication_member_weight

选主过程中的节点权重,可以将主机房的节点权重加大

  • clone_valid_donor_list

新加入节点,clone全量数据时,选择的donor节点,尽量使用非主节点作为donor节点

2.5 InnoDB配置优化

InnoDB部分参数供大家参考一下,就不在单独解释了。

  • innodb_buffer_pool_size = 内存的50%-80%
  • innodb_buffer_pool_instances = 4-8
  • innodb_data_file_path = ibdata1:1024M:autoextend
  • innodb_flush_log_at_trx_commit = 1
  • innodb_log_buffer_size = 16M
  • innodb_log_file_size = 1024M
  • innodb_log_files_in_group = 4
  • innodb_max_dirty_pages_pct = 75
  • innodb_file_per_table = 1
  • innodb_rollback_on_timeout
  • innodb_io_capacity = 3000
  • transaction_isolation = READ-COMMITTED
  • innodb_flush_method = O_DIRECT
  • innodb_thread_concurrency= 0
  • innodb_print_all_deadlocks =on
  • innodb_deadlock_detect =on
  • innodb_lock_wait_timeout =30
  • innodb_parallel_read_threads=4

三、MGR监控注意事项

  • 本节点是不是online及成员角色:

select member_host, member_port,member_state, member_role from replication_group_members where member_id=@@server_uuid;

或是

select member_id,member_host, member_port, member_state, member_role from replication_group_members;

  • 当前节点是不是可以写:

select * from performance_schema.global_variables where variable_name in (‘read_only’, ‘super_read_only’);

  • 查看节点上的执行队列情况

select count_transactions_in_queue, count_transactions_remote_in_applier_queue from performance_schema.replication_group_member_stats;

四、MGR使用上的限制

  1. 使用奇数个节点:3,5,7,9 最多9个成员。
  2. 网络稳定,延迟低,尽量避免WAN部署。
  3. 尽量使用单主模式。
  4. 表必须有主键。
  5. 必须使用InnoDB引擎。
  6. BINLOG_FORMAT=ROW。
  7. 必须启用GTID。
  8. 禁止使用外键。
  9. 不支持GAP LOCK ,MGR工作在RC模式。
  10. 在多主模式下DML和DDL对同一个表的操作,必须在同一个节点进行,否则会导致集群会crash。
  11. 在多主模式使用select for update可能会导致整个集群死锁。
  12. 如果配合mysqlshell使用MySQL版本需要8.0.20及以上。
  13. 不支持serializable事务模式。
  14. MGR成员间不支持复制过滤规则。

目前MGR也是官方主推的MySQL InnoDB Cluster主要方案,现在使用的客户比较多,基本可以说比较稳。现在除了官方之外,国内万里数据库也在从事MGR的二次开发工作及商业支持。

本篇完毕,如果有需要补充的地方,可以在评论中交流。如有兴趣,可以添加Wx: 82565387 交流

本文转载自: 掘金

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

再见前端!纯 Java 撸个管理系统,这框架用起来贼爽!

发表于 2021-05-25

SpringBoot实战电商项目mall(40k+star)地址:github.com/macrozheng/…

摘要

一个管理系统,往往需要后端+前端一起实现。单表CRUD操作往往都差不多,我们可以使用代码生成器来实现。有时候我们的管理系统只需要一些简单的CRUD页面,有没有什么框架能做到不写前端代码,纯Java撸个管理系统呢?这里推荐一个全栈类框架Erupt,希望对大家有所帮助!

Erupt简介

Erupt是一个低代码全栈类框架,它使用Java 注解动态生成页面以及增、删、改、查、权限控制等后台功能。零前端代码、零CURD、自动建表,仅需一个类文件 + 简洁的注解配置,快速开发企业级后台管理系统。

基本使用

我们首先来波实战,以商品品牌管理为例,来熟悉下Erupt结合SpringBoot的基本使用!

SpringBoot整合Erupt

由于Erupt原生支持SpringBoot,所以整合还是很方便的!

  • 为了方便管理Erupt版本,我们先在pom.xml中添加Erupt的版本属性;
1
2
3
xml复制代码<properties>
<erupt.version>1.6.13</erupt.version>
</properties>
  • 之后在pom.xml中添加Erupt的权限管理、数据安全、后台WEB界面及MySQL驱动依赖;
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
xml复制代码<dependencies>
<!--用户权限管理-->
<dependency>
<groupId>xyz.erupt</groupId>
<artifactId>erupt-upms</artifactId>
<version>${erupt.version}</version>
</dependency>
<!--接口数据安全-->
<dependency>
<groupId>xyz.erupt</groupId>
<artifactId>erupt-security</artifactId>
<version>${erupt.version}</version>
</dependency>
<!--后台WEB界面-->
<dependency>
<groupId>xyz.erupt</groupId>
<artifactId>erupt-web</artifactId>
<version>${erupt.version}</version>
</dependency>
<!--Mysql数据库驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.15</version>
</dependency>
</dependencies>
  • 修改项目的application.yml文件,添加数据源和JPA配置;
1
2
3
4
5
6
7
8
9
10
yaml复制代码spring:
datasource:
url: jdbc:mysql://localhost:3306/erupt?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
username: root
password: root
jpa:
show-sql: true
generate-ddl: true
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
database: mysql
  • 在项目的resources目录下创建如下配置文件(拷贝mall-tiny-erupt中的即可);

  • 添加Erupt的Java配置类EruptConfig,以启动类MallTinyApplication的包为准,配置包扫码路径;
1
2
3
4
5
6
7
8
9
java复制代码/**
* Created by macro on 2021/4/13.
*/
@Configuration
@ComponentScan({"xyz.erupt","com.macro.mall.tiny"})
@EntityScan({"xyz.erupt","com.macro.mall.tiny"})
@EruptScan({"xyz.erupt","com.macro.mall.tiny"})
public class EruptConfig {
}
  • 在MySQL中创建erupt数据库,之后使用启动类运行该项目,在erupt数据库中会自动创建如下表;

  • 项目启动成功后,可以直接访登录页,默认账号密码erupt:erupt,项目访问地址:http://localhost:8080/

  • 登录成功后会跳转到项目主页,我们可以发现没有写一行前端代码,却拥有了完整的权限管理和字典管理功能,是不是很棒!

实现单表 CRUD

使用核心注解@Erupt和@EruptField定义一个实体类即可快速完成CRUD操作,让我们以商品品牌管理为例试试吧。

  • 不需要Controller、Service、Dao,仅仅一个实体类即可完成CRUD,首先我们创建实体类PmsBrand;
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
java复制代码@Erupt(name = "商品品牌")
@Table(name = "pms_brand")
@Entity
public class PmsBrand {

@Id
@GeneratedValue(generator = "generator")
@GenericGenerator(name = "generator", strategy = "native")
@Column(name = "id")
@EruptField
private Long id;

@EruptField(
views = @View(title = "品牌名称"),
edit = @Edit(title = "品牌名称",notNull=true,search = @Search(vague = true))
)
private String name;

@EruptField(
views = @View(title = "品牌首字母"),
edit = @Edit(title = "品牌首字母",notNull=true)
)
private String firstLetter;

@EruptField(
views = @View(title = "品牌LOGO"),
edit = @Edit(title = "品牌LOGO", type = EditType.ATTACHMENT,
attachmentType = @AttachmentType(type = AttachmentType.Type.IMAGE))
)
private String logo;

@EruptField(
views = @View(title = "品牌专区大图"),
edit = @Edit(title = "品牌专区大图", type = EditType.ATTACHMENT,
attachmentType = @AttachmentType(type = AttachmentType.Type.IMAGE))
)
private String bigPic;

@EruptField(
views = @View(title = "品牌故事"),
edit = @Edit(title = "品牌故事")
)
private String brandStory;

@EruptField(
views = @View(title = "排序"),
edit = @Edit(title = "排序")
)
private Integer sort;

@EruptField(
views = @View(title = "是否显示"),
edit = @Edit(title = "是否显示")
)
private Boolean showStatus;

@EruptField(
views = @View(title = "品牌制造商"),
edit = @Edit(title = "品牌制造商")
)
private Boolean factoryStatus;

private Integer productCount;

private Integer productCommentCount;

}
  • 创建成功后重启项目,在菜单维护中添加一个叫商品的一级菜单;

  • 然后再添加一个叫品牌管理的二级菜单,注意选择好菜单类型和上级菜单,输入类型值为实体类的类名称PmsBrand;

  • 菜单添加成功后,刷新页面,完整的品牌管理功能就出现了,来试下新增;

  • 再看下查询列表页面,可以发现我们通过@Edit注解,将实体类的字段转换成了不同的输入控件,比如文本框、图片上传框、单选框和数值框。

核心注解说明

几个Erupt的核心注解,对照PmsBrand中的代码学习即可!

@Erupt

  • name:功能名称
  • desc:功能描述

@EruptField

  • views:表格展示配置
  • edit:编辑项配置
  • sort:前端展示顺序,数字越小越靠前

@View

  • title:表格列名称
  • desc:表格列描述
  • type:数据展示形式,默认为AUTO,可以根据属性类型自行推断
  • show:是否显示

@Edit

  • title:表格列名称
  • desc:表格列描述
  • type:编辑类型,默认为AUTO,可以根据属性类型自行推断
  • show:是否显示
  • notNull:是否为必填项
  • search:是否支持搜索,search = @Search(vague = true)会启用高级查询策略

扩展模块

当然Erupt的功能远不止于此,还集成了很多实用的系统功能,包括定时任务、代码生成器、系统监控及NoSQL支持等。

定时任务erupt-job

通过定时任务功能,我们可以在代码中定义好定时任务,然后在图形化界面中操作任务,有点之前讲过的PowerJob 的感觉!

  • 首先我们需要在pom.xml中添加erupt-job相关依赖;
1
2
3
4
5
6
xml复制代码<!--定时任务erupt-job-->
<dependency>
<groupId>xyz.erupt</groupId>
<artifactId>erupt-job</artifactId>
<version>${erupt.version}</version>
</dependency>
  • 之后在application.yml中添加邮件配置(否则启动会报错);
1
2
3
4
5
6
7
8
9
10
yaml复制代码spring:
mail:
username: xxxxxx@qq.com
password: 123456
host: smtp.exmail.qq.com
port: 465
properties:
mail.smtp.ssl.auth: true
mail.smtp.ssl.enable: true
mail.smtp.ssl.required: true
  • 之后创建一个定时任务实现类JobHandlerImpl,在exec方法中添加定时任务执行代码;
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码/**
* Created by macro on 2021/4/13.
*/
@Service
@Slf4j
public class JobHandlerImpl implements EruptJobHandler {
@Override
public String exec(String code, String param) throws Exception {
log.info("定时任务已经执行,code:{},param:{}",code,param);
return "success";
}
}
  • 之后重新启动应用,在任务维护中添加一个定时任务,每5秒执行一次;

  • 添加成功后,定时任务开始执行,点击任务列表中的日志按钮即可查看执行日志。

代码生成器erupt-generator

如果你觉得手写实体类比较麻烦的话,还可以用用Erupt中的代码生成器。

  • 在pom.xml中添加erupt-generator相关依赖;
1
2
3
4
5
6
xml复制代码<!-- 代码生成器 erupt-generator -->
<dependency>
<groupId>xyz.erupt</groupId>
<artifactId>erupt-generator</artifactId>
<version>${erupt.version}</version>
</dependency>
  • 在代码生成菜单中我们可以像在Navicat中一样,直接添加表和字段,从而生成实体类代码;

  • 我们在添加过程中可以发现,Erupt支持的编辑类型还挺多的,多达30种;

  • 添加成功后,点击列表项的代码预览按钮可以直接生成代码,复制到自己项目下即可。

系统监控erupt-monitor

通过使用Erupt的系统监控功能,我们可以查看服务器的配置、Redis的缓存使用情况和在线用户信息。

  • 在pom.xml中添加erupt-monitor相关依赖;
1
2
3
4
5
6
xml复制代码<!--服务器监控 erupt-monitor-->
<dependency>
<groupId>xyz.erupt</groupId>
<artifactId>erupt-monitor</artifactId>
<version>${erupt.version}</version>
</dependency>
  • 由于需要使用到Redis,所以要在application.yml中添加Redis配置,并开启Session的Redis存储功能;
1
2
3
4
5
6
7
8
9
10
yaml复制代码spring:
redis:
host: localhost # Redis服务器地址
database: 1 # Redis数据库索引(默认为0)
port: 6379 # Redis服务器连接端口
password: 123456 # Redis服务器连接密码(默认为空)
timeout: 3000ms # 连接超时时间
erupt:
# 开启redis方式存储session,默认false,开启后需在配置文件中添加redis配置
redisSession: true
  • 通过服务监控菜单,可以查看到服务器的CPU、内存和Java虚拟机信息;

  • 通过缓存监控菜单,可以查看到Redis信息、命令统计和Redis Key统计;

  • 通过在线用户菜单,可以查看到在线用户信息,还可以让用户强行退出!

NoSQL数据源erupt-mongodb

Erupt支持多种数据源,包括:MySQL、Oracle、PostgreSQL、H2,甚至支持 MongoDB。下面我们来体验下MongoDB的支持功能。

  • 在pom.xml中添加erupt-mongodb相关依赖;
1
2
3
4
5
6
xml复制代码<!--NoSQL数据源 erupt-mongodb-->
<dependency>
<groupId>xyz.erupt</groupId>
<artifactId>erupt-mongodb</artifactId>
<version>${erupt.version}</version>
</dependency>
  • 由于需要使用到MongoDB,所以要在application.yml中添加MongoDB配置;
1
2
3
4
5
6
yaml复制代码spring:
data:
mongodb:
host: localhost # mongodb的连接地址
port: 27017 # mongodb的连接端口号
database: erupt # mongodb的连接的数据库
  • 以一个简化版的商品管理为例,还是熟悉的套路,添加一个PmsProduct实体类;
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
java复制代码/**
* Created by macro on 2021/4/13.
*/
@EruptDataProcessor(EruptMongodbImpl.MONGODB_PROCESS) //此注解表示使用MongoDB来存储数据
@Document(collection = "product")
@Erupt(
name = "商品管理",
orderBy = "sort"
)
public class PmsProduct {
@Id
@EruptField
private String id;

@EruptField(
views = @View(title = "商品名称", sortable = true),
edit = @Edit(title = "商品名称", search = @Search(vague = true))
)
private String name;

@EruptField(
views = @View(title = "副标题", sortable = true),
edit = @Edit(title = "副标题", search = @Search(vague = true))
)
private String subTitle;

@EruptField(
views = @View(title = "价格", sortable = true),
edit = @Edit(title = "价格")
)
private Double price;

@EruptField(
views = @View(title = "商品图片"),
edit = @Edit(title = "商品图片", type = EditType.ATTACHMENT,
attachmentType = @AttachmentType(type = AttachmentType.Type.IMAGE))
)
private String pic;

@EruptField(
views = @View(title = "状态", sortable = true),
edit = @Edit(title = "状态",
boolType = @BoolType(trueText = "上架", falseText = "下架"),
search = @Search)
)
private Boolean publishStatus;

@EruptField(
views = @View(title = "创建时间", sortable = true),
edit = @Edit(title = "创建时间", search = @Search(vague = true))
)
private Date createTime;
}
  • 与之前操作MySQL的区别是通过@EruptDataProcessor注解指定用MongoDB来存储数据,@Table注解改为使用@Document注解;
1
2
3
4
5
6
7
8
9
java复制代码@EruptDataProcessor(EruptMongodbImpl.MONGODB_PROCESS)  //此注解表示使用MongoDB来存储数据
@Document(collection = "product")
@Erupt(
name = "商品管理",
orderBy = "sort"
)
public class PmsProduct {
//...省略若干代码
}
  • 接下来就是在菜单维护里面添加一个商品管理的菜单,刷新一下就可以看到该功能了。

在线接口开发erupt-magic-api

最后再介绍一个神奇的功能,直接通过UI界面来开发接口,无需定义Controller、Service、Dao、Mapper、XML、VO等Java对象!

  • 在pom.xml中添加erupt-magic-api相关依赖;
1
2
3
4
5
6
xml复制代码<!--在线接口开发 erupt-magic-api-->
<dependency>
<groupId>xyz.erupt</groupId>
<artifactId>erupt-magic-api</artifactId>
<version>${erupt.version}</version>
</dependency>
  • 在application.yml中添加magic-api相关配置;
1
2
3
4
5
6
7
8
9
yaml复制代码erupt:
# 设置具体哪些包被jackson消息转化而不是gson
jacksonHttpMessageConvertersPackages:
- org.ssssssss

magic-api:
web: /magic/web
# 接口配置文件存放路径
resource.location: D:/erupt/magic-script
  • 我们可以直接通过magic-api自己定义的脚本来实现查询,比如下面这个脚本,用于查询全部品牌;
1
2
javascript复制代码var sql = "select * from pms_brand";    
return db.select(sql);
  • 在接口配置菜单中直接添加该脚本即可实现品牌列表查询接口,无需额外编写代码;

  • 在浏览器中直接访问接口,发现已经自动生成接口,是不是很棒!

总结

如果你的需求是搭建一个业务并不复杂的后台管理系统,Erupt是一个很好的选择!它能让你不写前端代码!但是如果你的需求方对界面有很多要求,而你的业务逻辑又比较复杂的话那就要自己实现前端了!

参考资料

官方文档:www.yuque.com/erupts/erup…

项目源码地址

github.com/macrozheng/…

本文 GitHub github.com/macrozheng/… 已经收录,欢迎大家Star!

本文转载自: 掘金

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

字节面试:什么是责任链模式?

发表于 2021-05-25

微信搜索【三太子敖丙】关注这个贪财好色的程序员。

本文 GitHub github.com/JavaFamily 已收录,有一线大厂面试完整考点、资料以及我的系列文章。

前言

面试经历大家肯定都有过,但是面试的流程其实跟一种设计模式很像,每一轮的面试官都有自己的职责,一个求职者面试经历的过程就好比一次客户端的请求过程。

在设计模式系列的文章中之前已经为大家分享了创建型设计模式,感兴趣的小伙伴们可以再去翻看之前的分享。接下来开始分享设计模式三大类型中的行为型模式了,今天要分享的是责任链模式

大纲

定义

什么是责任链?它的原理是什么?

将请求的发送和接收解耦,让多个接收对象都有机会处理这个请求。将这些接收对象串成一条链,并沿着这条链传递这个请求,直到链上的某个接收对象能够处理它为止。

以上定义来自《设计模式之美》

再看看一张官方图解吧

  • Client(客户端):实例化一个处理器的链,在第一个链对象中调用handleRequest 方法。
  • Handle(处理器):抽象类,提供给实际处理器继承然后实现handleRequst方法,处理请求
  • ConcreteHandler(具体处理器):继承了handler的类,同时实现handleRequst方法,负责处理业务逻辑类,不同业务模块有不同的ConcreteHandler。

这么看结构其实还是比较简单的,但是我们还是拿面试的流程来模拟一下责任链吧!

代码实现

假设现在去一家公司面试,第一次去一面,第二次去二面,第三次去直接过了。那这个模拟面试代码怎么写呢?

1
2
3
4
5
6
7
8
9
java复制代码public abstract class Handler {

protected Handler handler;

public void setHandler(Handler handler) {
this.handler = handler;
}
public abstract void handleRequest(Integer times);
}

首先我们还是定义一个抽象Handler处理器,同时添加一个抽象处理方法 handleRequest,后面我只需要编写具体的处理器来继承Handler类

1
2
3
4
5
6
7
8
9
10
11
java复制代码public class FirstInterview extends Handler {
@Override
public void handleRequest(Integer times) {
// 条件判断是否是属于当前Handler的处理范围之内,不是则向下传递Handler处理器
if(times ==1){
// 假设这里是处理的业务逻辑代码
System.out.println("第一次面试"+times);
}
handler.handleRequest(times);
}
}

其次构建第一次面试Handler,内部实现handleRequest方法,判断一下是否是当前处理应该处理的业务逻辑,不是则向下传递。同样的第二次的SecondInterview和FirstInterview代码基本是一致的,我就不给大家贴出来了,直接看最后一个

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
java复制代码public class ThreeInterview extends Handler {
@Override
public void handleRequest(Integer times) {
if (times == 3) {
System.out.println("第三次面试"+ times + ",恭喜面试通过,HR会跟你联 系!!!");
}
}

public static void main(String[] args) {
Handler first = new FirstInterview();
Handler second = new SecondInterview();
Handler three = new ThreeInterview();
first.setHandler(second);
second.setHandler(three);

// 第一次面试
first.handleRequest(1);
System.out.println();
// 第二次面试
first.handleRequest(2);
System.out.println();
// 第三次面试
first.handleRequest(3);
System.out.println();
}
}

这个结果可以很明显的看出,根据我们传参,不同的Handler根据自己的职责处理着自己的业务,这就是责任链。

框架的应用

责任链在很多框架源码中也有体现。比如开始学SpringMVC中的 ServletFilter

以及Spring中的 SpringInterceptor 这里面其实都是运用了责任链模式的思想,达到框架的可扩展性的同时也遵循着开闭原则。

作为常见的RPC框架的DUBBO其实里面也同样有这个责任链的思想。

给大家一个思考问题?

dubbo服务一旦暴露出去了,那么基本任何服务都能调用,但是在一些特殊的业务中需要我们暴露服务,但是又不希望被不了解业务的人随便调用。

比如:商品的库存修改的dubbo服务,我们只允许下单,购物车,添加修改商品等一些指定场景可以调用。

那么有什么办法,在Provider这端做好拦截,针对特定的服务才允许调用,否则拦截下来不允许执行?

第一种方法,添加服务名称APP_NAME作为传参校验,这是很常见也最容易想到的办法。

第二种方法,实现一个DUBBO拦截器,对RPC调用进行选择性过滤。

针对上面的两种方法,给大家详细讲讲第二种方法具体怎么实现,每个公司都会基于现有的DUBBO源码做自己的特定化改动,那么第二种方式也是同样需要我们改动线有dubbo源码。

先修改ConsumerContextFilter消费者拦截器

这里我们以dubbo的2.7.19版本为例。在ConsumerContextFilter中添加APP_NAME至Attachments中,那么作为本次的RPC调用都能从Attachments中获取到我们塞入的值。

至于这个APP_NAME的获取 可以通过 System.getProperty(“project.name”, “”) 来获取服务名

这里我就不对DUBBO做过多的展开,大家如果有强烈建议讲解。那么在结束设计模式再跟大家详细剖析一下dubbo,以及zookeeper里面的ZAB,一致性选举算法等等。

CONSUMER既然已经填充了服务名称,那么在Provider同样的也就只需要写一个ProviderFilter 就可以了

这里就基本实现怎么处理每一次RPC调用的拦截了,然后想要那个服务拦截,在provider里面的filter里面指定一下这个DubboProviderFilter就可以了,也可以全局都实现。

注意 :这个Filter 要是用DUBBO包里面的,不要搞错了。

现实业务改造举例

框架中既然都有这种思想,那么怎么运用到业务代码中呢?

还是给大家举一个例子:

商品详情展示我们可以是分模块展示的,比如头图,商品信息,sku信息,配送地址,分期付费等等。

那么怎么进行组装到商品详情的展示呢?

1
2
3
4
5
java复制代码public abstract class AbstractDataHandler<T> {

// 处理模块化数据
protected abstract T doRequest(String query) throws Exception;
}

首先我们还是定一个抽象数据Handler,然后分别建立ItemInfoHandler 和SkuInfoHandler 来继承抽象处理器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Component
public class ItemInfoHandler extends AbstractDataHandler<ItemInfoHandler.ItemInfo> {
@Override
protected ItemInfoHandler.ItemInfo doRequest(String query) {
ItemInfoHandler.ItemInfo info = new ItemInfo();
info.setItemId(123456L);
info.setItemName("测试商品");
return info;
}

@Data
public static class ItemInfo {
private Long itemId;
private String itemName;
}
}

同样SkuInfoHandler类也是一样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scala复制代码@Component
public class SkuInfoHandler extends AbstractDataHandler<SkuInfoHandler.SkuInfo> {
@Override
protected SkuInfoHandler.SkuInfo doRequest(String query) {
SkuInfoHandler.SkuInfo info = new SkuInfoHandler.SkuInfo();
info.setSkuId(78910L);
info.setSkuName("测试SKU");
return info;
}
@Data
public static class SkuInfo {
private Long skuId;
private String skuName;
}
}

最后就是我们的测试代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Component
public class DataAggregation {
@Autowired
private SkuInfoHandler skuInfoHandler;
@Autowired
private ItemInfoHandler itemInfoHandler;

public Map convertItemDetail() throws Exception {
Map result = new HashMap();
result.put("skuInfoHandler", skuInfoHandler.doRequest("模拟数据请求"));
result.put("itemInfoHandler",itemInfoHandler.doRequest("模拟数据请求"));
return result;
}

public static void main(String[] args) throws Exception {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
DataAggregation dataAggregation = (DataAggregation) applicationContext.getBean("dataAggregation");
Map map = dataAggregation.convertItemDetail();
System.out.println(JSON.toJSONString(map));
// 打印的结果数据
// {"skuInfoHandler":{"skuId":78910,"skuName":"测试SKU"},"itemInfoHandler":{"itemId":123456,"itemName":"测试商品"}}
}
}

这个例子其实是经过一点小小的改动的,我们没有通过向下传递处理器的方式,而是通过实际业务逻辑在 convertItemDetail 的方法中去构建每个模块的数据,最后返回出一个Map结构数据。

这里其实还有另外的一种写法,把每一个需要处理的Handler 可以加载到一个List容器中,然后循环调用每个Handler中的doRequest方法,当然这是针对一些其他的业务场景这么写。

看完大家也能发现其实每个Handler是可以共用的,每一块业务的代码逻辑非常的清晰,这样的代码写出来就感觉很舒服了。

总结

设计模式不是一成不变的,只有适合自己当前业务的模式才是最好的模式。理解前辈的思想,组合我们自己需要的模式。

本次分享就到这里了,后面接着为大家分享行为型设计模式。

我是敖丙,你知道的越多,你不知道的越多,感谢各位人才的:点赞、收藏和评论,我们下期见!


文章持续更新,可以微信搜一搜「 三太子敖丙 」第一时间阅读,回复【资料】有我准备的一线大厂面试资料和简历模板,本文 GitHub github.com/JavaFamily 已经收录,有大厂面试完整考点,欢迎Star。

本文转载自: 掘金

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

前缀树/字典树的介绍及其简单应用

发表于 2021-05-24

介绍

前缀树是什么呢,先看一段维基百科上对它的定义:

在计算机科学中,trie,又称前缀树或字典树,是一种有序树,用于保存关联数组,其中的键通常是字符串。与二叉查找树不同,键不是直接保存在节点中,而是由节点在树中的位置决定。一个节点的所有子孙都有相同的前缀,也就是这个节点对应的字符串,而根节点对应空字符串。一般情况下,不是所有的节点都有对应的值,只有叶子节点和部分内部节点所对应的键才有相关的值。

要理解它首先要知道我们用这个东西的目的是什么,我举一个最直观的例子,比如要查询某个单词,很多搜索框都有搜索提示。你想查询“Hello”这个单词,当你输入“Hel”甚至输入“H”时可能提示框就已经出现了“Hello”这个单词。前缀树最常用的应用就在这里,即通过某一前缀去查到该前缀底下对应的有什么单词。有的人可能会问“那我直接把所有单词存在一个数组或者List,遍历一遍不是也能查到吗!”,确实,但你不觉得这样在查找某个单词的时候其实花了很多时间访问到了很多无用数据吗,十分的浪费时间。使用前缀树可以将查找某个单词的时间复杂度降到 O(logn)。

简单来说,首先它是一棵树,其次树中的每个结点都是当前的前缀,这样便于我们用更少的时间去查找某个元素。我再画一张图加深一下理解,为了图的简洁,假设当前单词库里就三个单词,“hi”、“me”和“min”。

好,我的废话可能有点多了,那么接下来就来看如何实现它。

实现

首先前缀树还是一课树,所以肯定少不了结点的插入、查找、删除等等。懂的了前缀树的本质,实现这些其实就是在树这个数据结构的基础上简单改变一下即可,LC208可以很好的练习前缀树的实现,这一节也根据这个来实现一个简单的前缀树。

定义数据结构

首先来定义结点,一颗树中的结点必然要有孩子结点,所以要有孩子结点的数组(或者列表),这里因为数据是英文单词,只有26个字母可以转移,所以定义一个长度为26的数组即可。

TrieNode[] childs = new TrieNode[26];

其次还需要知道当前是前缀还是已经查询到了结果,所以需要一个 boolean 来表示是否为需要查询的单词。

boolean isEnd;

初始化

创建一个根结点即可。

1
2
3
4
java复制代码/** Initialize your data structure here. */
public Trie() {
root = new TrieNode();
}

插入

插入一个新数据时,将单词拆分成一个个字母,根据当前树中的结点判断:

  • 若当前前缀已存在,则直接进入该结点;
  • 若当前前缀不存在,则插入新结点,记录当前前缀。
  • 当遍历到单词最后一个字母时,记录当前结点为一个单词。

例如在上面的例子中插入“him”这个单词如下图所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode curr = root;

for (char c : word.toCharArray()) {
if (curr.childs[c - 'a'] == null) {
curr.childs[c - 'a'] = new TrieNode();
}
curr = curr.childs[c - 'a'];
}

curr.isEnd = true;
}

查找

查找一个数据时,只需一个一个找结点匹配前缀即可,如下是查找到“min”这个单词的过程。

如下是查找“mid”这个单词失败的过程。

1
2
3
4
5
6
7
8
9
10
11
java复制代码/** Returns if the word is in the trie. */
public boolean search(String word) {
TrieNode curr = root;

for (char c : word.toCharArray()) {
if (curr.childs[c - 'a'] == null) return false;
curr = curr.childs[c - 'a'];
}

return curr.isEnd;
}

完成

到这就完成了简单的查找单词的前缀树/字典树,完整代码如下:

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复制代码class Trie {

class TrieNode {
boolean isEnd;
TrieNode[] childs = new TrieNode[26];
}

TrieNode root;

/** Initialize your data structure here. */
public Trie() {
root = new TrieNode();
}

/** Inserts a word into the trie. */
public void insert(String word) {
TrieNode curr = root;

for (char c : word.toCharArray()) {
if (curr.childs[c - 'a'] == null) {
curr.childs[c - 'a'] = new TrieNode();
}
curr = curr.childs[c - 'a'];
}

curr.isEnd = true;
}

/** Returns if the word is in the trie. */
public boolean search(String word) {
TrieNode curr = root;

for (char c : word.toCharArray()) {
if (curr.childs[c - 'a'] == null) return false;
curr = curr.childs[c - 'a'];
}

return curr.isEnd;
}

/** Returns if there is any word in the trie that starts with the given prefix. */
public boolean startsWith(String prefix) {
TrieNode curr = root;

for (char c : prefix.toCharArray()) {
if (curr.childs[c - 'a'] == null) return false;
curr = curr.childs[c - 'a'];
}

return true;
}
}

应用

前缀树的应用还是很多的,除了上述的搜索提示外,还可以用于诸如如下场景:

  • 字符串排序:字符串按字典顺序排序,可以使用前缀树
  • 查找两个字符串的最长公共前缀
  • 查找某二进制数,且要按前缀顺序查找

这里,我重点介绍一下查找某二进制数这个应用

二进制数上的应用

这道LC421就淋漓尽致地展现了前缀树在二进制数上的应用。题目如下:

这道题需要我们找到两个数的最大异或结果,且只能遍历一次数组。若是单纯比较两个数大小我们会怎么比呢?比如 123 和 120 ,我们会先看百位,都是 1,那么再比较十位,都是 2,再比较个位,3 肯定比 0 大,于是我们得出结论:123 > 120。同样的,在二进制数上也是一样的比较方式,比如 111 与 110,先比较最高位都是 1,再比较第二高位,也都是 1,再比较最后一位 1 > 0,于是我们知道 111 > 110。

到这里,是不是就清楚了前缀树该怎么用了。正因为我们要从最高位比到最低位,前面的几个高位不就相当于是一个前缀吗,所以用上前缀树,我们可以遍历一遍数组,一边往树里添加数字,一遍从中寻找最大的异或值,这题就解出来了。

那么如何去寻找最大的异或值?我们都知道异或的计算是对应二进制位若相同则取0,不同则取1。我们要找最大的结果,那么肯定要尽量在高位找 1 呀。所以在我们查找时,寻找与当前数字当前位不同的即可,比如当前位是 0,那么我们就找 1;如果当前为是 1,那么我们就找 0。当然,如果找不到那就只能取一样的了,这一位总不能没有值。

下图我以查找 0100 的最大异或结果为例做一个演示:

完整代码如下:

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

private final int MAX_BIT = 30; // 最大位数

private Trie root = new Trie();

public int findMaximumXOR(int[] nums) {
int ans = 0;

for (int num : nums) {
add(num);
// 每加入一个数字找一下这个数字和当前前缀树里的数的最大异或结果
ans = Math.max(ans, find(num));
}

return ans;
}

public void add(int num) {
Trie cur = root;

// 从最高位到最低位
for (int k = MAX_BIT; k >= 0; k--) {
int bit = (num >> k) & 1; // 计算当前位是什么

if (bit == 0) {
if (cur.left == null)
cur.left = new Trie(); // 如果不存在这个结点,就创建一个
cur = cur.left;
} else {
if (cur.right == null)
cur.right = new Trie(); // 如果不存在这个结点,就创建一个
cur = cur.right;
}
}
}

public int find(int num) {
Trie cur = root;
int res = 0;

// 从最高位到最低位
for (int k = MAX_BIT; k >= 0; k--) {
int bit = (num >> k) & 1; // 计算当前位是什么

if (bit == 0) {
// 当前位是 0,找 1
if (cur.right != null) {
res = (res << 1) + 1; // 找到 1 了,这一位可以直接置为 1
cur = cur.right;
} else {
// 没找到
res = res << 1; // 没有找到 1 ,这一位就只能是 0 了
cur = cur.left;
}
} else {
// 当前位是 1,找 0
if (cur.left != null) {
res = (res << 1) + 1; // 找到 0 了,这一位可以直接置为 1
cur = cur.left;
} else {
// 没找到
res = res << 1; // 没有找到 0 ,这一位就只能是 0 了
cur = cur.right;
}
}
}

return res;
}
}

class Trie {
Trie left = null; // 记录该位为 0
Trie right = null; // 记录该位为 1
}

进阶

看到这里,如果你觉得你懂了,跃跃欲试,就来试试LC1707吧,和上面那个如出一辙,都是寻找最大异或值,只是在此基础之上加了点限制。

总结

这篇文章也是入门了一下前缀树,其实它没有那么难理解,关键就在于如何应用它,前缀该如何定义?把到每一个结点的过程可以看作是一个状态的转移,那么在每一步又有多少种状态可以转移?像在单词的应用中,每一步有 26 个方向可以转移,即 26 个字母,而在二进制数中,每一步仅有两个方向可以转移,即 0 和 1。思考清楚这些,相信会对前缀树有更好的理解并且可以有更高级的应用。

本文转载自: 掘金

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

万字长文带你彻底搞懂什么是 DevOps

发表于 2021-05-24

分享 Java、.NET、Javascript、效率、软件工程、编程语言等技术知识。

本文 GitHub TechShare 已收录,没有广告,Just For Fun。

DevOps 日渐成为研发人员耳熟能详的一个组合词,但什么是 DevOps,为什么 DevOps 对于互联网企业如此重要,真正将其思考透彻的人却不多,带着这些困惑,本文将带你一探 DevOps 的起源、原则和实践,让你搞清楚到底何为 DevOps。

DevOps 的起源可以追溯到 2008 年,在一次敏捷大会的敏捷基础设施话题组被提及,从起源我们可以了解到 DevOps 的发展跟敏捷软件开发是密不可分的。

DevOps 定义

DevOps 经过这些年的发展,其定义也在不断变化,先来看三段 DevOps 的 wiki 定义。

  1. DevOps 2017 - 2020 年英文 wiki 定义(直译)

DevOps是一种软件工程文化和实践(Practices),旨在整合软件开发和软件运维。DevOps运动的主要特点是强烈倡导对构建软件的所有环节(从集成、测试、发布到部署和基础架构管理)进行全面的自动化和监控 DevOps 的目标是缩短开发周期,提高部署频率和更可靠地发布,与业务目标保持一致。
2. DevOps 2021 年英文 wiki 定义(直译)

DevOps 是一系列整合软件开发和软件运维活动的实践(Practices)。目标是缩短软件开发生命周期并使用持续交付提供高质量的软件。

另:

DevOps 与敏捷软件开发是互补关系,DevOps 的许多方面来自于敏捷方法论。
3. DevOps 中文 wiki 定义

DevOps(Development和Operations的组合词)是一种重视“软件开发人员(Dev)”和“IT运维技术人员(Ops)”之间沟通合作的文化、运动或惯例。透过自动化“软件交付”和“架构变更”的流程,来使得构建、测试、发布软件能够更加地快捷、频繁和可靠。

提取这三段的共同点,可以看到不论定义如何变化,DevOps 所要实现的目标都是一致的——缩短软件开发生命周期并使用 持续交付 提供高质量的软件。由于持续交付活动中包含了构建、测试和发布等活动,我更倾向于用这个定义,可以更好地缩减定义长度。

另外可以看到英文直接翻译过来的定义中都包含「实践」 一词,而中文 wiki 经过一定的翻译或本地化后变成了「文化、运动或惯例」,其还更强调开发运维之间 沟通合作 这一点,因此将最新的英文 wiki 定义与中文 wiki 定义相结合,可以帮助我们更好地理解 DevOps,那么它的最终定义是什么就交由读者朋友自己去领会吧。

DevOps 发展背景

为什么 DevOps 会如此热门,时常被人所提及,这与其发展背景是分不开的,主要原因可以概括为以下几点:

  1. 敏态需求的增加,即探索性工作的增加;
* 软件开发从传统的瀑布流方式到敏捷开发,再到现在对敏捷开发提出了更高的要求,近些年创新型的应用不断涌现,在这些应用的研发过程中多采用小步快跑、快速试错的方式,这些探索性工作要求运维能够具备一天发布多次的能力,需要企业完成由稳态到敏态的转变。
  1. 软件开发活动在企业经营活动中占比的不断增加;
* 业务发展对软件的依赖由轻度依赖、中度依赖发展到目前的重度依赖。
  1. 企业存在对消除浪费的需求。
* 软件开发活动在企业中的位置越来越重要,而像企业经营活动一样,软件开发活动中也存在着许多的浪费,企业管理上必然存在着 **识别并消除浪费** 的需求。
* 软件开发中的浪费包括不必要和必要的浪费,不必要的浪费有:无人使用的功能、软件bug、等待测试、等待审批等;必要的浪费包括:工作项移交、测试、项目管理等。

以上主要从企业的角度说明了 DevOps 的发展,这是较为深层次的原因,表层的推动因素包括:容器化技术的发展、微服务架构的发展等等,这些技术上的创新为 DevOps 提供了良好的发展条件,以解决企业面临的这些问题。

DevOps 原则与实践

了解了什么是 DevOps 及其发展原因后,又该如何具体的进行 DevOps 实践,我们采用黄金圈法则来思考这一问题。

golden_circle.png

DevOps 原则是总体指导思想,实践是具体的执行方法,DevOps 是一个动态的过程,在进行相关实践的时候可以看看其应用了哪些原则,当违背原则的时候需要思考实践的合理性。

DevOps 原则

DevOps 包含以下三大原则:

  1. 流动原则:加速 从开发、运维到交付给客户的流程;
  2. 反馈原则:建设 安全可靠 的工作体系;
  3. 持续学习与实验原则:采用科学的工作方式,将对组织的 改进和创新 作为工作的一部分。

流动原则

  1. 坚持少做
* 产品开始开发时采用 MVP 原则。
* 产品迭代时要适时做减法。
  1. 持续分解问题
* 大的变更或需求拆解为一系列小的变更,快速解决。
  1. 工作可视化
* 采用 Sprint 看板将工作可视化。
  1. 控制任务数量
* 减少前置时间,降低测试人员的等待时间。
* 任务越多,预估越不准确。
  1. 减少交接次数
* 减少不必要的沟通和等待。
  1. 持续识别和改善约束点
* 识别出影响流动的主要前置因素,比如搭建环境、需求文档。
* QA、开发、运维、产品持续提升生产力。
* 为非功能性需求预留20%的开发时间,减少技术债务。
  1. 消除价值流中的困境和浪费(导致交付延迟的主要因素)
* 半成品——未完全完成的工作。
* 额外工序——从不使用的文档、重复编写接口文档等。
* 额外功能——用户实际不需要的功能。
* 任务切换——将人员分配到多个项目或截然不同的工作任务中。
* 等待、移动、缺陷、非标准化的手动操作。

反馈原则

  1. 在复杂系统中安全地工作
* 管理复杂的工作,识别出设计和操作的问题;
* 群策群力解决问题,从而快速构建新知识;
* 在整个组织中,将区域性的知识应用到全局范围;
* 领导者要持续培养有以上才能的人。
  1. 及时发现问题
* 快速、频繁和高质量的信息流——每个工序的操作都会被度量和监控。
* 技术价值流的每个阶段(产品管理、开发、QA、安全、运维),建立快速的反馈和前馈回路(包括自动化构建、集成和测试过程)。
* 全方位的遥测系统。
  1. 在源头保障质量
* 过多的检查和审批流程,使得做决策的地方远离执行工作的地方,这导致流程有效性降低,减弱了因果关系之间反馈的强度。
* 让开发人员也对系统质量负责,快速反馈,加速开发人员的学习。
  1. 为内部客户优化工作
* 运维的非功能性需求(如架构、性能、稳定性、可测试性、可配置性和安全性)与用户功能同样重要。

持续学习与实验原则

  1. 建立学习型组织和安全文化
  2. 将日常工作的改进制度化
  3. 把局部发现转化为全局优化
  4. 在日常工作中注入弹性模式
    • 缩短部署的前置时间、提高测试覆盖率、缩短测试执行时间,甚至在必要时解耦架构,都属于在系统中引入类似张力的做法。
  5. 领导层强化学习文化
    • 领导者帮助一线工作者在日常工作中发现并解决问题。

DevOps 实践

基于 DevOps 的相关原则,有与其对应的实践,包括:流动的技术实践、反馈的技术实践和持续学习与实验的技术实践。在应用这些实践之前还需认真设计组织结构,使其有利于实践的开展。

设计组织结构

  • 利用康威定律设计团队结构。
    • 康威定律:软件的架构和软件团队的结构是一致的。
    • 软件的架构应该保证小团队能够独立运作,彼此充分解耦,从而避免过多不必要的沟通和协调。
  • 过度职能导向(成本优化)的危害。
    • 执行工作的人通常不理解自己的工作与价值流目标的关系(“我之所以要配置这台服务器,是因为别人要我这么做”)。
    • 如果运维部门的每个职能团队都要同时服务于多个价值流(即多个开发团队),那么问题更是雪上加霜,因为所有团队的时间都很宝贵。
  • 组建以市场为导向的团队。
    • 将工程师及其专业技能(例如运维、QA和信息安全)嵌入每个服务团队,或者向团队提供自助服务平台,其功能包括配置类生产环境、执行自动化测试或进行部署。
    • 这使每个服务团队能够独立地向客户交付价值,而不必提交工单给IT运维、QA或信息安全等其他部门。
  • 使职能导向有效。
    • 快速响应。
    • 高度信任的文化。
  • 将测试、运维和信息安全融入日常工作。
    • 保证质量、可用性和安全性不是某个部门的职责,而是所有人日常工作的一部分。
  • 使团队成员成为通才。
    • 培养全栈工程师。
    • 给工程师提供学习必要技能的机会,让他们有能力构建和运行所负责的系统。
  • 松耦合架构,提高生产力和安全性。
  • 保持小规模(“两个披萨原则”)。

要使职能导向有效,需要由传统的集中式运维向提供运维服务的方向转变。

oaas.png

运维融入项目开发工作

  • 创建共享服务(类生产环境、部署流水线、自动化测试工具、生产环境监控台、运维服务平台等),提高开发生产力。
  • 运维工程师融入开发团队。
    • 使产品团队自给自足,可以完全负责服务的交付和支持。
    • 派遣工程师到项目开发团队(运维工程师的面试和聘用仍由集中式运维团队完成)。
  • 为每个项目团队分派运维联络人(派遣的运维工程师)。
    • 集中式运维团队管理所有环境,派遣的运维工程师需要理解:新产品的功能、开发原因、程序如何工作、可运维性、可扩展性、监控能力、架构模式、对基础设施的要求、产品特性的发布计划等。
  • 邀请运维联络人参加开发团队会议、每日站会、回顾会议。
  • 使用看板图展示运维工作。

流动的技术实践

该部分包含以下内容:

  • 运行部署流水线的基础。
  • 实现快速可靠的自动化测试。
  • 代码持续集成。
  • 自动化和低风险发布。
  • 降低发布风险的架构。

运行部署流水线的基础

  • 自动化环境(开发、测试、正式)搭建。
    • 使用 Shell、IaC(Puppet、Ansible、Terraform)、Docker、K8S、OpenShift 等技术。
  • 所有内容做版本控制。
    • 应用程序代码版本控制;
    • 数据库代码版本控制;
    • 运维配置代码版本控制;
    • 自动化和手动测试的脚本;
    • 支持代码打包、部署、数据库迁移、应用配置的脚本;
    • 项目相关文件(需求文档、部署过程、发布说明等);
    • 防火墙配置、服务器配置等脚本。
  • 扩展完成的定义。
    • 在类生产环境中按照预期进行,开发工作才认为是完成的。

实现快速可靠的自动化测试

  • 持续构建、测试和集成。
    • 代码分支持续集成到主干中,并确保通过单元测试、集成测试和验收测试。
    • 常用工具:Jenkins、TFS、TeamCity、GitLab CI。
    • 对持续集成的配合:自动化测试工具;一旦失败必须立即解决的文化;代码持续合入到主干,而不是持续在特性分支上工作。
  • 构建快速可靠的自动化测试套件。
    • 单元测试:JUnit、Mockito、PowerMock
    • 单元测试度量:测试覆盖率。
    • 验收测试:自动化API测试、自动化GUI测试。
    • 并行测试:安全测试、性能测试、单元测试、自动化测试。
    • 测试驱动开发:TDD、ATDD。
  • 让部署流水线始终保持绿色状态。
    • 部署流水线失败时,所有人立即解决问题或者立即回滚代码,后续的代码提交应该拒绝。

代码持续集成

  • 持续集成代码。
    • 开发人员在自己的分支上独立工作的时间越长,就越难将变更合入主干。
  • 小批量开发。
  • 基于主干开发。
    • 频繁向主干提交(通过合并请求)代码。

自动化和低风险发布

  • 自动化部署步骤:构建、测试、部署;相关流程包括:
    • 代码打包、构建;
    • 上传 Docker 镜像;
    • 创建预配置的 K8S 服务;
    • 自动化单元测试、冒烟测试;
    • 数据库迁移自动化;
    • 配置自动化。
  • 应用自动化的自助式部署
    • 开发人员专注于编写代码,点击部署按钮,通过监控指标看到代码在生产环境中正常运行,在代码出错时能获得错误信息快速修复。
    • 通过代码审查、自动化测试、自动化部署,控制部署风险,必要时使开发人员也可进行部署操作,测试人员和项目经理可在某些环境中进行部署。
  • 将部署和发布解耦
    • 部署指在特定环境中安装制定版本的软件。
    • 发布指将产品特性提供给所有客户或部分客户使用。
  • 基于环境的发布模式
    • 蓝绿部署
    • 灰度(金丝雀)发布
  • 基于应用的发布模式
    • 实现特性开关,好处:轻松地回滚、缓解性能压力、可以屏蔽服务依赖。
    • 实现黑启动:发布潜在风险的新特性时,隐式调用,仅记录测试结果。
  • 持续交付的实践
    • 持续交付是指,所有开发人员都在主干上进行小批量工作,或者在短时间存在的特性分支上工作,并且定期向主干合并,同时始终让主干保持可发布状态,并能做到在正常的工作时段里按需进行一键式发布。开发人员在引入任何回归错误时(包括缺陷、性能问题、安全问题、可用性问题等),都能快速得到反馈。一旦发现这类问题,就立即加以解决,从而保持主干始终处于可部署状态。
  • 持续部署的实践
    • 持续部署是指,在持续交付的基础上,由开发人员或运维人员自助式地定期向生产环境部署优质的构建版本,这通常意味着每天每人至少做一次生产环境部署,甚至每当开发人员提交代码变更时,就触发一次自动化部署。
  • 大多数团队采用持续交付实践。

降低发布风险的架构

  • 松耦合架构
  • 面向服务的架构
  • 安全地演进企业架构
    • 绞杀者应用模式:API封装已有功能、按新架构实现新功能、API版本化。
  • 云原生架构

反馈的技术实践

这部分包含以下内容:

  • 建立遥测系统
  • 智能告警
  • 应用反馈实现安全部署
  • 应用A/B测试
  • 建立评审和协作流程

建立遥测系统

  • 什么是遥测(Telemetry)?
    • 遥测包含监控,实现对网络实时、高速和更精细的监控技术。
    • 相比于传统的网络监控技术,遥测通过推模式,主动向采集器上推送数据信息,提供更实时更高速更精确的网络监控功能。
  • 遥测的三大维度
    • Tracing(跟踪),Metrics(指标) , Logging(日志)。
  • 可观察性
    • 系统可以由其外部输出(遥测的数据)推断其内部状态的程度。
    • 能发现、预测并解决问题。
  • 集中式监控系统(可使用:Prometheus、SkyWalking)
    • 在业务逻辑、应用程序和环境层收集数据。
    • 负责存储和转发事件和指标的事件路由器。
  • 应用程序日志遥测(ELK、审计日志、Metrics)
  • 重大应用事件清单:
    • 认证/授权的结果(包括退出);
    • 系统和数据的访问;
    • 系统和应用程序的变更(特别是特权变更);
    • 数据的变更,例如增加、修改或删除数据;
    • 无效输入(可能的恶意注入、威胁等);
    • 资源(内存、磁盘、中央处理器、带宽或其他任何具有硬/软限制的资源);
    • 健康度和可用性;
    • 启动和关闭;
    • 故障和错误;
    • 断路器跳闸;
    • 延迟;
    • 备份成功/失败。
  • 将建立生产遥测融入日常开发工作。
  • 使用遥测指导问题的解决。
  • 建立自助访问的可视化遥测信息系统(信息辐射器)
    • Grafana
    • SkyWalking
    • Kibana
  • 发现和填补遥测的盲区(建立充分而完整的遥测)
    • 业务级别:订单量、用户数、流失率、广告展示和点击等。
    • 应用程序级别:事务处理事件、应用程序故障等。
    • 基础架构级别:服务器吞吐量、CPU负载、磁盘使用率等。
    • 客户端软件级别:应用出错和崩溃、客户端的事务处理事件等。
    • 部署流水线级别:流水线状态、部署频率等。

智能告警

  • 解决告警疲劳
    • 充分而完整的遥测会引入告警疲劳问题,需要更智能的报警。
  • 使用统计分析方法,而非静态阈值设置告警
    • 使用均值和标准差(适用于正态分布的数据):度量数据与均值存在较大标准差时告警。
  • 使用预防故障的告警,而不只是故障发生后的告警
    • 试着问有什么指标可以预测故障。
  • 异常检测技术
    • 平滑统计技术:使用移动平均数,利用每个点与滑动窗口中所有其他数据的平均值,来转换数据。
    • 支持高级异常检测的工具:Prometheus、Grafana。

应用反馈实现安全部署

  • 通过遥测使部署更安全——部署后能立即发现问题。
  • 价值流中的所有人(开发人员、开发经理、架构师、运维团队等)共同承担运维事故的下游责任。
    • 共同承担值班工作、共同解决生产环境问题。
  • 让开发人员跟踪工作对运维人员的影响。
    • 使开发的应用易于部署,提升运维人员幸福感。
  • 让开发团队自行管理生产服务。
    • 首先由开发团队管理,然后才交由集中的运维团队管理。
    • 运维工程师由生产支持转变为顾问或加入团队,帮助做好部署准备,建立服务发布指南(包括:支持有效的监控、部署可靠、架构能支持快速频繁的部署等)。
    • 为团队分配SRE人员。SRE定位:SRE就是软件开发工程师负责了运维工作,SRE非常稀少,只能分配给最重要的团队。

应用A/B测试

  • 在功能中集成A/B测试
    • 向用户随机展示一个页面的两个版本之一。
  • 在发布中集成A/B测试
    • 使用特性开关。
  • 在功能规划中集成A/B测试
    • 不仅要快速部署和发布软件,还要在实验方面不断提升,通过实验主动实现业务目标和客户满意度。

建立评审和协作流程

  • 防止「过度控制变更」
    • 反事实思维容易认为事故是由于缺乏审批流程导致。
  • 建立同行评审,缩短审批流程
    • DevOps 中高绩效的组织更多地依赖同行评审,更少地依赖外部变更批准(层层审批)。
  • 代码评审
    • 每个人的代码提交到主干时,必须由同行进行评审;
    • 每个人应该持续关注其他成员的提交活动;
    • 定义高风险变更,从而决定是否需要请领域专家进行审查;
    • 将大的提交变更拆分成小批量变更。
  • 利用结对编程改进代码变更
    • 研究表明:结对的程序员比两个独立工作的程序员慢了15%,而‘无错误’代码量却从70%增加到了85%。
    • 测试和调试程序的成本通常比写初始代码的成本高出多倍。
  • 评估合并请求的有效性
    • 与在生产环境产生的结果无关。
    • 有效合并请求的基本要素:必须足够详细地说明变更的原因、如何做的变更,以及任何已识别的风险和应对措施。

持续学习与实验的技术实践

这部分包含以下内容:

  • 将学习融入日常工作
  • 将局部经验转化为全局改进
  • 预留组织学习和改进的时间

将学习融入日常工作

  • 公正文化和学习文化
    • 人为错误往往不是问题的根本原因,可能是复杂系统中存在不可避免的设计问题而导致。
    • 不应该对造成故障的人进行「点名、责备和羞辱」,我们的目标是最大限度地抓住组织学习的机会。
    • 从学习的角度看待错误、报错、失误、过失等。
    • 相关实践1:在事后分析中,不指责,公正地进行评判,使工程师自己愿意对事情负责,并且热情地帮助其他人避免同样的错误发生;广泛地公开事后分析会议结果。
    • 相关实践2:在生产环境中引入受控的人为故障(捣乱猴),针对不可避免的问题进行演练。
  • 降低事故容忍度,寻找更弱的故障信号
    • 随着组织能力的提升,事故数量大幅降低,故障越不应该出现。
    • 在复杂的系统中,放大微弱的故障信号对于防范灾难性故障事关重要。
  • 重新定义失败
    • 高效能DevOps组织的变更频率是平均水平的30倍,即使失败率只有平均水平的一半,也显然意味着故障总数更多。
    • 鼓励创新并接受因此带来的风险。
  • 创建故障演练日
    • 帮助团队模拟和演练事故,使其具备实战能力。
    • 暴露系统的潜在缺陷。

将局部经验转化为全局改进

  • [ChatOps] 使用聊天机器人、积累组织知识
    • 自动化工具集成到聊天中,比如(@bot depoy owl to production);
    • 操作结果由机器人发送回聊天室,每个人都能看到发生的一切;
    • 新来的工程师也可以看到团队的日常工作及执行方式;
    • 看到他人互相帮助时,人们也会倾向于寻求帮助;
    • 使用话题组,建立起组织学习,知识得到快速积累。
    • 加强了透明、协作的文化。
  • 将标准、流程和规范转化为便于执行的形式
    • [ArchOps] 使工程师成为构建者,而不是砌砖工;
    • 将手动操作流程转换为可自动化执行的代码;
    • 将合规性使用代码表达出来。
  • 运用自动化测试记录和传播知识
    • 自动化界面测试,令使用者知道系统如何使用;
    • 单元测试,令调用者知道方法API如何使用。
  • 项目开发中包含非功能性的运维需求
    • 对各种应用和环境进行充分的遥测;
    • 准确跟踪依赖关系的能力;
    • 具有弹性并能正常降级的服务;
    • 各版本之间具有向前和向后的兼容性;
    • 归档数据来管理生产数据集的能力;
    • 轻松搜索和理解各种服务日志信息的能力;
    • 通过多个服务跟踪用户请求的能力;
    • 使用功能开关或其他方法实现简便、集中式的运行时配置。
  • 把可重用的运维用户故事纳入开发
    • 将重复的运维工作通过编码进行实现。
  • 技术选型需要考虑运维因素
    • 不能减慢工作流;
    • 思考举例:TIDB VS MySQL 该如何选择。

预留组织学习和改进的时间

  • 偿还技术债务制度化
    • 定时「大扫除」
    • 开发和运维针对非功能性需求进行优化,横跨整个价值流。
    • 价值:赋予一线工作人员不断识别和解决问题的能力。
  • 让所有人教学相长
    • 所有的工程师都越来越需要某些技能,而不只是开发人员如此。
    • 越来越多的技术价值流采用了DevOps的原则和模式。
    • [每周学习文化] 每周一次的学习时间,每个同伴既要自己学习,又要教别人。
  • 内部顾问和教练
    • 成立内部的教练和咨询组织,促进专业知识在组织内的传播。

实践重点

DevOps 的实践包含许多内容,提炼了以下重点方便查阅:

  • 流动原则的实践
    • 部署流水线的基础(所有内容做版本控制、在类生产环境按预期工作才算完成)
    • 实现快速可靠的自动化测试(自动化运行、始终保持流水线处于绿色状态)
    • 代码持续集成(小批量开发)
    • 自动化和低风险发布(自助式部署、部署和发布解耦、采用持续交付)
    • 降低发布风险的架构(云原生架构)
  • 反馈原则的实践
    • 建立遥测系统(Tracing、Metrics、Logging)
    • 智能告警(使用统计分析方法和预防故障的告警)
    • 应用反馈实现安全部署(部署后立即发现问题、共同承担责任)
    • 应用A/B测试(功能规划中集成A/B测试、使用特性开关)
    • 建立评审和协作流程(同行评审、减少审批流程、结对编程)
  • 持续学习与实验原则的实践
    • 将学习融入日常工作(从学习的角度看待事故、寻找更弱的故障信号)
    • 将局部经验转化为全局改进(ChatOps、让规范便于执行、非功能性的运维需求)
    • 预留组织学习和改进的时间(定时偿还技术债务、教学相长、内部教练)

结语

DevOps 的发展与技术的发展相辅相成,也为技术人员提供了更多的学习道路和发展方向,借用一句 DevOps 领袖的话来作为本文的结束语。

对于所有热爱创新、热爱变革的专业技术人士来说,我们的前方是美好而充满活力的未来。


本文整理自笔者分享的 ppt,原文及 ppt 地址:github.com/lcomplete/T…

本文转载自: 掘金

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

MySQL的外键约束(FOREIGN KEY),有点小复杂

发表于 2021-05-24

一、什么是参照完整性

在学习外键之前,我们必须先搞懂一个概念,什么是“参照完整性”。

参照的关系中的属性值必须能够在被参照关系找到或者取空值,否则不符合数据库的语义。

在实际操作时如更新、删除、插入一个表中的数据,通过参照引用相互关联的另一个表中的数据,来检查对表的数据操作是否正确,不正确则拒绝操作。

举个例子,有一张员工表,字段为工号、姓名、所属部门。 还有一张部门表,字段为部门编号,部门名称,部门位置。

员工表demo_employee如下

字段名称 字段类型 字段含义 主键/外键
emp_no varchar(20) 工号 主键
name varchar(20) 姓名
dept_id bigint(20) 所属部门ID 外键

部门demo_dept表如下

字段名称 字段类型 字段含义 主键/外键
dept_id varchar(20) 部门ID 主键
name varchar(20) 部门名称
location varchar(100) 地址

每一个员工都应该有一个从属部门,并且这个部门一定在部门表中有数据存在。或者此员工没有任何部门为NULL,例如员工新入职还未分配所属部门的情况。

如果某个员工尹洪亮,数据记录为从属于极光轻学的建筑业务部,但是公司根本就没有这个部门,这就代表违反了参照完整性,相反就是遵从了参照完整性。

二、MySQL的外键

  1. MySQL 外键约束(FOREIGN KEY)用来在两个表的数据之间建立链接,一个表可以有一个或多个外键。

例如员工表有姓名、年龄、性别、所属部门、籍贯几个列,那么可以只有所属部门这一个列是外键,也可以所属部门和籍贯两个列都是外键。

3.png

  1. 一个表的外键可以为空值,若不为空值,则每一个外键的值必须等于另一个表中主键的某个值。

例如员工表和部门表,员工表的外键,必须依赖于部门表的主键,可以表述为如下关系。

demo_employee(dept_id)外键->依赖->demo_dept(dept_id)主键
3. 外键是表的一个字段,不是本表的主键,但对应另一个表的主键。定义外键后,不允许删除另一个表中具有关联关系的行。

例如某个部门有10名员工,如果要没有把员工调整到其他部门或者遣散就直接撤销部门是不允许的。
4. 外键的主要作用是保持数据的一致性和完整性。、

三、主表与从表

  1. 主表也叫做父表:对于两个具有关联关系的表而言,相关联字段中主键所在的表就是主表。
  2. 从表也叫做子表:对于两个具有关联关系的表而言,相关联字段中外键所在的表就是从表。

例如,员工表和部门表的关系, 员工表依赖于部门表,所以部门表就是主表,员工表是从表,可以理解为1个部门下有多个员工。

四、如何选取外键约束字段

定义一个外键时,需要遵守下列规则:

  1. 父表必须已经存在于数据库中,或者是当前正在创建的表。如果是后一种情况,则父表与子表是同一个表,这样的表称为自参照表,这种结构称为自参照完整性。

如下一张员工表,有一个所属主管列,而主管也是一名员工,所以自己要参照自己的表,这就是自参照完整性,也叫表内外键。

5.png

  1. 必须为父表定义主键。
  2. 主键不能包含空值,但允许在外键中出现空值。也就是说,只要外键的每个非空值出现在指定的主键中,这个外键的内容就是正确的。
  3. 在父表的表名后面指定列名或列名的组合。这个列或列的组合必须是父表的主键或候选键。
  4. 外键中列的数目必须和父表的主键中列的数目相同。
  5. 外键中列的数据类型必须和父表主键中对应列的数据类型相同。

五、如何设置外键

5.1 在创建表时设置外键约束

在数据表中创建外键使用 FOREIGN KEY 关键字,具体的语法如下。

1
2
mysql复制代码[CONSTRAINT <外键名>] FOREIGN KEY 字段名 [,字段名2,…]
REFERENCES <主表名> 主键列1 [,主键列2,…]

其中:

<外键名>为定义的外键约束的名称,一个表中不能有相同名称的外键;

字段名表示子表需要添加外健约束的字段列;

主表名即被子表外键所依赖的表的名称;

主键列表示主表中定义的主键列或者列组合。

示例 1,为了展现表与表之间的外键关系,本例在 demo_db 数据库中创建一个部门表 demo_dept,表结构如下表所示。

1
2
3
4
5
6
7
mysql复制代码mysql> CREATE TABLE demo_dept
-> (
-> id INT(11) PRIMARY KEY,
-> name VARCHAR(22) NOT NULL,
-> location VARCHAR(50)
-> );
Query OK, 0 rows affected (0.37 sec)

创建数据表 demo_employee,并在表 demo_employee 上创建外键约束,让它的键 dept_id作为外键关联到表 demo_dept 的主键 id上,编写 SQL 语句和执行结果如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql复制代码mysql> CREATE TABLE demo_employee
-> (
-> id INT(11) PRIMARY KEY,
-> name VARCHAR(25),
-> dept_id INT(11),
-> salary FLOAT,
-> CONSTRAINT fk_emp_dept
-> FOREIGN KEY(dept_id) REFERENCES demo_dept(id)
-> );
Query OK, 0 rows affected (0.37 sec)
mysql> DESC demo_employee;
+--------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| name | varchar(25) | YES | | NULL | |
| dept_id| int(11) | YES | MUL | NULL | |
| salary | float | YES | | NULL | |
+--------+-------------+------+-----+---------+-------+
4 rows in set (1.33 sec)

以上语句执行成功之后,在表 demo_employee 上添加了名称为 fk_emp_dept 的外键约束,外键名称为 dept_id,其依赖于主表 demo_dept 的主键 id。

墙裂注意:子表的外键必须关联父表的主键,且关联字段的数据类型必须匹配,如果类型不一样,则创建子表时会出现错误“ERROR 1005(HY000):Can’t create table’database.tablename’(errno:150)”。

5.2 在修改表时添加外键约束

在修改数据表时添加外键约束的语法规则为:

1
mysql复制代码ALTER TABLE <数据表名> ADD CONSTRAINT <索引名> FOREIGN KEY (<列名>)  REFERENCES <主表名>(<列名>);

【示例2】修改数据表tb_employee,将字段dept_id设置为外键,与数据表demo_dept的主键id进行关联,输入的SQL语句和运行结果如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql复制代码mysql> ALTER TABLE tb_employee
-> ADD CONSTRAINT fk_demo_dept
-> FOREIGN KEY(dept_id)
-> REFERENCES demo_dept(id);
Query OK, 0 rows affected (1.38 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> SHOW CREATE TABLE tb_employee\G
*************************** 1. row ***************************
Table: tb_employee
Create Table: CREATE TABLE `tb_employee` (
`id` int(11) NOT NULL,
`name` varchar(30) DEFAULT NULL,
`dept_id` int(11) DEFAULT NULL,
`salary` float DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fk_demo_dept` (`dept_id`),
CONSTRAINT `fk_demo_dept` FOREIGN KEY (`dept_id`) REFERENCES `demo_dept` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312
1 row in set (0.12 sec)

5.3 删除外键约束

对于数据库中定义的外键,如果不再需要,可以将其删除。外键一旦删除,就会解除主表和从表间的关联关系,MySQL 中删除外键的语法格式如下:

ALTER TABLE <表名> DROP FOREIGN KEY <外键约束名>;

示例 3,删除数据表 tb_employee 中的外键约束 fk_demo_dept,编写 SQL 语句和执行结果如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql复制代码mysql> ALTER TABLE tb_employee
-> DROP FOREIGN KEY fk_demo_dept;
Query OK, 0 rows affected (0.19 sec)
Records: 0 Duplicates: 0 Warnings: 0

mysql> SHOW CREATE TABLE tb_employee\G
*************************** 1. row ***************************
Table: tb_employee
Create Table: CREATE TABLE `tb_employee` (
`id` int(11) NOT NULL,
`name` varchar(30) DEFAULT NULL,
`dept_id` int(11) DEFAULT NULL,
`salary` float DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `fk_demo_dept` (`dept_id`)
) ENGINE=InnoDB DEFAULT CHARSET=gb2312
1 row in set (0.00 sec)

可以看到,tb_employee 中已经不存在 FOREIGN KEY,原有的名称为 fk_emp_dept 的外键约束删除成功。

MySQL是日常工作中使用最对的数据库之一,所以必须要要较为深入和全面的掌握,对于高阶人员还要掌握分布式事务、各种数据库锁、传播机制等。

本文转载自: 掘金

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

盘点 SpringMVC MVC 主流程

发表于 2021-05-24

首先分享之前的所有文章 , 欢迎点赞收藏转发三连下次一定 >>>> 😜😜😜

文章合集 : 🎁 juejin.cn/post/694164…

Github : 👉 github.com/black-ant

CASE 备份 : 👉 gitee.com/antblack/ca…

一 . 前言

文章目标 :

  • 梳理 SpringMVC 主流程中的相关逻辑
  • 确定 相关流程得参数

本文章未涉及的部分:

  • MVC 的配置
  • 容器的初始化
  • 容器的加载
  • 等….

二 . 流程分析

此主要流程包括如下几个部分 :

  • 注解的扫描处理
  • 请求的拦截
  • 请求的转换
  • 请求的最终处理

image.png

2.1 注解的扫描处理

注解的扫描主要在 DispatcherServlet.initHandlerMappings 中 , 其逻辑处理类在 AbstractHandlerMethodMapping.getHandlerInternal 中.

2.1.1 注解的扫描

注解扫描的起点主要是 RequestMappingHandlerMapping , 其中做了这几件事 :

  • 在 AbstractHandlerMethodMapping 中通过 InitializingBean 的 afterPropertiesSet 处理所有的方法
    • detectHandlerMethods 中进行核心的处理
  • 对 RequestMapping 进行了扫描操作 , 并且生成 RequestMappingInfo
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
java复制代码
C31- RequestMappingHandlerMapping : 扫描的起点 , 因为类实现了 InitializingBean 接口 , 其核心基于 afterPropertiesSet
M- afterPropertiesSet
?- 这个方法中主要对 RequestMappingInfo.BuilderConfiguration 进行配置 ,最后调用父类 afterPropertiesSet 方法

C32- AbstractHandlerMethodMapping
M32_01- afterPropertiesSet
- initHandlerMethods() : 核心方法 , 对 Handler Method 进行处理 -> M32_02
M32_02- initHandlerMethods
?- 扫描ApplicationContext中的bean,检测和注册处理程序方法
- 获取所有的 BeanName , 调用 processCandidateBean 执行 -> M32_03
M32_03- processCandidateBean
- obtainApplicationContext().getType 获取具体的 Class 类
- 通过 isHandler 判断是否包含 Controller 或者 RequestMapping
?- RestController 继承了 Controller
- 如果需要处理 , 调用 detectHandlerMethods 处理 -> M32_04
M32_04- detectHandlerMethods
- 同样的 , 通过传入的 handle (类的别名) 获取对应的 Class<?>
- 获取所有的标注了注解的集合 Map<Method, T> -> ps:M32_04_01
- getMappingForMethod 获取注解对应的 Condition -> ps:M32_04_01 详细流程
- selectMethods
FOR- 循环所有的 methods , 首先搜索可用的代理 , 注册对应的 Bean
?- 代理是 Spring 中很重要的一环 , 后面开单章详细说
M32_05- registerHandlerMethod
- 注册到 MappingRegistry


// ps:M32_04_01 流程详细分析
Map<Method, T> methods = MethodIntrospector.selectMethods(userType,(MethodIntrospector.MetadataLookup<T>) method -> {
return getMappingForMethod(method, userType);
});

// 可以看到 , 其中核心的方法有2个 getMappingForMethod / selectMethods
getMappingForMethod 会返回Mapping 对应的 Condition , 例如 patternsCondition / methodCondition


// ps:M32_04_01
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {
// 此处对 RequestMapping 进行了扫描操作
// 代码比较简单 , 就是 AnnotatedElementUtils.findMergedAnnotation 后构建了一个 RequestMappingInfo bean
RequestMappingInfo info = createRequestMappingInfo(method);
if (info != null) {
// 此处是获取 Class 类上面的 RequestMapping
RequestMappingInfo typeInfo = createRequestMappingInfo(handlerType);
if (typeInfo != null) {
// 对2者进行组合 , 生成新的 Bean 对象
// 注意是类 组合 方法
// 主要是覆盖和合并 , url 比较特殊 , 变化为 /get -> /cookie/get
info = typeInfo.combine(info);
}

// 前缀 path 的处理 , 该属性在配置 PathMatchConfigurer 时处理 , 可以通过 addPathPrefix 添加
String prefix = getPathPrefix(handlerType);
if (prefix != null) {
info = RequestMappingInfo.paths(prefix).options(this.config).build().combine(info);
}
}
return info;
}

2.1.2 Bean 的注册

上面的环节已经对 Mapping Method 扫描完成 , 在此环节中进行注册 , 注册主要使用内部类MappingRegistry
, 其内部存在多个 Map ,用于保存地址的映射关系

(url -MappingRegistration/HandlerMethod/CorsConfiguration 等 )

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
java复制代码            
C- AbstractHandlerMethodMapping
VC33- MappingRegistry : 内部类
F33_01- Map<T, MappingRegistration<T>> registry = new HashMap<>();
F33_02- Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
F33_03- MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
F33_04- Map<String, List<HandlerMethod>> nameLookup = new ConcurrentHashMap<>();
F33_05- Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
M33_01- register(T mapping, Object handler, Method method) -> PS:M33_01
?- 此处主要是为了构建 Map 集合 , 用于映射时匹配
1- 通过 handler 和 Method 构建了使用时的主要单元Bean HandlerMethod
2- 以 RequestMappingInfo(mapping) 为key 将 HandlerMethod 放入 Map (F33_02) 集合
3- 以 URL 字符串为 key , 将 以 RequestMappingInfo(mapping) 放入 Map 集合 (F33_03)
4- 以 Name (策略类构建特殊的 mappingName)为 key , 将 handlerMethod 放入 Map (F33_04) -> PS:M33_02
5- 以 HandlerMethod 为 key , 将 CorsConfiguration 放入集合 Map (F33_05)
6- 以 RequestMappingInfo(mapping) 为key , 生成一个 MappingRegistration 放入集合 registry (F33_01)


// PS:M33_01 register 方法的关键和亮点
1- 使用 ReentrantReadWriteLock 保证多线程下访问的唯一性 , 在 finally 中解锁

// PS:M33_02 特殊的命名方式 RequestMappingInfoHandlerMethodMappingNamingStrategy
这里主要通过 RequestMappingInfoHandlerMethodMappingNamingStrategy 类生成对应的 Name


// M33_01 伪代码
public void register(T mapping, Object handler, Method method) {
this.readWriteLock.writeLock().lock();
try {
// 1- 通过 handler 和 Method 构建了使用时的主要单元Bean HandlerMethod
HandlerMethod handlerMethod = createHandlerMethod(handler, method);
validateMethodMapping(handlerMethod, mapping);

// 2- 以 RequestMappingInfo(mapping) 为key 将 HandlerMethod 放入 Map (F33_02) 集合
this.mappingLookup.put(mapping, handlerMethod);

List<String> directUrls = getDirectUrls(mapping);
for (String url : directUrls) {
// 3- 以 URL 字符串为 key , 将 以 RequestMappingInfo(mapping) 放入 Map 集合 (F33_03)
this.urlLookup.add(url, mapping);
}

String name = null;
if (getNamingStrategy() != null) {
name = getNamingStrategy().getName(handlerMethod, mapping);
// 4- 以 Name (策略类构建特殊的 mappingName)为 key , 将 handlerMethod 放入 Map (F33_04)
addMappingName(name, handlerMethod);
}

// 构建跨域配置
CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
if (corsConfig != null) {
5- 以 HandlerMethod 为 key , 将 CorsConfiguration 放入集合 Map (F33_05)
this.corsLookup.put(handlerMethod, corsConfig);
}
// 6- 以 RequestMappingInfo(mapping) 为key , 生成一个 MappingRegistration 放入集合 registry
this.registry.put(mapping, new MappingRegistration<>(mapping, handlerMethod, directUrls, name));
}finally {
this.readWriteLock.writeLock().unlock();
}
}


// 自此 , Mapper 的前期准备就完成了

MvcUrl001.jpg

HandlerMappingCondition.jpg

2.2 请求的来临

请求的入口还是通过 Servlet 来完成的 , 先看一下 Servlet 体系 PS:51_01

2.2.1 起点 : 初始化一个 Servlet

MVC 是基于 Servlet 的体系结构 , 其最初的起点是通过 StandardWrapper 进行 initServlet 开始
流转路径如下 :

1
java复制代码StandardWrapper -> GenericServlet (init) -> HttpServletBean (init) -> FrameworkServlet (initServletBean)

看一个很老的图 , 大概对 DispatcherServlet 有个初步的了解 :

mvc-context-hierarchy.png

2.2.2 核心 : DispatchServlet 的初始化

1
2
3
4
5
java复制代码C51- DispatcherServlet 
M51_10- initHandlerMappings(ApplicationContext context)
?- 这个方法的核心操作就是初始化 handlerMappings -> PS:M51_10_01
- 通过 BeanFactoryUtils 构建所有的 Mapping 类型结合
- 为 Map 进行排序

Filter 处理逻辑 , Filter 分为 3步 : 初始化 , 拦截 , 销毁 , 此处主要是初始化操作 >>

Filter 不是 Spring 专属的对象 , 其归属于 Servlet 原生体系 , 主要有以下2个流程


子流程一 : 核心处理容器 : StandardContext,添加 Filter 流程

  • ServletWebServerApplicationContext # selfInitialize : 获取所有的需要处理的 Filter 类
  • ServletContextInitializerBeans # ServletContextInitializerBeans : 从 Factory 中获取具体的类
  • RegistrationBean # onStartup : 启动 并且注册 FilterBean
  • StandardContext # addFilterDef : 添加 FilterDef , 即 filter definition
1
2
3
4
5
6
7
8
9
10
java复制代码                  
C- ServletWebServerApplicationContext
M- selfInitialize
- getServletContextInitializerBeans 其中会获取所有的 Beans , 分别调用 beans.onStartup 启动
M- getServletContextInitializerBeans
- 核心处理逻辑在 ServletContextInitializerBeans 中

C- ServletContextInitializerBeans
M- ServletContextInitializerBeans(ListableBeanFactory beanFactory,Class<? extends ServletContextInitializer>... initializerTypes)
?- 此方法中会获取 Servlet 和 Filter 的初始化类 --> TODO : 后续单独分析 Serlvet 时看

子流程二 : 调用 Filter 初始化流程

这是一个异步操作 : 通过一个 call 流程异步处理 , 具体的流程在 org.apache.catalina.core 中

  • StandardContext # filterStart : Set filterDefs.entrySet 中获取所有的 Filter
  • ApplicationFilterConfig : 构建一个 ApplicationFilterConfig 并且在其中调用 initFilter() 初始化
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
java复制代码            
// Filter 的拦截调用
Filter 的核心思路就是 FilterChain 的链表调用 . 其起点为 ApplicationFilterChain
C- ApplicationFilterChain
F- private ApplicationFilterConfig[] filters = new ApplicationFilterConfig[0];
- 选择一个 ApplicationFilterConfig , 调用 doFilter , 并且把当前对象传入
?- 后续就是调用链的逻辑




// HandlerInterceptor 的处理流程
- 与 Filter 不同的是 , 这里的 HandlerInterceptor 是 Spring 体系中的对象
- 拦截器主要在 WebMvcConfigurer 通过 InterceptorRegistry 手动注入

@Override
public void addInterceptors(InterceptorRegistry registry) {
logger.info("------> this is addInterceptors <-------");
registry.addInterceptor(new ConfigInterceptor());
}

// Interceptor 的初始化
Interceptor 的主要起点是 WebMvcConfigurationSupport
C- WebMvcConfigurationSupport
M- requestMappingHandlerMapping
- 其中会为当前 Mapping 添加 Interceptor


// Interceptor 的调用
Interceptor 的主调流程位于 HandlerExecutionChain
C- HandlerExecutionChain
M- applyPreHandle
- getInterceptors() 获取 HandlerInterceptor->LV001 数组
FOR- 循环 HandlerInterceptor:LV001 调用 preHandle


// DispatcherServlet 衔接到 RequestMapping 映射关联
- ApplicationFilterChain 处理完成 Filter
// 这里会进行一次继承类的循环调用
C- DispatcherServlet
M- FrameworkServlet
M- HttpServlet

- HttpServlet 执行 service(ServletRequest req, ServletResponse res) 方法
- FrameworkServlet 执行 service(HttpServletRequest request, HttpServletResponse response) 方法
- HttpServlet 执行 service(HttpServletRequest request, HttpServletResponse response) 方法
- FrameworkServlet 执行 doGet 处理请求
- FrameworkServlet 执行 processRequest 开始完整处理
- 来到具体实现类 DispatcherServlet

PS:51_01 Servlet 家族体系
ServletModule.png

PS:M51_10_01 HandlerMapping 家族体系
HandlerMappingModule.png

2.3 请求的解析

servlet 的起始方法是 HttpServlet , Spring 部分的核心方法是实现类FrameworkServlet

解析前置处理操作

  • 在 Reuqest 和 Response 转换为 HttpServletRequest 和 HttpServletResponse 后
  • 调用 HttpServlet 中 service(HttpServletRequest req, HttpServletResponse resp) 方法 , 触发 FrameworkServlet doGet 方法
  • 最终进入 DispatcherServlet 核心类 , 处理一个 Http 请求

2.3.1 DispatcherServlet 获取处理 Handler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码Start -> HttpServlet
// TODO : Dispatch 具体流程以后单独分析 , 这里先简单过一下
C51- DispatcherServlet
M51_01- doService(HttpServletRequest request, HttpServletResponse response)
?- 执行 Service 处理请求
- 调用 M51_02 执行具体的流程
M51_02- doDispatch(HttpServletRequest request, HttpServletResponse response)
1- WebAsyncUtils.getAsyncManager(request) : 准备异步处理管理器 WebAsyncManager -> PS:M51_02_01
2- checkMultipart(request) : 尝试解析 Multipart Request -> PS:M51_02_02
3- getHandler(processedRequest) : 获取具体需要执行的 Handler -> M51_03
?- 未找到对应的 Handler 会直接 return
4- getHandlerAdapter(mappedHandler.getHandler()) : 确定当前请求的处理程序适配器 -> PS:M51_02_04
5- 处理 Get-Head 请求 -> PS:M51_02_05
- mappedHandler.applyPreHandle : 前置处理
6- 调用 HandlerAdapter 处理 请求
7- applyDefaultViewName : view 名称的解析 , 此环节不分析
- mappedHandler.applyPostHandle : 后置处理 -> PS:M51_02_09
M51_03- getHandler
M51_04- getHandlerAdapter

// M51_03 源代码
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
for (HandlerMapping mapping : this.handlerMappings) {
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}


// M51_04 getHandlerAdapter 源代码
protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter adapter : this.handlerAdapters) {
// 太常见的处理方式 , 循环处理中通过 support 判断
if (adapter.supports(handler)) {
return adapter;
}
}
}
throw new ServletException("....");
}

PS:M51_02_01 WebAsyncManager 介绍

  • 作用 : 用于管理异步请求处理的中心类,主要用作SPI,通常不被应用程序类直接使用
  • 核心 : 通过多个不同的线程 , 处理请求和结果
  • 流程
    1. 异步场景以线程(T1)中正常的请求处理开始
    2. 并发请求处理可以通过调用startCallableProcessing或startDeferredResultProcessing来启动,这两种方法都会在单独的线程(T2)中产生一个结果。
    3. 保存结果并将请求分发给容器,以便在第三个线程(T3)中继续处理保存的结果。
    4. 在已分派线程(T3)中,可以通过getConcurrentResult()访问保存的结果,或者通过hasConcurrentResult()检测其是否存在。

PS:M51_02_02 : checkMultipart 的目的

为什么这里需要校验一下 checkMultipart ? 其底层主要做了如下几件事 :

  • 判断是否有 multipartResolver 并且用该解析器判断是否为Multipart
  • 如果以上条件达成 , multipartResolver.resolveMultipart(request) 进行解析返回新 Request
  • 否则返回原有 request

说白了 , 就是 multipart request 的处理 , 但是相对的问题又来了 >>> 什么是 multipart request ?

作用 : HTTP 多部分请求是 HTTP 客户机构造的 HTTP 请求,用于将文件和数据发送到 HTTP 服务器。

场景 : 浏览器和 HTTP 客户端通常使用它将文件上传到服务器。


PS:M51_02_04 getHandler 和 getHandlerAdapter 的区别 ?

1
2
3
4
5
6
7
8
9
10
java复制代码getHandler -> HandlerExecutionChain
getHandlerAdapter -> HandlerAdapter

// HandlerExecutionChain 的作用 :
- HandlerExecutionChain 是 Handler 的处理器链 , 其中包含了 HandlerInterceptor 的数组和对应的 Handler

// HandlerAdapter 的作用 :
- 首先 , 看名字就知道 , 这是个适配器 , 适配器能在原有功能上扩展新的功能
- MVC框架SPI,允许参数化核心MVC工作流 , 该接口用于允许DispatcherServlet无限扩展。
- DispatcherServlet通过这个接口访问所有已安装的处理程序

HandlerAdapter_system.png

PS:M51_02_05 GET/HEAD 请求细说

这地方老有意思了 ,刚接触代码那会这里出问题想了好久才知道有这么个东西

参考文档 @ www.cnblogs.com/longyongzhe…

特点 : HEAD方法跟GET方法相同,只不过服务器响应时不会返回消息体。一个HEAD请求的响应中,HTTP头中包含的元信息应该和一个GET请求的响应消息相同。这种方法可以用来获取请求中隐含的元信息,而不用传输实体本身。也经常用来测试超链接的有效性、可用性和最近的修改。

作用 :

  1. 只请求资源的首部;
  2. 检查超链接的有效性;
  3. 检查网页是否被修改;
  4. 多用于自动搜索机器人获取网页的标志信息,获取rss种子信息,或者传递安全认证信息等

PS:M51_02_09 前置处理和后置处理主要处理什么 ?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码- mappedHandler.applyPreHandle : 前置处理
- mappedHandler.applyPostHandle : 后置处理

// 这2个方法主要是对拦截器的处理 , 可以看到其中直接调用拦截器的方法
boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerInterceptor[] interceptors = getInterceptors();
if (!ObjectUtils.isEmpty(interceptors)) {
for (int i = 0; i < interceptors.length; i++) {
HandlerInterceptor interceptor = interceptors[i];
if (!interceptor.preHandle(request, response, this.handler)) {
triggerAfterCompletion(request, response, null);
return false;
}
this.interceptorIndex = i;
}
}
return true;
}

M51_02 : DispatcherServlet # doDispatch 源码

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
java复制代码
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;

// 准备异步处理管理器 WebAsyncManager
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

try {
ModelAndView mv = null;
Exception dispatchException = null;

try {
// 尝试解析 Multipart Request
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

// 获取具体需要执行的 Handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

// 确定当前请求的处理程序适配器
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

// 处理 Get-Head 请求
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}

// 拦截器前置处理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}

// 调用 HandlerAdapter 处理 请求
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

if (asyncManager.isConcurrentHandlingStarted()) {
return;
}

applyDefaultViewName(processedRequest, mv);
// 拦截器后置处理
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}

2.3.2 HandlerMapping 解析到方法

上文中已经获取到了 Adapter , 以下就是 Adapter 的详细处理

流程处理

流程处理分为2个部分 , 简单点说就是 :

  • getHandler 获取使用的 MethodHandler
  • getHandlerAdapter 获取适配处理器
  • 通过适配处理器处理 MethodHandler

一 : 获取处理的 Handler (getHandler)

DispatcherServlet # M51_03- getHandler 中可以看到 , 通过 HandlerExecutionChain # getHandler 获取 Handler 对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码C76- AbstractHandlerMapping 
M76_01- HandlerExecutionChain getHandler(HttpServletRequest request) : 构建处理链
- getHandlerInternal(request) 获取处理的 Handler -> M32_01
- 通过 Handler 构建 HandlerExecutionChain TODO
- 跨域配置的处理 CorsConfiguration
M76_02- getHandlerInternal
- 调用 父类 -> M32_01

C32- AbstractHandlerMethodMapping
M32_01- getHandlerInternal(HttpServletRequest request)
- 先配置了一个读锁
- 调用 lookupHandlerMethod -> M32_02
M32_02- lookupHandlerMethod : 对请求做真正的处理 , 获取 HandlerMethod
1- 构建 List<Match> 用于存放匹配结果
- MappingRegistry 通过 lookupPath 获取对象 List<RequestMappingInfo>
?- 底层实际上是调用上文初始化使用的 urlLookup.get(urlPath)
- 调用 addMatchingMappings , 将匹配的 Match放入 1 步构建的 List 中
- 返回处理的 Method

M32_02 lookupHandlerMethod 请求处理

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
java复制代码protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
// 从 MappingRegistry 中获取 Mapping List
List<T> directPathMatches = this.mappingRegistry.getMappingsByUrl(lookupPath);
if (directPathMatches != null) {
addMatchingMappings(directPathMatches, matches, request);
}
if (matches.isEmpty()) {
// No choice but to go through all mappings...
addMatchingMappings(this.mappingRegistry.getMappings().keySet(), matches, request);
}
// 如果匹配项不为空
if (!matches.isEmpty()) {
// {POST /test/getBody}
Match bestMatch = matches.get(0);
// 进行匹配 , 获取对应的 HandlerMethod
if (matches.size() > 1) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
bestMatch = matches.get(0);
if (CorsUtils.isPreFlightRequest(request)) {
return PREFLIGHT_AMBIGUOUS_MATCH;
}
Match secondBestMatch = matches.get(1);
if (comparator.compare(bestMatch, secondBestMatch) == 0) {
Method m1 = bestMatch.handlerMethod.getMethod();
Method m2 = secondBestMatch.handlerMethod.getMethod();
String uri = request.getRequestURI();
throw new IllegalStateException(
"Ambiguous handler methods mapped for '" + uri + "': {" + m1 + ", " + m2 + "}");
}
}

// request.setAttribute(HandlerMapping.PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE, lookupPath) -> PS:M32_02_01
request.setAttribute(BEST_MATCHING_HANDLER_ATTRIBUTE, bestMatch.handlerMethod);

handleMatch(bestMatch.mapping, lookupPath, request);
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}

// PS:M32_02_01
String PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE = HandlerMapping.class.getName() + ".pathWithinHandlerMapping";

步骤二 : 使用获取的 Handler (HandlerAdapter.handle)

从步骤一中我们已经获取到了 HandleMethod , 后续就是相关的处理逻辑

  • DispatcherServlet # doDispatch : 请求入口
  • AbstractHandlerMethodAdapter # handle : RequestMappingAdapter 的父入口
  • RequestMappingHandlerAdapter # handleInternal :请求处理主流程
  • RequestMappingHandlerAdapter # invokeHandlerMethod
  • ServletInvocableHandlerMethod # invokeAndHandle
  • InvocableHandlerMethod # invokeForRequest
  • InvocableHandlerMethod # doInvoke
  • Method # invoke : Java 反射到方法

步骤2-1 : RequestMappingHandlerAdapter # handleInternal 处理

在该方法中 , 如果需要视图解析,则调用准备ModelAndView的RequestMapping处理程序方法

核心 : ModelAndView 的准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
java复制代码protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

ServletWebRequest webRequest = new ServletWebRequest(request, response);
try {
WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

// 扩展InvocableHandlerMethod,通过注册的HandlerMethodReturnValueHandler处理返回值
ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
if (this.argumentResolvers != null) {
invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
}
if (this.returnValueHandlers != null) {
invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
}
invocableMethod.setDataBinderFactory(binderFactory);
invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

// ModuleAndView 容器
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
modelFactory.initModel(webRequest, mavContainer, invocableMethod);
mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

AsyncWebRequest asyncWebRequest = WebAsyncUtils.createAsyncWebRequest(request, response);
asyncWebRequest.setTimeout(this.asyncRequestTimeout);

WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.setTaskExecutor(this.taskExecutor);
asyncManager.setAsyncWebRequest(asyncWebRequest);

// 异步管理器注册回调拦截器
asyncManager.registerCallableInterceptors(this.callableInterceptors);
asyncManager.registerDeferredResultInterceptors(this.deferredResultInterceptors);

if (asyncManager.hasConcurrentResult()) {
Object result = asyncManager.getConcurrentResult();
mavContainer = (ModelAndViewContainer) asyncManager.getConcurrentResultContext()[0];
asyncManager.clearConcurrentResult();
invocableMethod = invocableMethod.wrapConcurrentResult(result);
}

invocableMethod.invokeAndHandle(webRequest, mavContainer);
if (asyncManager.isConcurrentHandlingStarted()) {
return null;
}

return getModelAndView(mavContainer, modelFactory, webRequest);
}
finally {
webRequest.requestCompleted();
}
}

ModelAndViewContainer 参数详情 :

image.png

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码



// Web 模块最终步骤
C- InvocableHandlerMethod


// 代码最终步骤 : 调用 Method 的 invoke 方法 , 去完成具体的逻辑
C39- Method
M39_01- invoke(Object obj, Object... args)
?- 通过此方法调用最后的代理类 . java 反射包中方法 ,省略

2.4 补充 : 属性的转换

属性转换的主要类是 RequestMappingHandlerAdapter , 其主要调用逻辑为 :

  • C90- RequestMappingHandlerAdapter # M90_1- invokeHandlerMethod
  • C91- ServletInvocableHandlerMethod # M91_1- invokeAndHandle
  • C92- InvocableHandlerMethod # M92_1- invokeForRequest : 对请求进行处理
  • C93- InvocableHandlerMethod # M93_1- getMethodArgumentValues : 获取参数
  • C94- HandlerMethodArgumentResolverComposite # M94_1- resolveArgument : 解析参数
  • C95- RequestResponseBodyMethodProcessor # M95_1- readWithMessageConverters : 转换参数
  • C96- AbstractMessageConverterMethodArgumentResolver # M96_1- readWithMessageConverters : 最终处理逻辑
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复制代码C- RequestMappingHandlerAdapter
M- invokeHandlerMethod(HttpServletRequest request,HttpServletResponse response, HandlerMethod handlerMethod)
- invocableMethod.invokeAndHandle(webRequest, mavContainer)

// Step 1 : 处理的入口
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

// 将 Request 解析处理
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);

if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
} else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}

mavContainer.setRequestHandled(false);
try {
this.returnValueHandlers.handleReturnValue(returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception ex) {
throw ex;
}
}


// PS : invokeForRequest 中主要流程
C- InvocableHandlerMethod
M- invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,Object... providedArgs)
- getMethodArgumentValues(request, mavContainer, providedArgs)
?- 详见下文参数获取

Step 2 : 参数的获取

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复制代码    protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}

// 构建一个 Object 数组用于存放 param
Object[] args = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
// 解析参数主体
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
throw ex;
}
}
return args;
}



public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
if (resolver == null) {
throw new IllegalArgumentException("Unsupported parameter type [" +
parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
}
return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
}

Step 3 : RequestResponseBodyMethodProcessor 解析 RequestBody

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
java复制代码 
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {

parameter = parameter.nestedIfOptional();
// 读取并且转换为相关对象
Object arg = readWithMessageConverters(webRequest, parameter, parameter.getNestedGenericParameterType());
String name = Conventions.getVariableNameForParameter(parameter);

if (binderFactory != null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, arg, name);
if (arg != null) {
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new MethodArgumentNotValidException(parameter, binder.getBindingResult());
}
}
if (mavContainer != null) {
mavContainer.addAttribute(BindingResult.MODEL_KEY_PREFIX + name, binder.getBindingResult());
}
}
return adaptArgumentIfNecessary(arg, parameter);
}


// Step : 转换操作入口
protected <T> Object readWithMessageConverters(NativeWebRequest webRequest, MethodParameter parameter,
Type paramType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
ServletServerHttpRequest inputMessage = new ServletServerHttpRequest(servletRequest);

Object arg = readWithMessageConverters(inputMessage, parameter, paramType);
if (arg == null && checkRequired(parameter)) {
throw new HttpMessageNotReadableException("Required request body is missing: " +
parameter.getExecutable().toGenericString(), inputMessage);
}
return arg;
}

Step end : 最终处理

最终处理中, 对 属性参数进行了最后的映射

PS: M96_1_01 继承体系

  • C- EmptyBodyCheckingHttpInputMessage
  • C- HttpInputMessage
  • C- HttpMessage

PS:M96_01_02 , 所有的 MessageConverter

  • ByteArrayHttpMessageConverter
  • StringHttpMessageConverter
  • ResourceHttpMessageConverter
  • ResourceRegionHttpMessageConverter
  • SourceHttpMessageConverter
  • AllEncompassingFormHttpMessageConverter
  • Jaxb2RootElementHttpMessageConverter
  • MappingJackson2HttpMessageConverter
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
java复制代码protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {

MediaType contentType;
boolean noContentType = false;
try {
// 内容类型 : application/json;charset=UTF-8
contentType = inputMessage.getHeaders().getContentType();
} catch (InvalidMediaTypeException ex) {
throw new HttpMediaTypeNotSupportedException(ex.getMessage());
}
if (contentType == null) {
noContentType = true;
contentType = MediaType.APPLICATION_OCTET_STREAM;
}

// Controller 类
Class<?> contextClass = parameter.getContainingClass();
// 实体类 class
Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
if (targetClass == null) {
ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
targetClass = (Class<T>) resolvableType.resolve();
}

// 获取请求类型
HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
Object body = NO_VALUE;

EmptyBodyCheckingHttpInputMessage message;
try {
// PS: M96_1_01
message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

// 获取所有的 HttpMessageConverter -> PS:M96_01_02
for (HttpMessageConverter<?> converter : this.messageConverters) {
Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
GenericHttpMessageConverter<?> genericConverter =
(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
// 此处主要是 MappingJackson2HttpMessageConverter
if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
(targetClass != null && converter.canRead(targetClass, contentType))) {
if (message.hasBody()) {
// 转换核心流程 -> PS:M96_01_02
// 主要为 Header
HttpInputMessage msgToUse =
getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
} else {
body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
}
break;
}
}
} catch (IOException ex) {
throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
}

if (body == NO_VALUE) {
if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||(noContentType && !message.hasBody())) {
return null;
}
throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
}

MediaType selectedContentType = contentType;
Object theBody = body;
LogFormatUtils.traceDebug(logger, traceOn -> {
String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
});

return body;
}

PS:M96_01_02 参数详情

convert_body.jpg

convert_maptoUser.jpg

2.5 补充 : Response 的转换

属性的转换主要在 HandlerMethodReturnValueHandlerComposite 中进行 , 主要的逻辑为 :

  • C- InvocableHandlerMethod # invokeForRequest
  • C- ServletInvocableHandlerMethod # invokeAndHandle
  • C- HandlerMethodReturnValueHandlerComposite # handleReturnValue : 调用 Handler 处理
  • C- RequestResponseBodyMethodProcessor # handleReturnValue
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复制代码// - C91- ServletInvocableHandlerMethod  # M91_1- invokeAndHandle
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {

Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);

if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
} else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}

mavContainer.setRequestHandled(false);
try {
// 此处进行 return 处理 : HandlerMethodReturnValueHandlerComposite
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
} catch (Exception ex) {
throw ex;
}
}


// HandlerMethodReturnValueHandlerComposite # handleReturnValue
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {

HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType);
if (handler == null) {
throw new IllegalArgumentException("Unknown return value type: " + returnType.getParameterType().getName());
}
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
}

// RequestResponseBodyMethodProcessor
public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException {

mavContainer.setRequestHandled(true);
ServletServerHttpRequest inputMessage = createInputMessage(webRequest);
ServletServerHttpResponse outputMessage = createOutputMessage(webRequest);

// 此处进行返回处理 , 本次先不深入 , 以后单独看看返回的处理
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
}

总结

总算是把 MVC 写完了 , 本来篇幅不大 ,结果越写越多 , 反而不容易说清楚了.

初始化阶段中 , RequestMappingHandlerMapping 调用 initHandlerMethods 完成对所有的 Bean 进行扫描 , 最后构建 RequestMappingInfo 再通过 MappingRegistry 进行注册

在调用的阶段 , 首先调用 DispatcherServlet , 它会调用 AbstractHandlerMapping 中 getHandler 获取 HandlerMapping ( lookupHandlerMethod 调用 MappingRegistry 进行匹配)

匹配完成后 , 会来到 Adapter 主逻辑 , RequestMappingHandlerAdapter 中进行 ModelAndView 等处理 , 同时调用 ServletInvocableHandlerMethod 进行方法的映射

在方法映射阶段 , 会来到 InvocableHandlerMethod 类 , 首先会进行参数的解析和映射 , 最后通过 AbstractMessageConverterMethodArgumentResolver 和 HttpMessageConverter 进行联合处理

映射完成后 , 就会 invoke 到对应的方法 , 同时通过 HandlerMethodReturnValueHandlerComposite 和 RequestResponseBodyMethodProcessor 进行 Response 的处理

更新日志

V20210821 更新流程图及总结

本文转载自: 掘金

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

1…661662663…956

开发者博客

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