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

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


  • 首页

  • 归档

  • 搜索

volatile之防止指令重排| 8月更文挑战

发表于 2021-08-01

volatile可以防止指令重排,在多线程环境下有时候我们需要使用volatile来防止指令重排,来保证代码运行后数据的准确性

什么是指令重排?

计算机在执行程序时,为了提高性能,编译器和处理器一般会进行指令重排,一般分为以下三种:

image.png

指令重排有以下三个特点:

1.单线程环境下指令重排后可以保证与顺序执行指令的结果一致(就是不进行指令重排的情况)

1
2
3
4
5
6
java复制代码//原来的执行顺序
a=1;
b=0;
//进行指令重排后执行
b=0;
a=1;

这两个顺序执行的指令结果都是a=1,b=0

2.进行指令重排的时候要考虑指令之间的数据依赖性(某个指令的数据需要根据另一个指令的数据获得)

1
2
3
4
5
6
7
8
9
java复制代码//原来的执行顺序
a=0; //指令1
a=10; //指令2
b=a+1; //指令3

//进行指令重排后
a=0;
b=a+1;
a=10;

此时两种顺序输出的结果就不一样了,这是因为指令3的数据依赖于指令2,单线程环境下指令重排不会出现这种情况。

3.多线程环境下,多个线程交替执行,由于编译器会进行指令重排,结果无法预测。

为什么指令重排能够提高性能

串行的代码确实会按代码语意正确的执行(就是编写的代码的运行逻辑),但是编译器对于代码本身的优化却并不一定会按实际的代码一步一步的执行,就比如下面这段代码

1
2
3
4
java复制代码public void process() {
int a = 10; #指令1
int b = 20; #指令2
}

代码的执行过程一定是是int a=10然后int b=20,但是代码转换成计算机可以识别的指令可能是指令2,指令1。

我们知道指令的执行可以分为这几步:

  • 取址 IF
  • 译码和取寄存器操作数 ID
  • 执行或者有效地址计算 EX (ALU逻辑计算单元)
  • 存储器访问 MEM
  • 写回 WB (寄存器)

一段代码并不是由单条指令就可以执行完毕的,而是通过流水线技术来执行多条指令。

流水线技术是一种将指令分解为多步,并让不同指令的各步操作重叠,从而实现几条指令并行处理,这样就提高了指令的执行速度

简单来说就是通过指令重排,可以使用流水线技术实现指令的细分,然后实现几条指令的并行处理,从而提高速度

volatile是怎么禁止指令重排的?

这就涉及到一个概念内存屏障(内存栅栏),它是一个cpu指令,有两个作用:

  1. 保证某些特定操作的执行顺序
  2. 保证某些变量的内存可见性(实现了volatile保证可见性)

编译器和处理器都可以进行指令重排,那么如果我们在程序中插入一条Memery Barrier(内存屏障),那么就会告诉编译器和cpu不能对这条指令进行重排,也就是说通过插入内存屏障,使屏障前后的指令不会进行重排优化,内存屏障还可以强制刷出cpu的缓存,因此cpu上的线程都能读到这些数据的最新版本。

简单来说就是插入内存屏障后告诉cpu和编译器,这个内存屏障前后的指令你不要给我进行重排序

内存屏障分为四种:

StoreStore屏障、StoreLoad屏障、LoadLoad屏障、LoadStore屏障。

  • Load相当于读屏障
  • Store相当于写屏障

本文转载自: 掘金

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

基于Java swing ATM简单的银行管理系统 |8月更

发表于 2021-08-01

项目介绍:

本项目是使用Java swing开发,可实现ATM系统/银行系统的基本登陆、转账、查询余额、存取款业务。界面设计比较简介

一、 概述****

自动柜员机(ATM)自发明以来,以其高效和低成本一直受到银行业的青睐,从我国引进第一台柜员机至今已经拥有6万台左右的规模,但与世界平均水平相比,我国的人均拥有量还有差距,与先进国家相比人均拥有数量甚至不到四分之一。为了增强我国银行的竞争力,国家相继出台了很多相关措施,比如《关于促进银行卡产业发展的若干意见》等,各商业银行也对柜员机的发展制定了详细的规划,甚至出现了由银行单独购买模式发展为银行租赁商业公司ATM的模式。可以说,随着我国经济的屈起,我国的柜员机也正面临着一个快速发展的春天。

二、设计目的****

(1)复习、巩固软件开发的基础知识,进一步加深对软件开发的理解和掌握;

(2)课程设计为学生提供了一个既动手又动脑,独立实践的机会,将课本上的理论知识和实际有机的结合起来,锻炼学生的分析解决实际问题的能力。提高学生适应实际,实践编程的能力;

(3)培养学生在项目开发中团队合作精神、创新意识及能力。

三、系统总体设计****

3.1系统功能模块划分本系统主要包括登陆模块设计,选择服务模块设计,取款服务模块设计,修改密码模块设计,存款服务模块设计,查询余额模块设计,转账服务模块设计,退卡操作模块设计等几个功能模块。

(1) 登陆模块设计:与ATM柜员机打交道比较多的,也是大家对安全比较关心的问题:密码。所以第一个界面就是要输入密码和卡号才能继续服务。

(2) 选择服务模块设计:在选择服务模块中,有各种ATM的服务功能,只要用户在该界面中选择按钮,它就会弹出各个相应的界面。

(3) 取款模块设计:在取款模块中,和其他的界面也是同样的结构。也是有一个文本框和一个按钮还有标签组成的,实现用户取款的服务

(4) 修改密码模块设计:在修改密码模块中,必须要两次输入你的新密码,且两次要匹配,否则系统会报错,然后再重新回到修改密码的界面

(5) 存款模块设计:在存款模块中,和其他的界面也是同样的结构。也是有一个文本框和一个按钮还有标签组成的,实现用户取款的服务

(6) 查询余额模块设计:此功能非常简单,仅仅是实现弹出卡的功能。

(7) 转账服务模块设计:在转账模块中,必须两次输入要转账的账户号,并且两次输入必须相同,否则系统会报错;然后输入转账金额,完成转账

(8) 退出操作模块设计:此功能非常简单,仅仅是实现弹出卡的功能。

ATM取款机系统功能结构图我所设计的ATM柜员机主要是由登录页面模块还由选择服务模块组成,其中选择服务模块又由取款服务模块、修改密码模块、存款服务模块

项目结构:

运行截图:

))

)))

好了,今天就到这儿吧,小伙伴们点赞、收藏、评论,一键三连走起呀,下期见~~

本文转载自: 掘金

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

JVM垃圾回收器详解(上)-CMS收集器| 8月更文挑战 J

发表于 2021-08-01

JVM垃圾回收器详解(上)-CMS收集器| 8月更文挑战

前言: 了解垃圾收集器之前需要先了解一下垃圾收集算法,如有不了解垃圾收集算法的,点击
JVM垃圾收集算法详解前往了解

Serial收集器

serial收集器是JVM中诞生最早的一款垃圾收集器,既然是诞生的最早那么肯定有当时的一些局限性,先Serial翻译过来是连续的,顺序的,所以可以知道它是串行的,单线程收集器。它的单线程不单单是只有一条线程在进行垃圾回收,还有因为在垃圾回收的时候必须停掉其他的工作线程,也就是STW,直到它垃圾回收结束,其他工作线程才能继续进行。

垃圾回收算法: 新生代采用复制算法,老年代采用标记-整理算法。

STW:全文为Stop The World(停止世界), 说的是指,在进行GC的过程中,会产生让我感觉应用停顿的感觉,可以理解为我们平时遇到的一种卡顿过程,整个程序的工作线程都被停止,你的请求无法得到任何响应,仿佛程序停止运行了一样,这种停顿称作为STW。

image.png

 简单提一下老年代版本的Serial收集器,叫Serial Old是Serial收集器的老年代版本,它也同样是一个”单线程””的垃圾收集器。由于也是单线程,所以使用的场景也很少了既然提到了,那就说明还是有它的用途,目前主要是用在JDK1.5以前的版本与Parallel Scavenge收集器一起使用,这种场景很少了,现在基本上都是JDK1.8,再次一点是1.7或1.6,所以基本可以忽略,另外一种用途是在CMS收集器并发的阶段出现concurrent mode failure则会使用Serial Old收集器的作为后备方案。

使用参数设置:-XX:+UseSerialGC -XX:+UseSerialOldGC

Parallel Scavenge收集器

按照程序发展的一个顺序来说,单线程的既然已经有了,那么接下来就应该是多线程了,所以Parallel Scavenge收集器登场了,因此可以说是Serial收集器的多线程版本。因为在除了用多线程进行收集垃圾以外,在其它的地方与Serial收集器很类似,例如垃圾回收算法,参数设置等等。另外Parallel Scavenge最关注的是CPU中用于运行用户代码的时间与CPU总消耗时间的比值,可以称作为吞吐量,可以通过 Scavenge提供的各种参数设置,来找到自己程序最合适的一个吞吐量。

image.png

垃圾回收算法: 新生代采用复制算法,老年代采用标记-整理算法。
使用参数设置: -XX:+UseParallelGC(年轻代) -XX:+UseParallelOldGC(老年代)

ParNew收集器

ParNew收集器跟 Parallel Scavenge收集器区别不是很大,主要区别在于它可以和CMS收集器来进行配合使用,它也是目前唯一能跟CMS收集器搭配使用的,当你指定CMS收集器的时候,默认会使用ParNew收集器作为新生代收集器

image.png

使用参数设置: -XX:+UseParNewGC

垃圾回收算法: 新生代采用复制算法,老年代采用标记-整理算法。

CMS收集器

CMS收集器全称为Concurrent Mark Sweep,并行标记扫描,是一款并发的老年代垃圾收集器,年轻代默认使用ParNew收集器,GC线程与工作线程可以同时进行,并且是通过以最短停顿时间(STW)为目标的收集器,从CMS中的Mark与Sweep可以看出,主要是采用的标记-清除算法,另外CMS收集器的运行过程与前面的几种收集器有很大的不同,主要可以分为四个步骤,如下:

  • 初始标记:停止所有的GC以外的线程,也就是STW,然后通过可达性分析算法,记录GC ROOTs能直接引用的对象,这一步速度很快.
  • 并发标记:并发标记阶段主要是从出书标记中记录的GC Roots的直接关联对象开始遍历整个对象的过程,这个过程中涉及的过程比较复杂,因此耗时比较长,但因为是并发的一个流程,GC线程与工作线程同时进行,是不需要停顿工作线程。这样做带来的弊端就是,由于程序在一直运行,会导致之前标记过的对象,在并发标记过程中发生了状态改变,因此才有了接下来的重新标记。
  • 重新标记:重新标记阶段主要是为了修正并发标记过程中在初始标记阶段记录后发生状态改变的对象,使用到的是三色标记中的增量更新算法,此阶段会进行STW,停顿时间比初始标记稍长,但比并发标记阶段时间段很多。
  • 并发清理: 同时开启工作线程与GC线程,GC线程主要对未标记的区域进行一个清理,由于也是并发的过程,这个阶段也会有新增的对象被标记,那么不会对该对象做任何处理。
  • 并发重置:重置这次GC过程中的标记数据。

image.png

优点:

  1. 并发收集:整个过程中,除了初始标记与重新标记会发生外,其他过程都是并发过程。
  2. 低停顿:除开初始标记与重新标记这两个过程会发生STW停顿外,其他的三个过程中不会发生STW停顿 。

缺点:

  1. CPU资源问题:CPU使用较强,容易抢多服务CPU资源。
  2. 浮动垃圾问题:产生的浮动垃圾无法处理,如在并发标记与并发阶段中又产生垃圾那么只能等到下次GC再进行处理。
  3. 空间碎片问题:由于使用的标记-清除算法,因此在回收结束的时候会产生空间碎片问题,但是可以通过参数设置来解决,不过会消耗点性能。
  4. concurrent mode failure问题:在并发标记或并发清理阶段的时候,如果又触发了垃圾回收,但是本次垃圾回收又没进行完,那么则会触发concurrent mode failure,这时候则会进入到单线程收集,先stop the world,随后使用上文提到的serial old垃圾回收器来进行回收。

垃圾回收算法: 标记-清除算法。

使用参数设置: -XX:+UseConcMarkSweepGC

其他参数设置:

  • -XX:+UseCMSCompactAtFullCollection: 可以让jvm在执行完标记清除后再做整理,解决上文提到的空间碎片问题,类似使用标记-整理算法。
  • -XX:ConcGCThreads:设置GC的时候并发线程数
  • -XX:CMSInitiatingOccupancyFraction: 老年大使用百分比达到该值的时候会触发垃圾回收(默认是92)
  • -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW。
  • -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW。

总结

Serial收集器与Parallel Scavenge收集器都是比较久远的收集器了,随后的CMS收集器,G1收集器(下期会讲)都是更优秀的收集器,Serial的特别性在于它的老年代版本Serial Old可以作为CMS收集器在concurrent mode failure的时候的备用,而ParNew作为CMS的搭档,负责新生代的收集,CMS负责老年代的收集.

Q.E.D.

本文转载自: 掘金

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

「 8月更文挑战 」预估活动排行榜,快来看看你排第几名

发表于 2021-08-01

null

文章会在 每天0点30、6点30、12点30、16点30 定时更新,但是有审核,数据更新有延迟 无延迟点这里

非官方统计,更新时间: 2021-08-31 09:49

前言

xdm,大家好,我是摸鱼专家呀,我又带来最新的排行榜啦。

初衷:通过排行榜激励每位创作者,创作出许许多多的优质文章,让大家卷起来。

排名

排名 作者 文章数 获赞数 浏览数 评论数 收藏数
1 猿说编程 54 62 2129 0 0
2 Shockang 50 48 3325 0 5
3 CDFMLR 40 69 7004 5 16
4 张中华 37 33 1789 2 1
5 西瓜watermelon 35 189 2422 3 7
6 zidea 35 548 11100 13 36
7 Gaby 33 121 8823 10 70
8 白又白i 32 215 5620 44 43
9 csdn来挖墙脚 32 241 3978 12 8
10 coderymy 31 60 1890 3 9
11 有出路 30 962 6335 8 20
12 职说测试 30 90 12578 0 8
13 LolitaAnn 30 35 1574 0 0
14 Cache技术分享 30 63 1041 1 0
15 kobesdu 30 11 1049 1 0
16 逍遥游lx 30 903 5310 10 27
17 干货满满张哈希 29 134 9161 17 87
18 NewBoy 29 133 3024 5 7
19 岛上码农 29 834 15752 17 59
20 白祁ovo 29 61 1451 2 1
21 小憨憨 29 86 3627 5 25
22 CZY 29 51 1534 3 0
23 硬核项目经理 29 7 1388 0 1
24 HelloWorld杰少 29 126 42235 34 16
25 雄狮虎豹 29 32 991 0 0
26 Rubble 29 48 1694 2 8
27 天行无忌 29 204 8683 7 131
28 阿策小和尚 29 71 5931 5 20
29 雷子说测试开发 29 8 1090 0 4
30 沐华 29 3544 107049 272 3916
31 云淡风轻的博客 29 33 1953 0 10
32 木亦Sam 29 1142 12372 28 166
33 xn213 29 73 2463 2 40
34 灰小猿 29 8 921 0 1
35 轩_xuan 29 84 1619 1 27
36 三掌柜 29 49 2192 33 33
37 小生凡一 29 618 22830 11 60
38 也笑 29 26 1808 0 2
39 Android唐浮 29 60 4663 7 37
40 半月子 29 266 1436 2 1
41 传达室老大爷 29 26 1596 0 4
42 yao 29 592 9031 1 82
43 不喝奶茶的Programmer 29 36 2546 0 5
44 HXYDJ 29 8 1328 0 0
45 是小小小前端呀 29 163 3062 2 13
46 xiaoff 29 902 7825 17 33
47 小黄鸡1992 29 471 13636 50 168
48 茶还是咖啡 29 48 2392 0 14
49 battleKing 29 224 6639 8 81
50 YuJian 29 14 1710 0 3
51 青烟小生 28 180 4091 0 29
52 liguodongiot 28 9 1658 2 2
53 万恶的沫白 28 34 1252 1 2
54 迷途小书童的Note 28 9 2870 2 2
55 无数 28 11 1238 0 0
56 宁在春 28 58 3056 8 20
57 伯仁 28 118 2419 7 5
58 gyx_这个杀手不太冷静 28 840 13249 27 138
59 _island 28 934 19283 67 379
60 笑容不是为了我 28 155 32021 2 103
61 打前端工人 28 50 1730 0 11
62 周帅帅 28 49 1484 2 2
63 夏季的野兽 28 78 3362 2 12
64 柠檬夕 28 50 1343 0 4
65 DS小龙哥 28 1 800 0 0
66 Mr_Wen 28 58 2132 1 0
67 彼岸繁華 28 85 2572 6 18
68 BiaDisaForTruth 28 15 831 0 1
69 _黄黄黄 28 1106 226256 165 1441
70 扫地僧的未来 28 53 1518 0 1
71 由也_ 28 75 1884 29 7
72 胖先森 28 43 3575 3 23
73 顽疾 28 7 858 0 3
74 凌大哥 28 14 2448 0 0
75 dangsh 28 62 2112 6 3
76 攻城狮Chova 28 68 2088 0 17
77 用户4398647336297 28 45 1121 3 1
78 coderwxf 28 31 2353 3 0
79 一撸程猿 28 7 1140 0 0
80 __white 28 96 6337 7 34
81 Lucifer三思而后行 28 1111 21836 20 46
82 盆友圈的小可爱 28 395 3482 8 10
83 jacqueshuang 28 59 1073 2 0
84 Andy阿辉 28 305 3217 3 7
85 liuzhen007 28 41 2089 0 6
86 君若雅 28 8 1320 0 2
87 前端郭德纲 28 44 2600 1 6
88 MySQ 28 56 2202 0 9
89 潇雷 28 548 11387 10 69
90 是芝麻吖 28 135 3766 13 23
91 代码迷途 28 881 8543 25 34
92 黑白绮 28 10 1173 0 2
93 青Cheng序员石头 28 807 8155 14 37
94 flager 28 43 2913 4 6
95 林深鹿 28 766 8688 13 68
96 卢卡多多 28 49 1435 0 6
97 哪能一直都快乐 28 230 5037 1 16
98 答案cp3 28 40 1761 7 3
99 ABestRookie 28 67 1813 0 4
100 阿银 28 90 2641 3 6
101 离岛121 28 13 1231 0 0
102 小阿杰 28 581 13630 24 63
103 KevinQ 28 68 3153 10 11
104 ruochen 28 51 2210 1 5
105 webmote33 28 165 11738 8 19
106 申阳 28 34 1625 0 2
107 快乐的提千万 28 36 1625 0 5
108 saberlgy 28 1141 7350 62 16
109 Andy_W 28 49 1117 1 0
110 Axjy 28 180 6473 25 56
111 y大壮 28 118 2313 1 4
112 小只前端攻城狮 28 605 7634 12 66
113 跋扈洋 28 3 856 0 0
114 叶秋主 28 38 3621 1 18
115 小丞同学 28 936 18092 39 164
116 JaylenL 28 559 9672 18 164
117 阿华12年 28 266 14052 19 44
118 XiaoLin_Java 28 882 18881 54 480
119 杰哥的IT之旅 28 961 24816 56 221
120 forever_Mamba 28 13 1659 2 0
121 卡喵妹 28 159 6236 4 18
122 爱看书的阿东 28 22 1715 0 7
123 书旅 28 50 3772 0 11
124 西柚果儿 28 6 1986 0 1
125 小魔童哪吒 28 710 10574 52 119
126 性感的小肥猫 28 125 3907 11 34
127 大鱼丶 28 1317 32978 36 153
128 潜心专研的小张同学 28 64 1957 0 14
129 前端picker 28 1500 18030 42 390
130 camellia 28 227 1758 4 4
131 劲风君 28 3 1088 0 0
132 Code皮皮虾 28 962 8024 23 72
133 不熬夜的程序猿 28 15 1062 0 0
134 雷学委 28 1233 11090 49 38
135 海听山歌ē 28 33 1102 0 2
136 山花 28 19 1237 0 0
137 空城机 28 483 3474 2 15
138 Augus 28 53 1399 0 2
139 码农戏码 28 12 1098 3 5
140 ShaderJoy 28 80 3996 7 3
141 程序员小潘 28 38 1762 0 7
142 liedmirror 28 84 2429 0 5
143 Ada_lake 27 6 1464 0 1
144 全栈道路 27 264 6319 3 113
145 ReganYue 27 668 9503 29 72
146 程序媛小庄 27 101 3237 2 15
147 诗一样的代码 27 1153 5689 15 13
148 rexkentzheng 27 47 2569 2 5
149 Gogo472 27 2 725 0 0
150 初念初恋 27 414 10118 16 86
151 小鲸鱼mm 27 34 1862 3 6
152 宫水三叶的刷题日记 27 77 6354 4 6
153 编程三昧 27 2560 36255 222 626
154 多喝热水__ 27 24 1347 0 3
155 奔跑的码仔 27 13 1484 1 0
156 梦想橡皮擦 27 22 1713 1 6
157 BrysonLin 27 70 1791 6 9
158 铭铭870 27 59 1502 0 5
159 张哲溪 27 4 1092 4 1
160 程序那些事 27 209 67880 46 80
161 Python测试和开发 27 171 3419 6 6
162 肥学大师 27 641 6432 5 27
163 小阿肥 27 158 2515 1 5
164 大熊G 27 823 8790 22 63
165 Always_positive 27 151 2532 0 18
166 用户3567784200662 27 13 2503 0 5
167 派大星不吃鱼 27 11 908 2 1
168 noBug123 27 22 1682 2 8
169 太子爷哪吒 27 1094 40861 39 182
170 数学家是我理想 27 7 1785 1 2
171 _q2nAmor 27 57 1884 3 4
172 SloppyJack 27 53 3063 6 1
173 红尘炼心 27 603 17598 37 94
174 张风捷特烈 27 351 15105 58 43
175 王大呀呀 27 534 4417 4 1
176 快跑啊小卢_ 27 1083 20024 105 508
177 CadeCode 27 98 2667 0 7
178 墨三十一 27 43 1680 0 4
179 chaoCode 27 103 2282 2 15
180 小码哥Damon 27 75 2202 0 20
181 Java雏鸡开发 27 140 4033 7 32
182 xcbeyond 27 153 53408 44 90
183 何小玍 27 300 8125 20 122
184 Mark_Zoe 27 221 5206 6 24
185 奔跑吧鸡翅 27 16 2190 0 3
186 一灰灰 27 217 15954 28 169
187 周杰倫本人 27 11 852 0 1
188 海拥 27 734 37831 16 44
189 ZJPRENO 27 207 16846 23 43
190 请叫我阿ken 27 2098 17778 104 142
191 hyman 27 21 1826 2 3
192 前端_卡卡西 27 23 1911 5 3
193 Yang0312 27 49 1513 1 1
194 如风飞翔 27 9 1211 0 1
195 陆景学 27 42 1829 0 0
196 xbhog 27 190 4579 9 23
197 大前端驿站 27 64 1686 0 1
198 霍文俊 27 417 2856 5 6
199 CocaCoder 27 321 2732 2 5
200 小蜗牛耶 27 33 1424 9 5
201 lio_zero 27 56 2299 1 5
202 计蒙不吃鱼 27 78 6986 14 34
203 Nordon 27 171 5186 6 18
204 不思量自难忘 27 412 3878 7 4
205 WangScaler 27 213 4908 13 37
206 ZackSock 27 94 4424 5 22
207 李浩宇Alex 27 305 5568 9 66
208 Java全栈路线 27 32 1467 0 3
209 公众号iOS逆向 27 287 6712 5 48
210 Panda_Loncon 27 43 1683 0 2
211 海轰Pro 27 40 1861 1 3
212 badspider 27 27 1911 5 6
213 小喵说编程 27 10 1179 0 0
214 脑子进水养啥鱼 27 41 1716 2 5
215 安逸的咸鱼 27 396 5784 6 38
216 so丶简单 27 167 6025 6 21
217 EngineerForSoul 27 7 1000 3 1
218 流浪尐仙 27 36 2499 2 10
219 又菜又想玩的XXX 27 80 2666 3 12
220 一点儿 27 99 3371 4 9
221 芝麻粒儿 27 277 9939 9 17
222 随身电源 27 116 3125 3 18
223 架构精进之路 27 76 3849 0 24
224 爱吃红薯粉 27 218 4516 0 18
225 云的世界 27 4500 128161 644 3214
226 南极大冰块 27 107 3071 16 16
227 Python研究者 27 93 4294 2 9
228 李迟 27 5 1219 0 0
229 silly8543 27 35 1185 2 6
230 tigeriaf 27 323 6884 9 24
231 Gw_gw 27 218 1611 0 3
232 咸鱼学Python 26 42 3109 0 3
233 HQ数字卡 26 20 1104 0 0
234 是xiuxi休息啊 26 104 1840 0 4
235 GTW_Zeus 26 62 1937 12 5
236 Luckly123博客 26 110 1749 5 4
237 印说十二越 26 31 2896 2 19
238 podongfeng 26 356 2887 1 5
239 临时营地 26 142 3224 8 12
240 程序员Daddy 26 27 1317 0 5
241 小_小陳 26 15 1068 1 2
242 卷卷啊 26 91 2141 4 16
243 丸子酱就是我 26 62 1150 0 0
244 ZZHoliday 26 49 1594 4 3
245 孤寒者 26 1316 11062 49 136
246 GodOuO 26 13 1012 2 0
247 风后奇门_ 26 40 1189 7 1
248 花狗Fdog 26 6 1007 0 0
249 陆离Luli 26 24 1133 3 0
250 iwin621 26 67 2061 5 47
251 枸杞菊花保温杯 26 65 10438 14 29
252 恬静的小魔龙itMonon 26 88 3459 0 1
253 ChinaManor 26 16 868 3 0
254 我是大明哥 26 72 4752 0 6
255 BraveWang 26 17 1359 4 0
256 undeifined 26 98 3320 3 13
257 晴天同学98202 26 28 1846 3 7
258 jiangxia_1024 26 17 1558 1 3
259 Sunshine 26 4 817 0 0
260 万里顾一程 26 32 1206 0 1
261 种花家的进阶 26 17 1544 0 3
262 天大地大老婆最大 26 69 2472 0 14
263 tomy 26 2 777 5 0
264 Ruovan 26 18 900 2 2
265 java李杨勇 26 1491 59468 54 305
266 苏世_ 26 28 2081 0 4
267 掩埋 26 22 1589 0 2
268 曾是惊鸿照影来 26 10 1185 2 1
269 和雍 26 151 1833 3 8
270 文斌大大鸟 26 56 2393 0 8
271 梦兮林夕 26 53 2146 4 7
272 Jayhaw_花 26 4 978 0 2
273 LBJ 26 1184 24932 76 716
274 明天也要努力 26 65 2546 4 39
275 海绵泡泡 26 19 1342 1 1
276 郎涯技术 26 7 1362 0 1
277 Charonmomo 26 18 1391 0 6
278 荷包蛋卷 26 73 2802 0 21
279 不砍树的光头强 26 4 1165 3 0
280 芒果啊 25 33 1611 0 3
281 王者天才玩家 25 41 859 1 8
282 微观技术 25 30 1659 0 6
283 鸡血园地 25 23 1451 0 6
284 daymove30km 25 64 1621 0 2
285 用户3325470509617 25 180 3044 0 21
286 木讷大叔爱运维 25 10 1814 0 2
287 highsheep 25 3 802 0 0
288 8aceSuper 25 22 1927 3 6
289 小騎士 25 35 1416 0 1
290 写代码的隔壁老王 25 36 1310 0 4
291 团子大圆帅 25 13 1824 0 2
292 狙击手000 25 18 2134 1 7
293 Allenzyg 25 5 829 2 1
294 izsm 25 65 5211 21 31
295 吃鱼的老鼠 25 38 1151 0 1
296 刘Java 25 64 1530 1 31
297 石臻臻的杂货铺 25 324 3257 19 19
298 布小禅 25 121 1583 0 0
299 不如吃茶去 25 68 3094 3 31
300 gw_GW 25 12 862 0 0
301 showsen 25 6 931 0 0
302 秋小秋 25 8 1093 2 1
303 百年孤独html 24 60 2752 47 19
304 SpringSun 24 5 1536 0 2
305 明无生 24 9 1065 0 0
306 PiZriY 24 210 3704 3 16
307 不是Null 24 99 2761 1 13
308 烟沙 24 3 853 0 2
309 Sunny_Chen 24 1027 10546 48 106
310 苏州干饭queen 24 11 1308 6 2
311 我的代码没错 24 21 1179 1 1
312 憨憨学设计 24 82 1419 3 0
313 知鱼君 24 35 1186 5 8
314 milkve 24 4 1205 0 0
315 Husky-Yellow 24 53 2138 5 6
316 开坦克的贝吉塔 24 126 2146 17 9
317 洛茛的技术小铺 24 676 4456 0 4
318 掘金用户007 24 25 5235 0 2
319 splendid 24 12 1094 0 0
320 coolFish 24 29 1862 0 5
321 黄丫丫 24 49 1095 5 1
322 豆干花生 24 395 2599 6 34
323 lovebugs 23 38 1653 0 3
324 ooooooh灰灰 23 55 2290 3 13
325 锐玩道 23 887 15430 31 62
326 倾城之夏 23 24 1104 2 1
327 Mannqo 22 96 2696 14 9
328 程序员小朱 22 10 1338 0 3
329 用户9320192463458 22 7 620 3 0
330 狸不开 22 299 7991 63 115
331 帆影匆匆ig 22 12 1339 0 7
332 LexSaints 22 133 15009 8 59
333 zuozewei 22 33 1554 0 3
334 香菜聊游戏 22 12 1038 0 0
335 Carleslucky 22 6 972 0 3
336 孙不坚1208 21 28 1210 0 1
337 程序员学长 21 59 1590 4 9
338 OpenCoder 21 31 1016 1 0
339 showjoker 21 43 1024 0 1
340 Better 21 67 2419 5 23
341 布客说 21 11 864 0 3
342 Andurils 21 103 4904 5 15
343 Coder的技术之路 21 33 1338 0 4
344 小岛秀夫 21 6 662 0 0
345 吾非同 21 59 3499 1 26
346 程序猿憨憨 21 38 1392 2 7
347 _Battle 21 2053 49563 152 1753
348 凌小可可 20 10 1051 0 0
349 青莲使者 20 99 3863 0 39
350 Style_月月 20 32 4480 2 15
351 banjming 20 17 1289 0 3
352 AllenLMN 20 8 929 0 0
353 兮动人 20 20 1777 2 0
354 马儿得得跑 20 23 1133 0 0
355 edison 20 14 1377 1 2
356 坚定 20 52 1398 0 3
357 小菜羊 20 12 1652 0 1
358 国家级划水运动员 20 10 902 0 1
359 追梦玩家 20 28 2410 5 16
360 Sine21726 20 65 2074 6 13
361 jack_zhou 19 38 868 0 0
362 沸羊羊 19 35 1495 0 3
363 dHy 19 8 1682 0 1
364 前端拌饭 19 12 859 0 0
365 喜洋洋 19 4 575 0 1
366 夜远曦白 19 61 2412 2 9
367 为为为什么 19 3 1147 1 0
368 皮皮克 19 54 1046 2 0
369 公里七 19 69 2577 6 21
370 xiaowangzhixiao 18 2 943 0 1
371 代码搬运媛 18 14 919 0 4
372 西北码农 18 65 1100 2 1
373 平平无奇前端练习生 18 12 1019 0 0
374 AndyLaw 18 32 1643 2 7
375 米洛丶 18 21 1855 0 0
376 Yummyq 18 31 1438 2 1
377 戴沐白 17 33 1642 0 8
378 一个大大大红包 17 6 1719 2 0
379 后山人_ 17 32 1491 1 9
380 Websoft9 17 8 2129 0 1
381 ava鱼 17 235 3526 7 56
382 七月在线的七仔 17 5 968 0 0
383 码中悍刀行 17 5 754 0 1
384 長安 17 81 1462 2 2
385 tntxia 17 7 899 0 1
386 粥里有勺糖 17 65 2256 6 14
387 用户9351632173777 17 2 562 0 0
388 不可食用盐 16 31 1340 0 7
389 CUGGZ 16 1009 15973 43 644
390 奥奥奥 16 27 1066 1 6
391 楼兰java宝藏圈 15 31 723 0 1
392 YDYK 15 4 499 0 0
393 切图老司机 15 24 915 1 1
394 我系小西几呀 15 68 1757 6 13
395 MrYangZCh 15 7 684 0 0
396 vaelcy 15 556 12231 52 357
397 小鑫同学 15 33 1379 1 4
398 Java学术趴 15 46 1166 0 10
399 小七别闹 15 19 955 1 2
400 风回路转 15 19 1096 3 10
401 牛牛_lz 14 68 2908 0 16
402 HealthyZhang 14 137 2626 3 16
403 Beam 14 20 1010 0 2
404 哈麻昂天 14 14 832 2 0
405 zeroone001 14 83 1295 0 2
406 闹了个笑话吧 14 27 1117 0 0
407 Benbinbin 14 35 1548 0 4
408 少年不想说话 14 31 745 0 2
409 twinkle 14 7 816 0 1
410 Ryukie_Lawliet 14 57 7842 10 43
411 _啊呜 14 687 8088 30 189
412 慵懒的凡人大大 14 78 1632 4 1
413 互联网老辛 14 8 745 0 3
414 正在努力中的杨Sir 14 1 330 1 0
415 ClyingDeng 14 391 6118 6 49
416 luzhaopan 14 21 686 0 0
417 北岸冷若冰霜 14 19 1065 0 6
418 心如止水_自在如风 14 9 641 0 0
419 徒手完结 14 74 5363 3 30
420 陈皮的JavaLib 13 33 1086 2 9
421 mytac 13 22 1422 4 7
422 kenx 13 159 1852 3 18
423 星期一研究室 13 409 6466 37 131
424 王中阳Go 13 140 5064 47 11
425 我爱吃桃 13 15 826 1 0
426 执鸢者 13 351 5006 14 101
427 BlackCat 13 7 507 0 0
428 忘我思考 13 22 580 14 2
429 庖丁解牛 13 28 1280 3 3
430 灬柯北 13 12 2674 3 10
431 南吕 13 15 646 0 1
432 拜小白 13 183 5501 22 136
433 饭熟了 13 41 2510 2 4
434 夏狗花 13 26 952 4 9
435 D调的蜀威 12 8 653 0 2
436 岛哥的质量效能笔记 12 29 1954 3 14
437 echo_xiyu 12 4 777 0 0
438 iwhao_top 12 457 4152 28 36
439 weak_PG 12 288 17819 15 16
440 artist 12 482 93241 38 428
441 iOS小文 12 10 5486 0 1
442 ZhaoYun 12 53 1956 5 10
443 jsmask 12 94 4194 26 45
444 窝里乐 12 12 333 0 1
445 悟空聊架构 12 30 941 1 6
446 fuyoufang 12 78 6383 18 33
447 清粥为伴 12 40 1260 4 12
448 Alone 12 26 967 0 1
449 missday 11 1 344 1 0
450 webInRun 11 17 985 5 10
451 三原 11 117 3394 29 31
452 lankongclub 11 32 1174 2 3
453 职业打卡人 11 21 1103 1 0
454 经典鸡翅 11 15 840 0 4
455 麦兜小子 11 58 1341 0 2
456 用户4668938395922 11 1 302 0 0
457 zheaven 11 3 480 0 0
458 幽幽超友善 11 10 835 1 3
459 coder-pig 11 92 5192 22 30
460 不甜可可 11 9 962 1 4
461 是小侯啊 10 12 495 0 0
462 阿粤Ayue 10 38 961 0 3
463 千寻编程 10 15 1233 0 2
464 Mancuoj 10 372 3489 26 66
465 fairyly 10 22 800 2 8
466 xtianyaa 10 23 1400 0 6
467 祈澈菇凉 10 33 1920 11 5
468 安余生大大 10 2 482 0 0
469 瑾行 10 108 1910 4 16
470 我的div丢了肿么办 10 38 1497 3 5
471 路边的小野花你不要采 10 0 281 0 1
472 零道 9 3 400 0 0
473 山水有轻音 9 15 607 1 2
474 蛋蛋蛋973 9 22 429 0 1
475 wjt 9 11 632 0 1
476 卧龙小蛋 9 5 417 0 1
477 逍遥coding 9 86 1534 1 7
478 良良不是Java 9 14 417 0 1
479 禾下月 9 12 344 1 0
480 那些年丶ny 9 283 2593 3 20
481 Focus4149 9 4 480 0 0
482 MoonLight 9 20 783 0 0
483 我为双鱼狂 9 9 444 0 1
484 松鼠不爱松果 9 13 623 0 3
485 程序员麻辣烫 9 16 912 0 3
486 程序员喵大人 9 192 2819 11 29
487 惜鸟 9 12 615 0 2
488 A梦多啦A 9 18 1394 5 11
489 酸菜鱼一号 9 12 772 0 4
490 写Bug的小杜 9 28 777 0 1
491 5加H 9 7 511 0 0
492 Houtaroy 8 11 1315 1 4
493 前端SkyRain 8 7 503 0 0
494 贰拾壹先生 8 11 368 0 6
495 火星飞鸟 8 242 2422 9 52
496 只想要周末双休 8 68 781 12 15
497 橘子味的二条 8 2 461 0 1
498 YK菌 8 18 862 2 2
499 杨慕晚 8 11 510 0 0
500 Simplyme0823 8 13 466 0 1
501 Java升级之路 8 9 593 3 2
502 zero_face 8 60 6626 5 40
503 阿九筒的春天 8 10 475 0 0
504 勿在浮沙築高台 8 134 1165 3 2
505 摸鱼专家 8 576 26418 20 32
506 不知名的小狮子 8 23 927 0 7
507 用户3414427254300 8 4 648 1 0
508 萧杨 8 9 468 1 0
509 心有灵犀920 8 52 1284 5 3
510 Honest1y 8 190 23544 4 43
511 Android帅次 8 223 3948 12 25
512 随便歇一歇 8 5 328 0 0
513 jojo的奇妙前端 8 48 1057 2 8
514 对马弹琴 8 194 2650 18 18
515 life_is_short 8 14 514 0 0
516 纪先生 8 396 6439 25 103
517 头疼脑胀的代码搬运工 8 41 1658 3 14
518 pany 8 126 1847 0 34
519 嗨_Python 8 2 1074 0 1
520 酱豆腐文化宫 8 7 505 1 0
521 撸码社区 8 14 561 0 1
522 肉坨子 8 32 1191 2 14
523 薛定喵的谔 8 10 722 0 0
524 程序员紫菜苔 8 29 1498 0 5
525 似曾_相识 8 10 362 0 0
526 Kimser 8 6 497 2 0
527 右晓晓 8 7 868 0 7
528 秃头哥编程 8 8 485 0 2
529 iRoot 8 4 296 1 0
530 想要飞翔的猪 8 25 850 1 2
531 呦呦鹿鸣丶 8 7 405 0 0
532 AntBlack 8 103 3987 0 9
533 西vvi 7 359 2386 13 24
534 假装懂编程 7 112 11936 47 71
535 ajajaj 7 412 4119 9 53
536 每天微笑一厘米 7 8 622 0 1
537 零狐冲 7 231 1037 7 16
538 前端小轩 7 12 343 2 0
539 Drk 7 8 554 0 1
540 kaliarch 7 28 3899 0 8
541 敲代码的小菜 7 72 1221 3 7
542 安小轩 7 20 937 0 5
543 Fitme 7 10 363 0 0
544 ME学编程甜蜜蜜 7 14 446 5 1
545 程序员徐小白 7 12 882 1 0
546 Ichmag 7 63 1756 3 19
547 fstar 7 31 1063 6 7
548 二毛本尊 7 1 220 1 0
549 rainscloud 7 2 200 0 2
550 一块小砖头儿 7 14 439 3 0
551 HUALEI 7 35 989 4 0
552 可乐爱宅着 7 26 819 8 8
553 WhiteSheep 7 11 1675 0 7
554 敲代码有瘾 7 30 604 4 3
555 pikachues 7 35 938 4 8
556 那根笔 7 26 1467 3 12
557 shengtu_归尘 7 7 617 0 0
558 勇敢的Jerry 7 4 302 1 0
559 Wilson712 7 14 676 0 2
560 叫我阿柒啊 7 37 775 5 4
561 进军的王小二 7 67 1659 8 6
562 shiyuq 7 28 1533 9 11
563 诡异泡泡 7 1 221 0 0
564 玖柒也要敲代码 7 9 208 0 0
565 学不完的前端_卷它 7 25 1109 0 11
566 铅华56 7 12 481 0 2
567 lizhenwen 7 75 1216 5 17
568 YankeeDoodle 7 7 375 0 0
569 用户5704420040657 7 0 202 0 0
570 ihoneys 7 38 1646 7 28
571 loki_ 7 2 280 0 0
572 poo 7 17 922 0 0
573 白鹿第一帅 7 8 313 8 0
574 Fun_ 7 152 762 10 7
575 小阿托 7 13 548 0 8
576 HelloGitHub 7 42 4606 3 20
577 aoho 7 22 2844 1 11
578 源码学徒 7 16 597 1 3
579 范超萌 7 11 409 0 0
580 AndersonHjb 7 45 918 3 0
581 安安安安卓 7 48 2053 15 17
582 名字刚好七个字 7 14 640 0 0
583 love_eagle 7 4 494 0 2
584 毅航同学 7 16 478 0 0
585 需要坚持的人 7 4 353 0 0
586 机灵鹤 7 3 1225 1 4
587 Gxin 7 15 788 0 0
588 前端学长Joshua 7 6 340 0 0
589 ClariS 7 19 724 0 2
590 游走走2021 7 18 419 1 2
591 嘉欣Cansiny 7 30 717 0 15
592 工匠若水 7 40 1737 2 18
593 十里青山 7 10 892 1 5
594 技术四毛喵 7 55 1815 3 3
595 六脉神剑 7 20 1250 2 9
596 超级爽朗的郑 7 13 593 1 2
597 vvmily 7 13 445 0 0
598 沃明 7 12 555 2 1
599 vike123 7 89 882 5 7
600 情非得已小猿猿 7 15 647 0 5
601 chankay 7 1 390 0 0
602 Android开发编程 7 42 1992 2 17
603 程序员秃头之路 7 12 715 0 0
604 Ctrl走天下 7 7 451 0 1
605 吴敬悦 7 31 2048 5 18
606 言淦 7 15 1200 0 3
607 迷恋着你微笑的人zz 7 256 3503 5 31
608 Ned 7 102 1100 9 7
609 萧卓 7 41 1068 3 2
610 鲸落_ 7 31 519 0 0
611 程序猿阿朗 6 16 797 0 5
612 细雨轻言 6 3 300 0 0
613 Rudy248958 6 23 444 0 6
614 LuckyRui 6 2 559 0 0
615 陈小夫子 6 7 804 1 3
616 Java中文社群 6 86 5134 9 66
617 旺仔小小馒头 6 2 348 0 0
618 奥利瓦 6 11 388 0 0
619 码克吐温 6 18 1153 0 7
620 徐四 6 5 348 0 0
621 Rio_Kwok 6 9 458 0 0
622 六七十三 6 2 522 2 2
623 Hydra 6 13 909 0 8
624 当诺 6 3 223 0 0
625 不失者 6 20 450 0 0
626 Happyileaf 6 13 552 0 4
627 大屁登 6 3 222 2 1
628 JonesYong 6 13 1111 1 4
629 小木笔记 6 9 345 0 0
630 月光也会跟着我 6 34 953 0 15
631 掘金安东尼 6 127 3517 17 44
632 我取_ 6 4 346 0 2
633 勤任风 6 17 306 6 7
634 season_zhu 6 115 2981 7 23
635 亦黑迷失 6 26 1093 8 7
636 一尾流莺 6 421 12040 62 238
637 WinterChenS 6 13 928 5 8
638 掘黄金的打工人 6 3 498 0 0
639 fundroid 6 157 8111 41 55
640 rimwood 6 2 243 1 0
641 Mr_Carl 6 16 767 2 11
642 前端小菜鸡之菜鸡互啄 6 69 2601 11 40
643 忆想不到的晖 6 31 822 2 6
644 lanrain 6 3 336 0 1
645 陈思煜 6 4 313 0 1
646 奔波儿灞取经 6 80 3837 20 20
647 土家肸哥 6 29 1756 1 27
648 Holland_ 6 24 740 2 6
649 情系半生e 6 4 313 2 1
650 AaronOhOhOh 6 27 1147 3 5
651 狗子你学废了吗 6 15 462 0 0
652 木子又又又又 6 23 933 2 1
653 JulyYu 6 6 683 0 2
654 Victor 6 13 653 0 5
655 土圭垚墝 6 2 295 0 0
656 yangxy 6 22 508 3 2
657 路人甲丶 6 7 426 0 0
658 cpp加油站 5 18 355 0 0
659 Aaron_hj 5 45 1025 4 6
660 西红柿蛋炒饭 5 94 2935 59 25
661 于五五 5 21 660 4 11
662 定栓 5 10 409 0 1
663 Fly 5 465 15687 152 277
664 Learn1993 5 0 181 0 0
665 LTAND 5 1 182 0 0
666 七七和可莉 5 3 346 0 0
667 Curly_Brackets 5 7 426 0 0
668 梧桐叶子 5 76 1596 4 4
669 fanyee^_^ 5 6 379 0 8
670 蜗牛互联网 5 0 289 0 0
671 圊妖 5 39 1469 2 19
672 梅长酥 5 0 237 0 0
673 Devil 5 6 255 0 0
674 RainyJiang 5 15 1156 5 12
675 易科 5 7 502 0 3
676 唯依绾青丝 5 2 277 0 0
677 Mumbler 5 8 451 0 2
678 luochen123 5 11 1051 2 1
679 亦雨 5 5 563 1 1
680 爱吃肉的阿喆 5 3 523 0 0
681 给小叶倒茶 5 6 350 0 1
682 村雨遥 5 24 1246 4 4
683 三丶斤 5 8 231 0 1
684 CoderStar 5 22 1134 5 17
685 鱼穷千里目 5 10 290 2 0
686 littleLuck 5 14 487 0 2
687 面屏思过 5 49 938 1 6
688 多多益善 5 2 133 0 0
689 静心Study 5 11 694 0 6
690 时光足迹 5 28 1674 7 5
691 茶无味的一天 5 15 810 4 9
692 L我是小学生 5 15 2569 0 0
693 NoBugBoy 5 30 1218 0 12
694 用户2412013885711 5 3 462 2 5
695 viruss 5 11 385 0 0
696 Moons 5 13 318 0 0
697 前端周同学 5 54 2694 15 26
698 yongxinz 5 26 1637 2 25
699 陈序猿_Android 5 78 2182 29 19
700 顶级饮水机管理员 5 44 1731 0 28
701 小粥粥出击 5 7 354 2 1
702 老码农Stephen 5 5 582 0 1
703 寇寇菌 5 27 728 0 4
704 人der_了 4 1 246 0 0
705 沙蒿同学 4 6 381 0 1
706 fcl999 4 1 200 0 0
707 A boy。 4 42 875 0 8
708 Vam的金豆之路 4 12 472 0 7
709 金色小芝麻 4 31 2222 10 7
710 你呀不牛 4 23 819 0 0
711 糖葫芦qq 4 29 1520 0 14
712 前端工兵 4 15 235 4 0
713 HideonBush 4 15 520 7 0
714 Aisanyi 4 9 572 0 1
715 Recluse 4 4 173 0 0
716 七月的云 4 3 111 0 1
717 Shenfq 4 296 13145 85 225
718 double倩 4 6 393 0 0
719 丸子啦 4 6 332 0 1
720 荣顶 4 886 32767 213 819
721 不停同学 4 42 1374 0 10
722 LoveqLRC 4 1 209 0 1
723 水天滑稽天照八野滑稽 4 3 348 0 4
724 Starfish 4 1 210 0 1
725 白房子 4 6 369 0 1
726 迷失技术de小猪 4 4 182 1 0
727 Ijiran 4 40 2209 3 31
728 用户6704321759999 4 7 572 0 4
729 小R算法 4 4 222 3 0
730 ChuckGao 4 12 293 5 0
731 云胡不喜คิดถึง 4 1 183 0 2
732 dreamer_sen 4 43 968 1 6
733 哈罗哈皮 4 65 1584 11 26
734 徐小冠 4 5 255 0 1
735 4ye酱 3 95 2555 4 52
736 墨安 3 2 166 0 0
737 无量空处 3 42 602 4 3
738 gds2333 3 2 160 2 0
739 红尘灬客栈 3 3 118 1 0
740 code秦潘 3 6 608 0 0
741 洛明xml 3 4 427 0 0
742 VeggieOrz 3 5 425 0 3
743 codeMan 3 10 385 0 0
744 Throwable 3 24 555 2 2
745 JexLau 3 2 278 0 2
746 爱学习的小余 3 2 162 0 0
747 亦一 3 13 223 0 1
748 前端张小白 3 6 203 0 0
749 MouthMouth 3 2 149 0 0
750 一棵栾树 3 0 93 0 0
751 小Bob来啦 3 5 310 2 0
752 烂笔濤 3 3 143 0 0
753 写给李华的信 3 0 73 0 0
754 晓色暮云 3 1 113 0 0
755 程序猿的flag 3 3 224 0 1
756 JacobHuang 3 2 148 2 0
757 ZhangYiFan 3 13 495 0 6
758 梦落成空 3 25 743 0 5
759 耳东蜗牛 3 25 940 5 11
760 Lucelia 3 1 347 0 1
761 淹死在鱼塘的程序猿 3 45 669 0 2
762 小梓川呀 3 24 819 2 11
763 Android清风 3 19 829 6 13
764 古柳_Deserts_X 3 9 353 0 1
765 A滑稽诺夫 3 0 219 0 1
766 Hlianfa 3 32 442 0 1
767 清汤饺子 3 152 10880 48 223
768 起小就些熊 2 22 1093 2 4
769 一天一语言 2 13 360 4 0
770 卖坚果的怪叔叔 2 17 447 2 9
771 蒋老湿 2 4 516 0 0
772 雾灵 2 20 430 2 4
773 星始流年 2 7 335 1 3
774 付十一 2 216 9697 60 120
775 Daxian1996 2 3 116 0 1
776 幸存者drifter 2 4 286 0 4
777 呆呆敲代码的小Y 2 87 989 5 17
778 不能停止进步 2 1 59 0 0
779 rookie_only 2 2 192 0 2
780 大猛 2 3 95 0 0
781 孟饭饭 2 5 190 0 0
782 无痕不想说话32338 2 8 627 4 2
783 慕枫技术笔记 2 0 110 0 0
784 007号前端切图师 2 7 385 0 0
785 可爱子 2 8 236 0 3
786 越走越远的风 2 7 258 0 5
787 火星上的飞猫 2 3 131 0 0
788 liweiwei1419 2 0 72 0 0
789 紫苏子 2 7 132 0 0
790 Lee淳淳同学 2 7 477 0 3
791 Airoure 2 1 314 2 0
792 随便逛逛不说话 2 5 167 0 0
793 shawntime 2 2 125 0 0
794 山间小僧 2 7 267 0 0
795 得道的小野猪 2 6 251 0 0
796 写代码的男生最帅 2 14 378 0 3
797 DevUI团队 2 65 3829 8 46
798 Jsnewbie 2 7 204 0 0
799 漫漫Coding路 2 3 125 0 1
800 Jereau 2 2 181 0 0
801 IT影子 2 0 90 0 1
802 oldUath 2 2 415 0 1
803 knowjs 2 7 392 0 3
804 李存志 2 6 106 0 0
805 zobol 2 0 117 0 0
806 enjoygayhub 2 4 172 0 0
807 前端_逗叔 2 11 230 0 5
808 世界级划水大师 2 2 108 0 1
809 聪聪ccoder 2 2 32 0 0
810 zxhtom 2 69 3809 15 14
811 Bella的技术轮子 2 5 210 0 0
812 低开 2 1 97 0 0
813 前端自学驿站 2 105 8742 43 81
814 FutureDreamer 2 9 322 0 2
815 xiaosongxiaosong 2 9 281 0 1
816 跳动的字节 2 12 290 3 2
817 展程 2 46 711 7 4
818 摸鱼上尉大白酱 2 9 165 0 0
819 clown 2 0 51 0 0
820 mirai 2 6 463 0 1
821 MuffinMan23673 2 9 305 2 4
822 游戈程序员 2 9 193 0 1
823 清香苦茶 2 5 331 0 2
824 一个装睡的人 2 1 97 0 0
825 KKComeOn 2 2 131 0 0
826 村上小树 2 18 278 0 0
827 Thezero 2 2 122 0 0
828 无限循环无限 2 5 224 0 1
829 Try to do 1 10 2705 2 10
830 汪和呆喵 1 0 64 0 0
831 Nayuta 1 374 14619 103 289
832 Daisyzzzz 1 1 82 0 0
833 政采云前端团队 1 6 1563 0 0
834 aliacha 1 3 146 1 0
835 一天清晨 1 2 98 0 0
836 Larry萝卜 1 0 136 0 0
837 法医 1 16 913 0 4
838 Ivy_Lin 1 1 27 0 0
839 赵小川 1 0 106 0 1
840 圈a最怕空气突然安静 1 5 242 2 1
841 北极光之夜 1 3 59 1 0
842 阿花和猫 1 8 216 1 4
843 杰出D 1 19 894 1 15
844 什么冬梅 1 3 106 0 0
845 nikolausliu 1 1 103 0 0
846 TomCatalina 1 0 49 0 0
847 好冷啊 1 4 288 0 0
848 f_arkadia 1 3 230 0 1
849 用户8801284604281 1 0 94 0 0
850 kylin冰麒麟 1 4 76 0 0
851 突突突突突 1 4 140 0 0
852 Monetchang 1 1 53 0 0
853 summer01 1 0 63 0 0
854 洛小豆 1 1 84 0 0
855 前端phil 1 1 34 0 0
856 捡代码的小女孩 1 2 36 0 0
857 番茄先森 1 21 739 0 34
858 阳光是sunny 1 11 354 0 7
859 _wangyibo 1 3 228 0 0
860 suming 1 8 377 0 6
861 JavaGieGie 1 27 1164 1 3
862 宝山金城武 1 0 85 0 0
863 繁星air 1 1 87 0 0
864 对半 1 2 70 0 0
865 叶梅树 1 5 512 0 2
866 夏末_阳光依然耀眼 1 3 106 0 0
867 恪晨 1 6 322 6 2
868 eguid_1 1 0 60 0 0
869 丁大忽悠 1 4 101 0 0
870 修之竹子 1 2 116 0 0
871 insomnia247 1 1 138 0 0
872 Yjx46231 1 1 68 0 1
873 雪月 1 37 964 4 32
874 锐角转向 1 3 117 0 1
875 Youlik 1 0 55 0 0
876 happlyfox 1 0 287 0 0
877 颜青 1 2 146 0 0
878 阿里智能营销平台前端 1 0 48 0 0
879 zz 1 11 198 2 4
880 草莓千层蛋糕 1 2 59 0 0
881 ice_bear 1 2 49 0 0
882 DK无畏 1 11 302 3 6
883 寒草 1 37 705 10 9
884 isysc1 1 1 79 0 0
885 out_m 1 1 155 1 0
886 milimsung 1 0 68 0 0
887 彭丑丑 1 45 1992 5 36
888 qwer 💫 1 0 34 0 0
889 Amazing_YI 1 1 27 0 0
890 我是970 1 5 154 0 0
891 小猪皮皮呆 1 2 58 0 0
892 代码只写一行半 1 27 684 0 28
893 格雷福斯 1 0 106 0 0
894 CVNot 1 4 242 0 2
895 头痛的可达鸭Z 1 3 66 0 0
896 减法小学生 1 2 35 0 0
897 一杯茶一支烟一个bug改一天 1 3 209 1 2
898 YongCode 1 2 166 3 0
899 掘金酱 1 155 33690 513 34
900 wbh爱吃西瓜 1 1 80 1 0
901 橙某人 1 12 265 1 6
902 JasonYin 1 5 78 0 0
903 Jasmine989 1 4 165 0 0
904 JackWu 1 0 90 0 0
905 羡鱼戏渊 0 0 0 0 0
906 神奇的程序员 0 0 0 0 0
907 望道同学 0 0 0 0 0
908 0 0 0 0 0
909 HaiJun 0 0 0 0 0

本文转载自: 掘金

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

SpringMVC源码解析

发表于 2021-08-01

SpringMVC源码解析

Spring Web MVC是基于Servlet API构建的原始Web框架,从一开始就包含在Spring框架中。
其正式名称“Spring Web MVC”来自它的源模块(Spring -webmvc)的名称,但它更常见的名称是“Spring MVC”。
本节介绍Spring Web MVC。

(1)servlet3.0新特性
  • 在这里插入图片描述

通过上图可以知道,servlet3.0给我们提供了一个非常牛逼的规范,只要我们按照这个规范,我们就能在tomcat启动的时候去掉web.xml,而且还能初始化spring环境。

  1. 定义了一个新的规范,即在资源文件的META-INF/services文件夹下面,有一个以javax.servlet.ServletContainerInitializer命名的文件,里面定义一个你自己的类的全类名。同时该类实现javax.servlet.ServletContainerInitializer接口,并重写onStartup方法
  2. 在上面这个规范下,所有按照这个规范的servlet服务器,例如:tomcat,在服务启动的时候,会自己反射执行这个类的onStartup()方法
  3. 通过这个新的规范,我们就不需要按照传统的方法,需要在web.xml文件中,初始化spring等配置和环境,这样做就能实现零配置,springboot就是按照这个思想实现零配置的。
(2)模拟SpringBoot零配置,内嵌tomcat
  • 在这里插入图片描述

通过上图,可以看到模拟springboot零配置和内嵌tomcat,主要要注意几点:

  1. tomcat.addContext和tomcat.addWebapp的区别:

(1) addWebapp表示该项目是一个web项目,tomcat启动的时候,就会默认去加载jsp视图解析器,然后没有添加jsp视图解析器的依赖,就会报错。
(2) addContext表示的仅仅是往tomcat的webapps目录添加一个context,tomcat不会加载jsp视图解析器,也就不会报找不到jsp视图解析器依赖的错了。
(3) springboot基本上是已经默认不再使用jsp技术,例如:thymeleaf,freemarker…等,所以springboot的底层,肯定不会使用addWebapp这个方法。

  1. 这里不使用上面提到的servlet3.0新特性的规范,主要实现WebApplicationInitializer这个接口,tomcat启动要执行到这个类的代码,一定要在web项目的情况下,才会执行到。即在使用addContext这个方式的情况下,是不会执行到我们的类。那么这里的唯一做法:只能在一个软方法里面,同时初始化spring ioc,spring mvc和tomcat等环境。

在开始spring mvc的源码解析之前,我们先要有这样的一个概念:

  1. 通过浏览器发起的一个请求,只能请求到一个servlet的方法,它是无法直接请求到一个java类的某个方法,也就是我们经常使用的controller类的方法。
  2. 那么spring mvc框架可以让一个请求,执行到对应的controller类的某个方法,肯定是先让一个请求去到servlet,然后这个servlet再调用到我们的controller类的某个方法。

request —-> ! servlet.class(这是不可能实现的)
request —-> servlet —-> controller(只能是方法调用,不然无法实现)(方法调用,底层一定是反射技术:indexController:index())

  1. DispatcherServlet这个servlet,就是spring mvc的核心类。
(3)Spring MVC源码解析
  • 先上图:SpringMVC核心流程图
  • 在这里插入图片描述

总结:
(1) 首先请求进入DispatcherServlet 由DispatcherServlet 从HandlerMappings中提取对应的Handler。
(2) 此时只是获取到了对应的Handle,然后得去寻找对应的适配器,即:HandlerAdapter。
(3) 拿到对应HandlerAdapter时,这时候开始调用对应的Handler处理业务逻辑了。
(这时候实际上已经执行完了我们的Controller) 执行完成之后返回一个ModeAndView
(4) 这时候交给我们的ViewResolver通过视图名称查找出对应的视图然后返回。
(5) 最后 渲染视图 返回渲染后的视图 –>响应请求。

3.1Spring MVC初始化阶段

我们要从哪里入手呢?
通过上面的分析,我们知道spring mvc的核心类是DispatcherServlet,这是一个servlet类,那么看这个类,就从这个类的init方法开始。

  • DispatcherServlet类的结构图
  • 在这里插入图片描述
  • 查看DispatcherServlet类,没有看法init的方法,那么就只能找这个类的父类。
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
java复制代码# 1.执行父类HttpServletBean的init()方法
//tomcat启动,就会执行该方法,初始化DispatcherServlet
@Override
public final void init() throws ServletException {
// Set bean properties from init parameters.
PropertyValues pvs = new ServletConfigPropertyValues(getServletConfig(), this.requiredProperties);
if (!pvs.isEmpty()) {
try {
BeanWrapper bw = PropertyAccessorFactory.forBeanPropertyAccess(this);
ResourceLoader resourceLoader = new ServletContextResourceLoader(getServletContext());
bw.registerCustomEditor(Resource.class, new ResourceEditor(resourceLoader, getEnvironment()));
initBeanWrapper(bw);
bw.setPropertyValues(pvs, true);
}
catch (BeansException ex) {
throw ex;
}
}
//初始化web环境(重要)
initServletBean();
}

# 2.执行子类FrameworkServlet类的initServletBean()方法
@Override
protected final void initServletBean() throws ServletException {
getServletContext().log("Initializing Spring FrameworkServlet '" + getServletName() + "'");
long startTime = System.currentTimeMillis();
try {
//初始化web环境
this.webApplicationContext = initWebApplicationContext();
initFrameworkServlet();
}
catch (ServletException | RuntimeException ex) {
throw ex;
}
}

# 3.执行FrameworkServlet类的initWebApplicationContext()方法
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;

if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
if (cwac.getParent() == null) {
cwac.setParent(rootContext);
}
//配置和刷新spring容器(重要)
//这个无非就是初始化spring ioc的环境,创建bean和实例化bean等操作
//这个方法最终也是调用refresh()方法,已在spring源码解析中解析过了
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
wac = findWebApplicationContext();
}
if (wac == null) {
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
//初始化DispatcherServlet的配置initStrategies() (重点)
onRefresh(wac);
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}

# 4.执行DispatcherServlet类的onRefresh()方法,初始化springmvc的配置
protected void onRefresh(ApplicationContext context) {
//初始化springmvc的配置
initStrategies(context);
}

(1)通过上面代码分析,可以得到在执行DispatcherServlet的init方法,会执行父类的HttpServletBean的init方法,然后调用了FrameworkServlet的initServletBean()方法。
HttpServletBean#init() —> FrameworkServlet#initServletBean()
(2)执行initWebApplicationContext()方法,就是对spring ioc环境的初始化。那么这里就衍生出了一个面试题:spring容器和spring mvc的容器的区别?通过源码的分析,spring和spring mvc底层,都是调用了同一个refresh()方法,所以spring容器和spring mvc容器是没有区别的,都是指的是同一个容器。
(3)执行到onRefresh()方法,就是开始初始化DispatcherServlet了,也就是开始初始化spring mvc。

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
java复制代码# 1.执行DispatcherServlet类的initStrategies()方法
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);//上传文件
initLocaleResolver(context);//国际化
initThemeResolver(context);//前段的主题样式
initHandlerMappings(context);//初始化HandlerMappings(请求映射器)重点
initHandlerAdapters(context);//初始化HandlerAdapters(处理适配器)
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);//视图转换器
initFlashMapManager(context);//重定向数据管理器
}

# 2.执行initHandlerMappings()方法
private void initHandlerMappings(ApplicationContext context) {
this.handlerMappings = null;
if (this.detectAllHandlerMappings) {
// Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
Map<String, HandlerMapping> matchingBeans =
BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
if (!matchingBeans.isEmpty()) {
this.handlerMappings = new ArrayList<>(matchingBeans.values());
AnnotationAwareOrderComparator.sort(this.handlerMappings);
}
}
else {
try {
HandlerMapping hm = context.getBean(HANDLER_MAPPING_BEAN_NAME, HandlerMapping.class);
this.handlerMappings = Collections.singletonList(hm);
}
}
//通过配置文件中的配置信息,得到handlerMappings
if (this.handlerMappings == null) {
//使用defaultStrategies获取数据
this.handlerMappings = getDefaultStrategies(context, HandlerMapping.class);
}
}

# 3.执行getDefaultStrategies()方法
protected <T> List<T> getDefaultStrategies(ApplicationContext context, Class<T> strategyInterface) {
String key = strategyInterface.getName();
// defaultStrategies 是DispatcherServlet.properties 配置文件,在static静态代码块初始化
String value = defaultStrategies.getProperty(key);
if (value != null) {
String[] classNames = StringUtils.commaDelimitedListToStringArray(value);
List<T> strategies = new ArrayList<>(classNames.length);
for (String className : classNames) {
try {
// 获取class字节码文件
Class<?> clazz = ClassUtils.forName(className, DispatcherServlet.class.getClassLoader());
// 底层是通过调用spring的getBean的方式创建该对象(可以进行bean的属性装配)
// 请求映射就是在这个方法实现装配的
Object strategy = createDefaultStrategy(context, clazz);
strategies.add((T) strategy);
}
}
return strategies;
}
else {
return new LinkedList<>();
}
}

(1) initHandlerMappings方法,就是初始化我们的handlerMapping(请求映射器)。
(2) handlerMapping的主要作用是,找到请求路径对应的controller的方法。

例如:请求的路径 “/index”,然后这个handlerMapping,在初始化的时候,已经将所有controller的请求路径映射保存在一个map集合,当请求过来的时候,就将”/index”作为一个key,从map集合中找到对应的controller的index方法。

(3) 这里初始化handlerMappings ,默认是有两个handlerMappings ,是直接在defaultStrategies配置文件中获取。
(4) 那么defaultStrategies的值是什么时候初始化的呢?

通过查看源码,defaultStrategies这个值,是DispatcherServlet类的静态代码块初始化的。
全世界都知道,当一个类被初始化的时候,会执行该类的static静态代码块的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码# 1.DispatcherServlet类的static静态代码块
static {
try {
/**
* 从属性文件加载默认策略实现
* 说白了这里的意思就是从DEFAULT_STRATEGIES_PATH这个文件当中拿出所有的配置
* 可以去数一下一共有8个: DispatcherServlet.properties == DEFAULT_STRATEGIES_PATH
*/
ClassPathResource resource = new ClassPathResource(DEFAULT_STRATEGIES_PATH, DispatcherServlet.class);
defaultStrategies = PropertiesLoaderUtils.loadProperties(resource);
}
}

# 2.DispatcherServlet.properties文件
//这里就贴出HandlerMapping和HandlerAdapter的类
org.springframework.web.servlet.HandlerMapping=
org.springframework.web.servlet.handler.BeanNameUrlHandlerMapping,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping

org.springframework.web.servlet.HandlerAdapter=
org.springframework.web.servlet.mvc.HttpRequestHandlerAdapter,\
org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter,\
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter

从DispatcherServlet.properties配置文件,可以看出handlerMapping默认是有两个:
1.BeanNameUrlHandlerMapping (主要处理object)
2.RequestMappingHandlerMapping(主要处理method)

3.2Spring MVC请求阶段分析

用户的一个请求过来,会由servlet接收到,然后一步一步调用到DispatcherServlet的doService方法。

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复制代码# 1.DispatcherServlet类的doService()方法
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
//核心方法(重点)
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
}
}

# 2.调用DispatcherServlet类的doDispatch()方法
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
//异步编程
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
//定义变量
ModelAndView mv = null;
Exception dispatchException = null;
try {
//检查请求中是否有文件上传操作
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);

//确定当前请求的处理程序(重点),推断controller和handler的类型,
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}

//推断适配器,不同的controller类型,交给不同的适配器去处理
//如果是一个bean,mappedHandler.getHandler()返回的是一个对象
//如果是一个method,mappedHandler.getHandler()返回的是一个方法
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
//到这里,spring才确定我要怎么反射调用

//前置拦截器处理
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
//通过适配器,处理请求(可以理解为,反射调用方法)(重点)
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
}

通过对DispatcherServlet的分析,得到请求的核心处理方法是doDispatch(),主要是分了几步:
(1) 检查请求中是否有文件上传操作
(2) 确定当前请求的处理的handler(重点)
(3) 推断适配器,不同的controller类型,交给不同的适配器去处理
(4) 执行前置拦截器处理interceptor
(5) 通过找到的HandlerAdapter ,反射执行相关的业务代码controller的方法。
(6) 返回结果。

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复制代码# 1.DispatcherServlet类的getHandler()方法
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
//循环所有的HandlerMappings
//this.handlerMappings这个是什么时候初始化的?(重点)
//在handlerMappings初始化的时候
for (HandlerMapping hm : this.handlerMappings) {
//把请求传过去看能不能得到一个handler
//注意:怎么得到handler和handlerMapping自己实现的逻辑有关系
HandlerExecutionChain handler = hm.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}

# 2.执行到AbstractHandlerMapping的getHandler()方法
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
//获取handler(重点)
Object handler = getHandlerInternal(request);
if (handler == null) {
handler = getDefaultHandler();
}
if (handler == null) {
return null;
}
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
if (CorsUtils.isCorsRequest(request)) {
CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}

(1) getHandler()方法,主要是遍历在DispatcherServlet初始化是,初始化的handlerMappings。
(2) 这个方法的主要思想是,通过request的路径,去匹配对应的controller去处理。
(3) SpringMVC自己自带了2个HandlerMapping 来供我们选择 至于 为什么要有2个呢?

  • 我们用2种方式来注册Controller 分别是:
  • (1) 作为Bean的形式:实现Controller接口,重写handleRequest方法,请求路径为”/test”
1
2
3
4
5
6
7
8
9
java复制代码@Component("/test")
public class TesrController implements org.springframework.web.servlet.mvc.Controller{
@Override
public ModelAndView handleRequest(HttpServletRequest request,
HttpServletResponse response) throws Exception {
System.out.println("1");
return null;
}
}
  • (2) 以Annotation形式:
1
2
3
4
5
6
7
8
java复制代码@Controller
public class AnnotationController {
@RequestMapping("/test2")
public Object test(){
System.out.println("test");
return null;
}
}

(1) 经过测试,可以得到以Bean方式的controller,是通过BeanNameUrlHandlerMapping去匹配
(2)以注解方法的controller,是通过RequestMappingHandlerMapping去匹配

  • BeanNameUrlHandlerMapping处理bean方式的源码分析:
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复制代码# 1.执行到AbstractUrlHandlerMapping的getHandlerInternal()方法
protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
//获取请求的路径
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
//找到对应的handler(重点)
Object handler = lookupHandler(lookupPath, request);
if (handler == null) {
Object rawHandler = null;
if ("/".equals(lookupPath)) {
rawHandler = getRootHandler();
}
if (rawHandler == null) {
rawHandler = getDefaultHandler();
}
if (rawHandler != null) {
// Bean name or resolved handler?
if (rawHandler instanceof String) {
String handlerName = (String) rawHandler;
rawHandler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(rawHandler, request);
handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null);
}
}
return handler;
}

# 2.执行到AbstractUrlHandlerMapping的lookupHandler()方法
protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
//通过请求的路径,在handlerMap中去匹配。
//handlerMap这个值,什么时候填充值?在init初始化的时候,就已经存放在这个handlerMap种
Object handler = this.handlerMap.get(urlPath);
if (handler != null) {
// Bean name or resolved handler?
if (handler instanceof String) {
String handlerName = (String) handler;
handler = obtainApplicationContext().getBean(handlerName);
}
validateHandler(handler, request);
return buildPathExposingHandler(handler, urlPath, urlPath, null);
}
....忽略....
}

(1) 以Bean方式的controller,匹配请求的路径,是通过一个handlerMap去匹配,比较简单。
(2) 这里的问题是,这个handlerMap的值,是什么时候放进去的?

通过源码分析,BeanNameUrlHandlerMapping是实现了ApplicationContextAware接口。
如果你精通spring的源码,就知道spring的实例bean的时候,会回调这些类的setApplicationContext()方法。

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复制代码# 1.执行父类的ApplicationObjectSupport的setApplicationContext()方法
public final void setApplicationContext(@Nullable ApplicationContext context) throws BeansException {
if (context == null && !isContextRequired()) {
// Reset internal context state.
this.applicationContext = null;
this.messageSourceAccessor = null;
}
else if (this.applicationContext == null) {
this.applicationContext = context;
this.messageSourceAccessor = new MessageSourceAccessor(context);
//初始化ApplicationContext,就会执行到子类的方法(重点)
initApplicationContext(context);
}
}

# 2.执行到AbstractDetectingUrlHandlerMapping类的initApplicationContext()方法
@Override
public void initApplicationContext() throws ApplicationContextException {
super.initApplicationContext();
// 检测出handler
detectHandlers();
}

# 3.执行到AbstractDetectingUrlHandlerMapping类的detectHandlers()方法
protected void detectHandlers() throws BeansException {
ApplicationContext applicationContext = obtainApplicationContext();
//获取spring ioc所有的beanName,然后判断beanName,那些是以 "/" 开头
String[] beanNames = (this.detectHandlersInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(applicationContext, Object.class) :
applicationContext.getBeanNamesForType(Object.class));
for (String beanName : beanNames) {
//然后判断beanName,那些是以 "/" 开头
String[] urls = determineUrlsForHandler(beanName);
if (!ObjectUtils.isEmpty(urls)) {
//注册handler(重点)
registerHandler(urls, beanName);
}
}
}

# 4.执行到AbstractUrlHandlerMapping的registerHandler()方法
protected void registerHandler(String[] urlPaths, String beanName) throws BeansException, IllegalStateException {
Assert.notNull(urlPaths, "URL path array must not be null");
for (String urlPath : urlPaths) {
registerHandler(urlPath, beanName);
}
}

# 5.AbstractUrlHandlerMapping的registerHandler()方法
protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException {
Object resolvedHandler = handler;
//最终put到map集合中(省略其他无关代码)
this.handlerMap.put(urlPath, resolvedHandler);
}

BeanNameUrlHandlerMapping处理bean方式的源码分析,其实是很简单:
(1) 在类初始化的时候,就已经将所有实现了Controller接口的controller类,拿到他们的@Componet(‘/test’)
(2) 然后将’/test’这个作为key,controller类作为value,放入到一个map集合。
(3) 当一个请求过来的时候,拿到这个请求的uri,在map里面找,找到了即表示匹配上

  • RequestMappingHandlerMapping处理注解方式的源码分析:
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
java复制代码# 1.AbstractHandlerMethodMapping#getHandlerInternal
// 对于RequestMappingHandlerMapping,indexController.index(),方法的请求路径映射
@Override
protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
//获取请求路径
String lookupPath = getUrlPathHelper().getLookupPathForRequest(request);
this.mappingRegistry.acquireReadLock();
try {
//通过请求路径,获取handler
HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request);
return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null);
}
finally {
this.mappingRegistry.releaseReadLock();
}
}

# 2.AbstractHandlerMethodMapping#lookupHandlerMethod
protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
List<Match> matches = new ArrayList<>();
//从mappingRegistry的urlLookup,匹配请求路径
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()) {
Comparator<Match> comparator = new MatchComparator(getMappingComparator(request));
matches.sort(comparator);
if (logger.isTraceEnabled()) {
logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches);
}
Match bestMatch = matches.get(0);
if (matches.size() > 1) {
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();
throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" +
request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
}
}
handleMatch(bestMatch.mapping, lookupPath, request);
//返回handler
return bestMatch.handlerMethod;
}
else {
return handleNoMatch(this.mappingRegistry.getMappings().keySet(), lookupPath, request);
}
}

# 3.AbstractHandlerMethodMapping.MappingRegistry#getMappingsByUrl
public List<T> getMappingsByUrl(String urlPath) {
return this.urlLookup.get(urlPath);
}

RequestMappingHandlerMapping处理注解方式的源码分析,比较复杂,用一个MappingRegistry维护所有的请求路径映射。
MappingRegistry的初始化,也是在该bean实例化的时候,就已经做好的了。
原理也是和上一个差不多,都是从一个map集合里面匹配。所以这里就不再做解析了。

总结:getHandler()

  • 接下来到找Apapter适配器了
1
2
3
4
5
6
7
8
9
10
11
12
java复制代码protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException {
if (this.handlerAdapters != null) {
for (HandlerAdapter ha : this.handlerAdapters) {
if (logger.isTraceEnabled()) {
logger.trace("Testing handler adapter [" + ha + "]");
}
if (ha.supports(handler)) {
return ha;
}
}
}
}

其实能看见他是从一个handlerAdapters属性里面遍历了我们的适配器 这个handlerAdapters哪来的呢? 跟我们的HandlerMappings一样 在他的配置文件里面有写,就是我们刚刚所说的 。

至于什么是适配器,我们结合Handler来讲, 就如我们在最开始的总结时所说的, 一开始只是找到了Handler 现在要执行了,但是有个问题,Handler不止一个, 自然而然对应的执行方式就不同了, 这时候适配器的概念就出来了:对应不同的Handler的执行方案。当找到合适的适配器的时候, 基本上就已经收尾了,因为后面在做了一些判断之后(判断请求类型之类的),就开始执行了你的Handler了,上代码:

1
java复制代码mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

这个mv就是我们的ModlAndView 其实执行完这一行 我们的Controller的逻辑已经执行完了, 剩下的就是寻找视图 渲染图的事情了。

总结:
其实我们的SpringMVC关键的概念就在于Handler(处理器) 和Adapter(适配器)
通过一个关键的HandlerMappings 找到合适处理你的Controller的Handler
然后再通过HandlerAdapters找到一个合适的HandlerAdapter 来执行Handler即Controller里面的逻辑。
最后再返回ModlAndView…

总的来说,springmvc的源码,还是很复杂,本博客只是大概的描述了主要的执行流程。
源码注释下载地址:github.com/llsydn/spri…

本文转载自: 掘金

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

SQL注入笔记记录+MySQL的事务隔离级别 (一)SQL注

发表于 2021-08-01

(一)SQL注入。

1.如何理解SQL注入?

SQL注入是一种将SQL代码添加到输入参数中,传递到SQL服务器解析并执行的一种攻击手法。

2.SQL注入是如何产生的?

web开发人员无法保证所有的输入都已经过滤。

攻击者利用发送给SQL服务器的输入数据构造可执行的SQL代码

数据库未做相应的安全配置。

3.如何寻找SQL注入漏洞?

借助逻辑推理:

识别web应用中所有输入点。

了解哪些类型的请求会触发异常。(get特殊字符‘ “)

检测服务器响应中的异常。

4.如何进行SQL注入攻击?

数字注入。

(1)select * from name where id=-1 OR 1=1; 这样就会查询全表。

字符串注入。

(1)SQL,#后面会被注释掉的

select * from name= ‘llsydn’#’ and password = “123456”;

(2)SQL,–后面会被注释掉的

select * from name= ‘llsydn’–’ and password = “123456”;

5.如何预防SQL注入?

严格检查输入变量的类型和格式。

过滤和转义特殊字符。

利用mysql的预编译机制。

(二)MySQL隔离级别

1.MySQL事务隔离级别。

SERIALIZABLE; REPEATABLE READ; READ COMITTED; READ UNCOMMITTED

序列化; 可重复读; 提交读; 未提交读

(1)serializable,最高级别,当别操作未提交时,不能操作数据库。

(2)repeatable read,可重复读。(默认的隔离级别)

出现幻读的情况,A事务插入数据insert,并提交commit。B事务查询select,并更新update数据的时候,并提交commit,B查询会出现幻读,即会显示A事务插入的数据。

(3)read committed,提交读。A事务更新update数据,并提交commit。B事务中都可以读取到被更新的数据。(即出现了不可重复读)

(4)read uncommitted,未提交读。A事务更新update数据,未提交。B事务中都可以读取到被更新的数据。(A事务,回滚rollback了,则会出现脏读)

2.MySQL性能与非事务表的表锁定。

支持事务的数据库在保持不同用户彼此隔离方面要比非事务数据库复杂,因此自然

反映在系统的性能上面。

在使用事务表时,提高性能的一些方法:

(1)使用小事务。

(2)选择合适的隔离级别。

(3)保证开始事务前一切都是可行的。

(4)避免死锁。

本文转载自: 掘金

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

Mysql数据库设计规范

发表于 2021-08-01

1. 数据库命名规范

  1. 采用单词+下划线’_‘组成
  2. 命名简洁明确(长度不能超过30个字符)
  3. 例如:user,stat,log,也可以wifi_user, wifi_stat,wifi_log 给数据库加个前缀
  4. 除非是备份数据库可以加0-9的自然数

2. 数据库表名命名规范

  1. 采用26 个英文字母(区分大小写)和0-9 的自然数(经常不需要)加上下划线’_‘组成
  2. 命名简洁明确,多个单词用下划线’_‘分隔。

3. 数据库表字段命名规范

  1. 单词+下划线组成
  2. 例: user_login, user_id
  3. 每个表中必须有自增主键,add_time(默认系统时间)
  4. 表与表之间的相关联字段名称要求尽可能的相同

4. 数据库表字段类型规范

用尽量少的存储空间来存储一个字段的数据

能用int就不用varchar,char,能用varchar(10)就不用varchar(255)

IP地址最好用INT类型

固定长度的类型最好使用char,例如:邮编

能使用tinyint就不要使用smallint, int

最好每个字段一个默认值,最好不能为Null

5. 数据库表索引规范

命名简洁明确,例如:user_login表,user_name字段的索引应为user_name_index唯一索引

为每个表创建一个主键索引

为每个表创建合理的索引

建立复合索引请慎重

6. 数据库范式

  1. 第一范式(1NF) :字段值具有原子性,不能再分(所有关系型数据库系统都满足第一范式)
* 例如:姓名字段,其中姓和名是一个整体,如果区分姓和名那就必须设立两个字段
  1. 第二范式(2NF):一个表必须有主键,即每行数据都能被唯一区分
* 备注:必须先满足第一范式
  1. 第三范式:一个表中不能包含其他相关表中非关键字段的信息,即表数据中不能用冗余字段
* 备注:必须先满足第二范式
* 一般不需要完全遵循第三范式,因为过度的工程化会导致项目过度细化而影响系统整体性能
* 例如:老师和学生都有账号,密码等,如果把老师账号和学生账号冗余在其信息表内,而学生账号放在学生信息表内,其实没有影响其整体的存储,但这样的效率省了大量的join操作,提升了性能,(就算拆成几张表也无法节省内存,也只会占用更多的硬盘空间,或内存空间)血的教训!!!!

本文转载自: 掘金

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

==与equals 的区别,为什么重写 equals 方法也

发表于 2021-08-01

==

  • 引用类型:== 是直接比较的两个对象的堆内存地址,如果相等,则说明两个引用实际是指向同一个对象地址的。
  • 基本类型:对于 基本数据类型(8个)和 String 来说又是怎样呢?
1
2
3
4
5
6
7
8
9
10
java复制代码int a = 123;
int b = 123;
System.out.println(a == b); // true

String s1 = "abc";
String s2 = "abc";
System.out.println(s1 == s2); // true

String s3 = new String("abc");
System.out.println(s1 == s3); // false

对于基本类型(8个)和 直接声明的 “abc” 字符串,他们是作为字面量存在常量池中以 HashSet 策略存储起来的,在常量池中,一个常量只会对应一个地址,所以它们的引用都是指向的同一块地址。

  • 另外,对于基本数据的包装类型,除了Float和Double之外,其他的六种都是实现了常量池技术,其中 Integer 在常量池中的存储范围为 [-128,127] ,在这个范围外的值,会在堆内存中创建一个新的对象保存这个值。

equals

  • Object 的通用方法,在没有重写之前,与 == 是没有区别的;
1
2
3
java复制代码    public boolean equals(Object obj) {
return (this == obj);
}
  • 而一般 equals 方法是需要我们自行重写的,String 和 基本类型封装类就重写了 equals,从而进行的是内容的比较;
  • 一般实现:
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class Student {
private String num;
private String name;

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
return Objects.equals(num, student.num) &&
Objects.equals(name, student.name);
}

@Override
public int hashCode() {
return Objects.hash(num, name);
}
}
+ 检查是否为同一个对象的引用,如果是直接返回 true; + 检查是否为空,是否同一类型,为空 或 类型不一致,返回 false; + 将 Object 对象转型; + 判断每个属性的值是否相等

hashCode

  • Object 的通用方法,hashcode是根据对象的内存地址经哈希算法得来的;
  • hashCode 方法主要是为了给 HashMap、HashSet 等集合类使用的。
  • 一些规定:
    • 两个对象相等,hashcode一定相等
    • 两个对象不等,hashcode不一定不等
    • hashcode相等,两个对象不一定相等
    • hashcode不等,两个对象一定不等

为什么重写 equals 方法一定要重写 hashCode 方法?

  • 官方说法是:对象的equals方法被重写,那么对象的hashCode()也尽量重写;(主要看应用场景)
  • equals 方法 和 hashCode 方法本身并没有紧密的联系,只是应用场景(如HashMap、HashSet等集合类的存取)使它们出现在了一起,才有了这句话;
  • 哈希集合(如HashMap)要保证元素唯一性。key对象放入集合,先经过hash运算,得到index,找到数组对应位置,还要和此位置的对象进行比较(先用hash比较,后用equals方法),不一样就插入到后面形成链表,一样的话就不用插入了。重写了equals()方法,但不重写hashcode()方法,那可能两个相同的对象放入到了不同的位置,元素的唯一性就不存在了。(HashMap存入键值对的方式要复杂的多,这里只是简要说下)

本文转载自: 掘金

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

Java 中 this 和 super 的用法详解

发表于 2021-08-01

前言

Java 中的关键字上篇我们讲到了final,深入理解 Java 中的 final 关键字
,这次我们来回顾一下this和super这两个关键字的用法,作为一名Java程序员,我觉得基础是最重要的,因为它决定了我们的上限,所以我的文章大部分还是以分享Java基础知识为主,学好基础,后面的知识我想学起来就变得简单。废话不多说,进入正文。

this

this 关键字只能在方法内部使用,表示对调用方法的那个对象的引用。

其实简单来说 this 关键字就是表示当前对象,下面我们来具体介绍 this 关键字在Java中的用法。

1、调用成员变量

在一个类的方法内部,如果我们想调用其成员变量,不用 this,我们会怎么做?

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

private String name = "xiaoming";

public String getName() {
return name;
}

public void setName(String name) {
name = name;
}
}

看上面的代码,我们在 ThisTest 类中创建了一个 name 属性,然后创建了一个 setName 方法,注意这个方法的形参也是 String name,那么我们通过 name = name 这样赋值,会改变成员变量 name 的属性吗?

1
2
3
4
5
java复制代码public static void main(String[] args) {
ThisTest thisTest = new ThisTest();
thisTest.setName("xiaoma");
System.out.println(thisTest.getName());
}

打印结果是 xiaoming,而不是我们重新设置的 xiaoma,显然这种方式是不能在方法内部调用到成员变量的。因为形参的名字和成员变量的名字相同,setName 方法内部的 name = name,根据最近原则,编译器默认是将这两个 name 属性都解析为形参 name,从而导致我们设值操作和成员变量 name 完全没有关系,当然设置不了。

解决办法就是使用 this 关键字。我们将 setName 方法修改如下:

1
2
3
java复制代码public void setName(String name) {
this.name = name;
}

在调用上面的 main 方法进行赋值,打印的结果就是 xiaoma了。

this 表示当前对象,也就是调用该方法的对象,对象.name 肯定就是调用的成员变量。

2、调用构造方法

构造方法是与类同名的一个方法,构造方法没有返回值,但是也不能用 void 来修饰。在一个类中,必须存在一个构造方法,如果没有,编译器会在编译的时候自动为这个类添加一个无参构造方法。一个类能够存在多个构造方法,调用的时候根据参数来区分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class Student {

private int age;

private String name;

public Student() {
this("小马",50);
}

public Student(String name, int age) {
this.name = name;
this.age = age;
System.out.println(name + "今年" + age + "岁了");
}


public static void main(String[] args) {
Student student01 = new Student();
Student student02 = new Student("小军",45);
}
}

通过this("小马",50)来调用另外一个构造方法 Student(String name, int age) 来给成员变量初始化赋值。

输出结果:

1
2
3
4
text复制代码小马今年50岁了
小军今年45岁了

Process finished with exit code 0

注意:通过 this 来调用构造方法,只能将这条代码放在构造函数的第一行,这是编译器的规定,如下所示:放在第二行会报错。

image-20210729163733035

3、调用普通方法

this 表示当前对象,那么肯定能够调用当前类的普通方法。

1
2
3
4
5
6
7
java复制代码public Student() {
this.say();
}

public void say(){
System.out.println("小马很会唱歌。");
}

4、返回当前对象

1
2
3
4
5
6
java复制代码public class ThisTest {

public Object newObject(){
return this;
}
}

这表示的意思是谁调用 newObject() 方法,那么就返回谁的引用。

super

Java 中的 super 关键字则是表示 父类对象的引用。

我们分析这句话父类对象的引用,那说明我们使用的时候只能在子类中使用,既然是对象的引用,那么我们也可以用来调用成员属性以及成员方法,当然了,这里的 super 关键字还能够调用父类的构造方法。

具体有如下几种用法:

1、调用父类的构造方法

Java中的继承大家都应该了解,子类继承父类,我们是能够用子类的对象调用父类的属性和方法的,我们知道属性和方法只能够通过对象调用,那么我们可以大胆假设一下:在创建子类对象的同时,也创建了父类的对象,而创建对象是通过调用构造函数实现的,那么我们在创建子类对象的时候,应该会调用父类的构造方法。

下面我们看这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public class Teacher {

public Teacher(){
System.out.println("我是一名人民教师。");
}
}

class Student extends Teacher {

public Student(){
System.out.println("我是一名学生。");
}
}

下面我们创建子类的对象:

1
2
3
java复制代码public static void main(String[] args) {
Student s = new Student();
}

输出结果:

1
2
3
4
text复制代码我是一名人民教师。
我是一名学生。

Process finished with exit code 0

通过打印结果看到我们在创建子类对象的时候,首先调用了父类的构造方法,接着调用子类的构造方法,也就是说在创建子类对象的时候,首先创建了父类对象,与前面我们猜想的一致。

那么问题又来了:是在什么时候调用的父类构造方法呢?

可以参考Java官方文档:docs.oracle.com/javase/spec…

image-20210729185246748

红色框内的英文翻译为:如果声明的类是原始类Object,那么默认的构造函数有一个空的主体。否则,默认构造函数只是简单地调用没有参数的超类构造函数。

也就是说:除了顶级类 Object.class 构造函数没有调用父类的构造方法,其余的所有类都默认在构造函数中调用了父类的构造函数(没有显式声明父类的子类其父类是 Object)。

那么是通过什么来调用的呢?我们接着看官方文档:

image-20210729185503815

上面的意思大概就是:超类构造函数通过 super 关键字调用,并且是以 super 关键字开头。

所以上面的 Student类的构造方法实际上应该是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码class Student extends Teacher {

public Student(){
super();//子类通过super调用父类的构造方法
System.out.println("我是一名学生。");
}


public static void main(String[] args) {
Student s = new Student();
}
}

子类默认是通过 super() 调用父类的无参构造方法,如果父类显示声明了一个有参构造方法,而没有声明无参构造方法,实例化子类是会报错的。

image-20210729185801603

 解决办法就是通过 super 关键字调用父类的有参构造方法:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码class Student extends Teacher {

public Student(){
super("小马");
System.out.println("我是一名学生。");
}


public static void main(String[] args) {
Student s = new Student();
}
}

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

public String name = "小马";

public Teacher() {
System.out.println("我是一名人民教师。");
}
}

class Student extends Teacher {

public Student() {
System.out.println("我是一名学生。");
}

public void fatherName() {
System.out.println("我的父类名字是:" + super.name);//调用父类的属性
}

public static void main(String[] args) {
Student student = new Student();
student.fatherName();
}
}

输出结果:

1
2
3
4
5
text复制代码我是一名人民教师。
我是一名学生。
我的父类名字是:小马

Process finished with exit code 0

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

public String name;

public Teacher() {
System.out.println("我是一名人民教师。");
}

public void setName(String name){
this.name = name;
}
}

class Student extends Teacher {

public Student() {
super();//调用父类的构造方法
System.out.println("我是一名学生。");
}

public void fatherName() {
super.setName("小军");//调用父类普通方法
System.out.println("我的父类名字是:" + super.name);//调用父类的属性
}

public static void main(String[] args) {
Student student = new Student();
student.fatherName();
}
}

输出结果:

1
2
3
4
5
text复制代码我是一名人民教师。
我是一名学生。
我的父类名字是:小军

Process finished with exit code 0

4、this 和 super 出现在同一个构造方法中?

假设 super() 在 this() 关键字的前面

首先通过 super() 调用父类构造方法,对父类进行一次实例化。接着调用 this() ,this() 方法会调用子类的构造方法,在子类的构造方法中又会对父类进行一次实例化。也就是说我们对子类进行一次实例化,对造成对父类进行两次实例化,所以显然编译器是不允许的。

image-20210729191452110

反过来 this() 在 super() 之前也是一样。而且编译器有限定 this() 和 super() 这两个关键字都只能出现在构造方法的第一行,将这两个关键字放在一起,总有一个关键字在第二行,编译是不能通过的。

image-20210729191530142

this和super异同

  • super(参数):调用基类中的某一个构造函数(应该为构造函数中的第一条语句)。
  • this(参数):调用本类中另一种形成的构造函数(应该为构造函数中的第一条语句)。
  • super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名、 super.成员函数据名(实参) 。
  • this:它代表当前对象名(在程序中易产生二义性之处,应使用 this 来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用 this 来指明成员变量名)。
  • 调用super()必须写在子类构造方法的第一行,否则编译不通过。每个子类构造方法的第一条语句,都是隐含地调用 super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
  • super() 和 this() 类似,区别是,super() 从子类中调用父类的构造方法,this() 在同一类内调用其它方法。
  • super() 和 this() 均需放在构造方法内第一行。
  • 尽管可以用this调用一个构造器,但却不能调用两个。
  • this 和 super 不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有 super 语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
  • this() 和 super() 都指的是对象,所以,均不可以在 static 环境中使用。包括:static 变量,static 方法,static 语句块。
  • 从本质上讲,this 是一个指向本对象的指针, 然而 super 是一个 Java 关键字。

结尾

我是一个正在被打击还在努力前进的码农。如果文章对你有帮助,记得点赞、关注哟,谢谢!

本文转载自: 掘金

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

SpringBoot核心机制解读系列三、Applicatio

发表于 2021-08-01

@[toc]

所有调试均使用SpringBoot 2.4.5版本。

ApplicationListener事件监听机制其实是由Spring提供的,应用内部的事件驱动机制。也就是Pub/Sub发布订阅机制在应用内部的实现。一般主要是用于监控应用内部的一些运行状况,在应用开发中也可以使用。

具体的实现机制可以到Spring中去探究,这里就来简单理解下SpringBoot对这个事件驱动机制做了哪些封装。

一、事件监听器使用

1、自己实现一个事件监听器

首先创建一个自定义的事件监听器:

1
2
3
4
5
6
java复制代码public class MyApplicationListener implements ApplicationListener<ApplicationEvent> {
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
System.out.println("======>MyApplicationListener: "+applicationEvent);
}
}

然后还是在项目的spring.factories中配置监听器

1
2
properties复制代码org.springframework.context.ApplicationListener=\
com.roy.applicationListener.MyApplicationListener

然后配置启动类。在启动类中发布一个自己的事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@SpringBootApplication
public class P1Application implements CommandLineRunner {
public static void main(String[] args) {
final SpringApplication application = new SpringApplication(P1Application.class);
// application.addInitializers(new MyApplicationContextInitializer());
application.run(args);
}
@Autowired
private ApplicationContext applicationContext;

@Override
public void run(String... args) throws Exception {
//自行发布一个事件。
applicationContext.publishEvent(new ApplicationEvent("selfEvent") {
});
}
}

正常启动SpringBoot应用,就能打印出启动过程中的关键事件的日志。这里把关键的事件日志给整理出来:

1
2
3
4
5
6
7
8
9
10
11
dart复制代码======>MyApplicationListener: org.springframework.boot.context.event.ApplicationStartingEvent[source=org.springframework.boot.SpringApplication@60c6f5b]
======>MyApplicationListener: org.springframework.boot.context.event.ApplicationEnvironmentPreparedEvent[source=org.springframework.boot.SpringApplication@60c6f5b]
======>MyApplicationListener: org.springframework.boot.context.event.ApplicationContextInitializedEvent[source=org.springframework.boot.SpringApplication@60c6f5b]
======>MyApplicationListener: org.springframework.boot.context.event.ApplicationPreparedEvent[source=org.springframework.boot.SpringApplication@60c6f5b]
======>MyApplicationListener: org.springframework.context.event.ContextRefreshedEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@1d296da, started on Wed Apr 28 13:33:33 CST 2021]
======>MyApplicationListener: org.springframework.boot.context.event.ApplicationStartedEvent[source=org.springframework.boot.SpringApplication@60c6f5b]
======>MyApplicationListener: org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@1d296da, started on Wed Apr 28 13:33:33 CST 2021]
======>MyApplicationListener: com.roy.P1Application$1[source=selfEvent] ##!!自行发布的事件。
======>MyApplicationListener: org.springframework.boot.context.event.ApplicationReadyEvent[source=org.springframework.boot.SpringApplication@60c6f5b]
======>MyApplicationListener: org.springframework.boot.availability.AvailabilityChangeEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@1d296da, started on Wed Apr 28 13:33:33 CST 2021]
======>MyApplicationListener: org.springframework.context.event.ContextClosedEvent[source=org.springframework.context.annotation.AnnotationConfigApplicationContext@1d296da, started on Wed Apr 28 13:33:33 CST 2021]

这里就打印出了SpringBoot应用启动过程中的多个内部事件。实际上这多个内部事件也就对应了启动过程的各个阶段,是梳理SpringBoot启动流程非常好的入口。

Spring事件机制的其他细节这里就不多说了,大家可以自行了解。我们这里还是专注于SpringBoot的部分。

2、事件监听器的其他配置方式:

这个事件监听机制是Spring非常重要的一个机制,有非常多的配置方式。除了上面提到的基于spring.factories文件配置的方式,还有其他几种配置方式。

2.1 SpringApplication.addListener

跟之前的Initializer一样,这个事件监听器也可以在SpringApplication中直接添加。

1
2
3
4
5
6
7
8
9
java复制代码@SpringBootApplication
public class P1Application implements CommandLineRunner {
public static void main(String[] args) {
final SpringApplication application = new SpringApplication(P1Application.class);
//添加事件监听器
application.addListeners(new MyApplicationListener());
application.run(args);
}
}

2.2 基于注解添加

基于注解,将MyApplicationListener配置到Spring的IOC容器中。

1
2
3
4
5
6
7
java复制代码@Configuration
public class MyApplicationListener implements ApplicationListener<ApplicationEvent> {
@Override
public void onApplicationEvent(ApplicationEvent applicationEvent) {
System.out.println("======>MyApplicationListener: "+applicationEvent);
}
}

2.3 在SpringBoot的配置文件中配置

另外还一种方式,可以在SpringBoot的配置文件application.properties中配置

1
properties复制代码context.listener.classes=com.roy.applicationListener.MyApplicationListener

这几种方式都可以配置事件监听器。另外,其实在上一章节ApplicationContextInitializer中也能看到,在SpringBoot内部也在应用初始化中扩展出了很多通过application添加事件监听器的扩展。

二、核心机制解读

事件机制的使用方式很多,不同的配置方式也有不同的加载流程。我们这里还是只解读SpringBoot中如何通过spring.factories文件来加载事件监听器的。

SpringBoot中对于监听器的处理,也跟ApplicationContextInitializer的处理流程是差不多的。首先在SpringApplication的构造方法中加载所有的监听器:

1
2
3
4
5
6
7
java复制代码public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
...
this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
//加载spring.facotries中注册的所有事件监听器
this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
...
}

然后在SpringApplication的run方法中启动所有监听器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
java复制代码public ConfigurableApplicationContext run(String... args) {
...
SpringApplicationRunListeners listeners = this.getRunListeners(args);//<====注册SpringApplicationRunListener
listeners.starting(bootstrapContext, this.mainApplicationClass); //<===会发布ApplicationStartingEvent事件

try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = this.prepareEnvironment(listeners, bootstrapContext, applicationArguments);//<===在这个过程中会发布ApplicationEnvironmentPreparedEvent
this.configureIgnoreBeanInfo(environment);
Banner printedBanner = this.printBanner(environment);
context = this.createApplicationContext();
context.setApplicationStartup(this.applicationStartup);
this.prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);// <==发布ApplicationContextInitializedEvent ApplicationPreparedEvent事件
this.refreshContext(context);// <===发布ContextRefreshedEvent事件
this.afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
(new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
}

listeners.started(context);//<===发布ApplicationStartedEvent AvailabilityChangeEvent事件
this.callRunners(context, applicationArguments);
} catch (Throwable var10) {
this.handleRunFailure(context, var10, listeners);
throw new IllegalStateException(var10);
}

try {
listeners.running(context);//发布ApplicationReadyEvent 和 AvailabilityChangeEvent事件
return context;
} catch (Throwable var9) {
this.handleRunFailure(context, var9, (SpringApplicationRunListeners)null);
throw new IllegalStateException(var9);
}
}

首先在注册SpringApplicationRunListener时,就会解析spring.factories,读取其中的org.springframework.boot.SpringApplicationRunListener配置。

1
2
3
properties复制代码# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

然后从发布事件的地方往下调试,可以看到SpringBoot事件发布的核心对象EventPublishingRunListener。通过其中的initialMulticaster组件来发布不同的事件。 而他实现事件监听的方式就是在发布事件时,实时调用一下已经注册的所有对应事件的监听器。

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 class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {

private final SpringApplication application;

private final String[] args;

private final SimpleApplicationEventMulticaster initialMulticaster;

public EventPublishingRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
this.initialMulticaster = new SimpleApplicationEventMulticaster();
for (ApplicationListener<?> listener : application.getListeners()) {
this.initialMulticaster.addApplicationListener(listener);
}
}

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

@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
//发布事件
this.initialMulticaster
.multicastEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args));
}
.....
}

调用Spring中的SimpleApplicationEventMulticaster组件发布事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
...

public void multicastEvent(ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);
Executor executor = this.getTaskExecutor();
//根据event查找注册的监听器
Iterator var5 = this.getApplicationListeners(event, type).iterator();

while(var5.hasNext()) {
ApplicationListener<?> listener = (ApplicationListener)var5.next();
if (executor != null) {
executor.execute(() -> {
//调用Listener的onApplicationEvent方法。
this.invokeListener(listener, event);
});
} else {
this.invokeListener(listener, event);
}
}

}

这其中initialMulticaster对象已经是Spring-Context包中的内容。所以这里就完成了SpringBoot中事务监听机制的梳理。

这个事件机制也是SpringBoot使用过程中非常好的功能扩展点,因为应用启动过程中,这些事件都已经默认发布了,可以叠加自己想要的应用初始化工作。这些关键事件的发布顺序也是非常重要的。例如,如果你的扩展功能需要用到Spring的IOC容器,那就只能去监听ContextRefreshedEvent之后的几个内部事件。

三、SpringBoot中的核心实现

接下来梳理SpringBoot当中通过spring.factories默认注册的事务监听器

1
2
3
4
5
6
7
8
9
10
11
12
13
properties复制代码#spirng-boot.jar
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
#spring-boot-autoconfigure
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

那接下来同样是梳理几个有代表性的事务监听器逻辑。

例如BackgroundPreinitializer,他会将几个比较耗时的初始化工作提前到应用启动过程中加载,并且单独启动一个线程来加快加载速度。

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复制代码@Order(LoggingApplicationListener.DEFAULT_ORDER + 1)
public class BackgroundPreinitializer implements ApplicationListener<SpringApplicationEvent> {

//这段说明很重要
/**
* System property that instructs Spring Boot how to run pre initialization. When the
* property is set to {@code true}, no pre-initialization happens and each item is
* initialized in the foreground as it needs to. When the property is {@code false}
* (default), pre initialization runs in a separate thread in the background.
* @since 2.1.0
*/
public static final String IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME = "spring.backgroundpreinitializer.ignore";

private static final AtomicBoolean preinitializationStarted = new AtomicBoolean();

private static final CountDownLatch preinitializationComplete = new CountDownLatch(1);

private static final boolean ENABLED;

static {
ENABLED = !Boolean.getBoolean(IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME) && !NativeDetector.inNativeImage()
&& Runtime.getRuntime().availableProcessors() > 1;
}
//监听SpringApplicationEvent事件。这个事件是之前调试过的几个重要事件的父接口
@Override
public void onApplicationEvent(SpringApplicationEvent event) {
if (!ENABLED) {
return;
}
if (event instanceof ApplicationEnvironmentPreparedEvent
&& preinitializationStarted.compareAndSet(false, true)) {
performPreinitialization();
}
if ((event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent)
&& preinitializationStarted.get()) {
try {
preinitializationComplete.await();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
...
}

接下来其他几个事件监听器的功能也简单总结下。有兴趣的建议自行调试下代码,这样才能形成自己的理解。

  • org.sf.boot.ClearCachesApplicationListener:应用上下文加载完成后对缓存做清除工作,响应事件ContextRefreshedEvent
  • org.sf.boot.builder.ParentContextCloserApplicationListener:监听双亲应用上下文的关闭事件并往自己的孩子应用上下文中传播,相关事件ParentContextAvailableEvent/ContextClosedEvent
  • org.sf.boot.context.FileEncodingApplicationListener:如果系统文件编码和环境变量中指定的不同则终止应用启动。
    具体的方法是比较系统属性file.encoding和环境变量spring.mandatory-file-encoding是否相等(大小写不敏感)。
  • org.sf.boot.context.config.AnsiOutputApplicationListener:根据spring.output.ansi.enabled参数配置AnsiOutput
  • org.sf.boot.context.config.DelegatingApplicationListener:监听到事件后转发给环境变量context.listener.classes指定的那些事件监听器
  • org.sf.boot.context.logging.LoggingApplicationListener 配置LoggingSystem。使用logging.config环境变量指定的配置或者缺省配置
  • org.springframework.boot.env.EnvironmentPostProcessorApplicationListener: 加载spring.factories文件中配置的EnvironmentPostProcessor。
  • org.sf.boot.liquibase.LiquibaseServiceLocatorApplicationListener 使用一个可以和Spring Boot可执行jar包配合工作的版本替换liquibase ServiceLocator

org.sf.boot.context.config.ConfigFileApplicationListener : 指定SpringBoot的配置文件地址。 但是在2.4.5版本已经移除。

最后,有没有觉得EnvironmentPostProcessorApplicationListener这个事件监听器挺有意思的?SpringBoot直接将spring.factories中EnvironmentPostProcessor机制的加载工作交给了事件监听器,接下来我们就趁热打铁,来继续看下EnvironmentPostProcessor 的处理机制。

四、EnvironmentPostProcessor使用

EnvironmentPostProcessor是在环境信息加载完成后进行一些补充处理。例如下面一个示例可以在SpringBoot读取完application.properties后,补充读取另一个配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码//@Component
public class MyEnvironmentPostProcessor implements EnvironmentPostProcessor{

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
try(InputStream input = new FileInputStream("E:\\ds.properties")) {
Properties properties = new Properties();
properties.load(input);
PropertiesPropertySource propertySource = new PropertiesPropertySource("ve", properties);
environment.getPropertySources().addLast(propertySource);
System.out.println("====加载外部配置文件完毕====");
} catch (Exception e) {
e.printStackTrace();
}
}

使用的方式同样可以配置到spring.factories文件中,或者通过@Component注解加入到IOC容器中。

1
2
properties复制代码org.springframework.boot.env.EnvironmentPostProcessor=\
com.roy.environmentPostProcessor.MyEnvironmentPostProcessor

四、EnvironmentPostProcessor 加载机制

EnvironmentPostProcessor 是通过EnvironmentPostProcessorApplicationListener监听Spring事件来对运行环境进行补充的后续处理。先来看下他监听了哪些事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class EnvironmentPostProcessorApplicationListener implements SmartApplicationListener, Ordered {

//执行优先级非常高
/**
* The default order for the processor.
*/
public static final int DEFAULT_ORDER = Ordered.HIGHEST_PRECEDENCE + 10;
...
@Override
public void onApplicationEvent(ApplicationEvent event) {
if (event instanceof ApplicationEnvironmentPreparedEvent) {
onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
}
if (event instanceof ApplicationPreparedEvent) {
onApplicationPreparedEvent((ApplicationPreparedEvent) event);
}
if (event instanceof ApplicationFailedEvent) {
onApplicationFailedEvent((ApplicationFailedEvent) event);
}
}

从这几个事件当中可以看出,在他的子处理器中是不能引用IOC容器的。

然后再往下继续调试他的onApplicationEnvironmentPreparedEvent方法,找下他解析spring.factories,加载EnvironmentPostProcessor 实现的机制。这个机制就不再是使用 SpringFactoriesLoader 了。

1
2
3
4
5
6
7
8
9
java复制代码//处理EnvironmentPostProcessor的方式。
private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
ConfigurableEnvironment environment = event.getEnvironment();
SpringApplication application = event.getSpringApplication();
//重点跟踪getEnvironmentPostProcessors方法
for (EnvironmentPostProcessor postProcessor : getEnvironmentPostProcessors(event.getBootstrapContext())) {
postProcessor.postProcessEnvironment(environment, application);
}
}

一路往下调试,会跟踪到ReflectionEnvironmentPostProcessorsFactory的getEnvironmentPostProcessors方法。

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public List<EnvironmentPostProcessor> getEnvironmentPostProcessors(DeferredLogFactory logFactory,
ConfigurableBootstrapContext bootstrapContext) {
Instantiator<EnvironmentPostProcessor> instantiator = new Instantiator<>(EnvironmentPostProcessor.class,
(parameters) -> {
parameters.add(DeferredLogFactory.class, logFactory);
parameters.add(Log.class, logFactory::getLog);
parameters.add(ConfigurableBootstrapContext.class, bootstrapContext);
parameters.add(BootstrapContext.class, bootstrapContext);
parameters.add(BootstrapRegistry.class, bootstrapContext);
});
return instantiator.instantiate(this.classNames);
}

这个parameters就是给这一组实例添加一些默认的参数类。后面各个子类可以创建包含了这些参数的构造方法来进行实例化。 有没有发现,这就是一个简单的IOC属性注入吗?学IOC有比这个更经典的入手案例吗?

具体实例化的方式在Instantiator这个类中

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码private T instantiate(Class<?> type) throws Exception {
Constructor<?>[] constructors = type.getDeclaredConstructors();
Arrays.sort(constructors, CONSTRUCTOR_COMPARATOR);
for (Constructor<?> constructor : constructors) {
//args就来自于上面指定的几个类
Object[] args = getArgs(constructor.getParameterTypes());
if (args != null) {
ReflectionUtils.makeAccessible(constructor);
return (T) constructor.newInstance(args);
}
}
throw new IllegalAccessException("Unable to find suitable constructor");
}

五、SpringBoot中注册的EnvironmentPostProcessor实现

接下来还是梳理下SpringBoot在spring.factories当中注册的EnvironmentPostProcessor实现:

1
2
3
4
5
6
7
8
9
properties复制代码#spirng-boot.jar当中
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor

这些实现类具体是干什么的,这个我暂时还看不太懂,应该跟具体的一些细节场景有关了。网上也暂时没有找到相关的资料。就不再去具体分析了。

不得不吐槽下,网上的帖子千篇一律只讲了怎么用EnvironmentPostProcesser读取额外的配置文件,但是这些机制还真没有人来分析过。

接下来简单跟踪了一下ConfigDataEnvironmentPostProcessor这个实现。发现他的实现过程中又引入了spring.factories中的org.springframework.boot.context.config.ConfigDataLoader功能机制。又是一个庞大复杂的功能机制,目前我也没太弄明白他的具体功能。以后了解到了再补上把。

本文转载自: 掘金

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

1…585586587…956

开发者博客

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