From 5dda3425451094f416a6486b9d128e1f619a514c Mon Sep 17 00:00:00 2001 From: dabin <1713476357@qq.com> Date: Sat, 12 Aug 2023 11:14:21 +0800 Subject: [PATCH 1/8] update --- .../excellent-article/29-idempotent-design.md | 1 + .../excellent-article/30-yi-di-duo-huo.md | 563 ++++++++++++++++++ .../31-mysql-data-sync-es.md | 503 ++++++++++++++++ docs/learn/ghelper.md | 29 +- docs/note/docker-note.md | 15 + docs/note/write-sql.md | 93 +++ docs/zsxq/article/select-max-rows.md | 225 +++++++ docs/zsxq/article/sql-optimize.md | 196 ++++++ docs/zsxq/introduce.md | 2 +- docs/zsxq/question/qa-or-java.md | 29 + docs/zsxq/share/completable-future-bug.md | 250 ++++++++ docs/zsxq/share/oom.md | 13 + docs/zsxq/share/slow-query.md | 62 ++ .../zsxq/share/spring-upgrade-copy-problem.md | 146 +++++ package-lock.json | 159 +++++ 15 files changed, 2280 insertions(+), 6 deletions(-) create mode 100644 docs/advance/excellent-article/30-yi-di-duo-huo.md create mode 100644 docs/advance/excellent-article/31-mysql-data-sync-es.md create mode 100644 docs/note/docker-note.md create mode 100644 docs/note/write-sql.md create mode 100644 docs/zsxq/article/select-max-rows.md create mode 100644 docs/zsxq/article/sql-optimize.md create mode 100644 docs/zsxq/question/qa-or-java.md create mode 100644 docs/zsxq/share/completable-future-bug.md create mode 100644 docs/zsxq/share/oom.md create mode 100644 docs/zsxq/share/slow-query.md create mode 100644 docs/zsxq/share/spring-upgrade-copy-problem.md diff --git a/docs/advance/excellent-article/29-idempotent-design.md b/docs/advance/excellent-article/29-idempotent-design.md index 3ab7b34..6cf7d5b 100644 --- a/docs/advance/excellent-article/29-idempotent-design.md +++ b/docs/advance/excellent-article/29-idempotent-design.md @@ -12,6 +12,7 @@ head: - name: description content: 努力打造最优质的Java学习网站 --- + ## 接口的幂等性如何设计? 分布式系统中的某个接口,该如何保证幂等性? diff --git a/docs/advance/excellent-article/30-yi-di-duo-huo.md b/docs/advance/excellent-article/30-yi-di-duo-huo.md new file mode 100644 index 0000000..75657f1 --- /dev/null +++ b/docs/advance/excellent-article/30-yi-di-duo-huo.md @@ -0,0 +1,563 @@ +--- +sidebar: heading +title: 异地多活 +category: 优质文章 +tag: + - 架构 +head: + - - meta + - name: keywords + content: 异地多活,同城灾备,同城双活,两地三中心,异地双活,异地多活 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + +在软件开发领域,「异地多活」是分布式系统架构设计的一座高峰,很多人经常听过它,但很少人理解其中的原理。 + +**异地多活到底是什么?为什么需要异地多活?它到底解决了什么问题?究竟是怎么解决的?** + +这些疑问,想必是每个程序看到异地多活这个名词时,都想要搞明白的问题。 + +有幸,我曾经深度参与过一个中等互联网公司,建设异地多活系统的设计与实施过程。所以今天,我就来和你聊一聊异地多活背后的的实现原理。 + +认真读完这篇文章,我相信你会对异地多活架构,有更加深刻的理解。 + +**这篇文章干货很多,希望你可以耐心读完。** + +![](http://img.topjavaer.cn/img/202306290845298.png) + + + +# 01 系统可用性 + +要想理解异地多活,我们需要从架构设计的原则说起。 + +现如今,我们开发一个软件系统,对其要求越来越高,如果你了解一些「架构设计」的要求,就知道一个好的软件架构应该遵循以下 3 个原则: + +1. 高性能 +2. 高可用 +3. 易扩展 + +其中,高性能意味着系统拥有更大流量的处理能力,更低的响应延迟。例如 1 秒可处理 10W 并发请求,接口响应时间 5 ms 等等。 + +易扩展表示系统在迭代新功能时,能以最小的代价去扩展,系统遇到流量压力时,可以在不改动代码的前提下,去扩容系统。 + +而「高可用」这个概念,看起来很抽象,怎么理解它呢?通常用 2 个指标来衡量: + +- **平均故障间隔 MTBF**(Mean Time Between Failure):表示两次故障的间隔时间,也就是系统「正常运行」的平均时间,这个时间越长,说明系统稳定性越高 +- **故障恢复时间 MTTR**(Mean Time To Repair):表示系统发生故障后「恢复的时间」,这个值越小,故障对用户的影响越小 + +可用性与这两者的关系: + +> 可用性(Availability)= MTBF / (MTBF + MTTR) * 100% + +这个公式得出的结果是一个「比例」,通常我们会用「N 个 9」来描述一个系统的可用性。 + +![](http://img.topjavaer.cn/img/202306290845411.png) + +从这张图你可以看到,要想达到 4 个 9 以上的可用性,平均每天故障时间必须控制在 10 秒以内。 + +也就是说,只有故障的时间「越短」,整个系统的可用性才会越高,每提升 1 个 9,都会对系统提出更高的要求。 + +我们都知道,系统发生故障其实是不可避免的,尤其是规模越大的系统,发生问题的概率也越大。这些故障一般体现在 3 个方面: + +1. **硬件故障**:CPU、内存、磁盘、网卡、交换机、路由器 +2. **软件问题**:代码 Bug、版本迭代 +3. **不可抗力**:地震、水灾、火灾、战争 + +这些风险随时都有可能发生。所以,在面对故障时,我们的系统能否以「最快」的速度恢复,就成为了可用性的关键。 + +可如何做到快速恢复呢? + +这篇文章要讲的「异地多活」架构,就是为了解决这个问题,而提出的高效解决方案。 + +下面,我会从一个最简单的系统出发,带你一步步演化出一个支持「异地多活」的系统架构。 + +在这个过程中,你会看到一个系统会遇到哪些可用性问题,以及为什么架构要这样演进,从而理解异地多活架构的意义。 + +# 02 单机架构 + +我们从最简单的开始讲起。 + +假设你的业务处于起步阶段,体量非常小,那你的架构是这样的: + +![](http://img.topjavaer.cn/img/202306290846808.png) + +这个架构模型非常简单,客户端请求进来,业务应用读写数据库,返回结果,非常好理解。 + +但需要注意的是,这里的数据库是「单机」部署的,所以它有一个致命的缺点:一旦遭遇意外,例如磁盘损坏、操作系统异常、误删数据,那这意味着所有数据就全部「丢失」了,这个损失是巨大的。 + +如何避免这个问题呢?我们很容易想到一个方案:**备份**。 + +![](http://img.topjavaer.cn/img/202306290846288.png) + +你可以对数据做备份,把数据库文件「定期」cp 到另一台机器上,这样,即使原机器丢失数据,你依旧可以通过备份把数据「恢复」回来,以此保证数据安全。 + +这个方案实施起来虽然比较简单,但存在 2 个问题: + +1. **恢复需要时间**:业务需先停机,再恢复数据,停机时间取决于恢复的速度,恢复期间服务「不可用」 +2. **数据不完整**:因为是定期备份,数据肯定不是「最新」的,数据完整程度取决于备份的周期 + +很明显,你的数据库越大,意味故障恢复时间越久。那按照前面我们提到的「高可用」标准,这个方案可能连 1 个 9 都达不到,远远无法满足我们对可用性的要求。 + +那有什么更好的方案,既可以快速恢复业务?还能尽可能保证数据完整性呢? + +这时你可以采用这个方案:**主从副本**。 + +# 03 主从副本 + +你可以在另一台机器上,再部署一个数据库实例,让这个新实例成为原实例的「副本」,让两者保持「实时同步」,就像这样: + +![](http://img.topjavaer.cn/img/202306290846494.png) + +我们一般把原实例叫作主库(master),新实例叫作从库(slave)。这个方案的优点在于: + +- **数据完整性高**:主从副本实时同步,数据「差异」很小 +- **抗故障能力提升**:主库有任何异常,从库可随时「切换」为主库,继续提供服务 +- **读性能提升**:业务应用可直接读从库,分担主库「压力」读压力 + +这个方案不错,不仅大大提高了数据库的可用性,还提升了系统的读性能。 + +同样的思路,你的「业务应用」也可以在其它机器部署一份,避免单点。因为业务应用通常是「无状态」的(不像数据库那样存储数据),所以直接部署即可,非常简单。 + +![](http://img.topjavaer.cn/img/202306290846710.png) + +因为业务应用部署了多个,所以你现在还需要部署一个「接入层」,来做请求的「负载均衡」(一般会使用 nginx 或 LVS),这样当一台机器宕机后,另一台机器也可以「接管」所有流量,持续提供服务。 + +![](http://img.topjavaer.cn/img/202306290848119.png) + +从这个方案你可以看出,提升可用性的关键思路就是:**冗余**。 + +没错,担心一个实例故障,那就部署多个实例,担心一个机器宕机,那就部署多台机器。 + +到这里,你的架构基本已演变成主流方案了,之后开发新的业务应用,都可以按照这种模式去部署。 + +![](http://img.topjavaer.cn/img/202306290846366.png) + +但这种方案还有什么风险吗? + +# 04 风险不可控 + +现在让我们把视角下放,把焦点放到具体的「部署细节」上来。 + +按照前面的分析,为了避免单点故障,你的应用虽然部署了多台机器,但这些机器的分布情况,我们并没有去深究。 + +而一个机房有很多服务器,这些服务器通常会分布在一个个「机柜」上,如果你使用的这些机器,刚好在一个机柜,还是存在风险。 + +如果恰好连接这个机柜的交换机 / 路由器发生故障,那么你的应用依旧有「不可用」的风险。 + +> 虽然交换机 / 路由器也做了路线冗余,但不能保证一定不出问题。 + +部署在一个机柜有风险,那把这些机器打散,分散到不同机柜上,是不是就没问题了? + +这样确实会大大降低出问题的概率。但我们依旧不能掉以轻心,因为无论怎么分散,它们总归还是在一个相同的环境下:**机房**。 + +那继续追问,机房会不会发生故障呢? + +一般来讲,建设一个机房的要求其实是很高的,地理位置、温湿度控制、备用电源等等,机房厂商会在各方面做好防护。但即使这样,我们每隔一段时间还会看到这样的新闻: + +- 2015 年 5 月 27 日,杭州市某地光纤被挖断,近 3 亿用户长达 5 小时无法访问支付宝 +- 2021 年 7 月 13 日,B 站部分服务器机房发生故障,造成整站持续 3 个小时无法访问 +- 2021 年 10 月 9 日,富途证券服务器机房发生电力闪断故障,造成用户 2 个小时无法登陆、交易 +- … + +可见,即使机房级别的防护已经做得足够好,但只要有「概率」出问题,那现实情况就有可能发生。虽然概率很小,但一旦真的发生,影响之大可见一斑。 + +看到这里你可能会想,机房出现问题的概率也太小了吧,工作了这么多年,也没让我碰上一次,有必要考虑得这么复杂吗? + +但你有没有思考这样一个问题:**不同体量的系统,它们各自关注的重点是什么?** + +体量很小的系统,它会重点关注「用户」规模、增长,这个阶段获取用户是一切。等用户体量上来了,这个阶段会重点关注「性能」,优化接口响应时间、页面打开速度等等,这个阶段更多是关注用户体验。 + +等体量再大到一定规模后你会发现,「可用性」就变得尤为重要。像微信、支付宝这种全民级的应用,如果机房发生一次故障,那整个影响范围可以说是非常巨大的。 + +所以,再小概率的风险,我们在提高系统可用性时,也不能忽视。 + +分析了风险,再说回我们的架构。那到底该怎么应对机房级别的故障呢? + +没错,还是**冗余**。 + +# 05 同城灾备 + +想要抵御「机房」级别的风险,那应对方案就不能局限在一个机房内了。 + +现在,你需要做机房级别的冗余方案,也就是说,你需要再搭建一个机房,来部署你的服务。 + +简单起见,你可以在「同一个城市」再搭建一个机房,原机房我们叫作 A 机房,新机房叫 B 机房,这两个机房的网络用一条「专线」连通。 + +![](http://img.topjavaer.cn/img/202306290848349.png) + +有了新机房,怎么把它用起来呢?这里还是要优先考虑「数据」风险。 + +为了避免 A 机房故障导致数据丢失,所以我们需要把数据在 B 机房也存一份。最简单的方案还是和前面提到的一样:**备份**。 + +A 机房的数据,定时在 B 机房做备份(拷贝数据文件),这样即使整个 A 机房遭到严重的损坏,B 机房的数据不会丢,通过备份可以把数据「恢复」回来,重启服务。 + +![](http://img.topjavaer.cn/img/202306290848730.png) + +这种方案,我们称之为「**冷备**」。为什么叫冷备呢?因为 B 机房只做备份,不提供实时服务,它是冷的,只会在 A 机房故障时才会启用。 + +但备份的问题依旧和之前描述的一样:数据不完整、恢复数据期间业务不可用,整个系统的可用性还是无法得到保证。 + +所以,我们还是需要用「主从副本」的方式,在 B 机房部署 A 机房的数据副本,架构就变成了这样: + +![](http://img.topjavaer.cn/img/202306290849386.png) + +这样,就算整个 A 机房挂掉,我们在 B 机房也有比较「完整」的数据。 + +数据是保住了,但这时你需要考虑另外一个问题:**如果 A 机房真挂掉了,要想保证服务不中断,你还需要在 B 机房「紧急」做这些事情**: + +1. B 机房所有从库提升为主库 +2. 在 B 机房部署应用,启动服务 +3. 部署接入层,配置转发规则 +4. DNS 指向 B 机房接入层,接入流量,业务恢复 + +看到了么?A 机房故障后,B 机房需要做这么多工作,你的业务才能完全「恢复」过来。 + +你看,整个过程需要人为介入,且需花费大量时间来操作,恢复之前整个服务还是不可用的,这个方案还是不太爽,如果能做到故障后立即「切换」,那就好了。 + +因此,要想缩短业务恢复的时间,你必须把这些工作在 B 机房「提前」做好,也就是说,你需要在 B 机房提前部署好接入层、业务应用,等待随时切换。架构就变成了这样: + +![](http://img.topjavaer.cn/img/202306290849453.png) + +这样的话,A 机房整个挂掉,我们只需要做 2 件事即可: + +1. B 机房所有从库提升为主库 +2. DNS 指向 B 机房接入层,接入流量,业务恢复 + +这样一来,恢复速度快了很多。 + +到这里你会发现,B 机房从最开始的「空空如也」,演变到现在,几乎是「镜像」了一份 A 机房的所有东西,从最上层的接入层,到中间的业务应用,到最下层的存储。两个机房唯一的区别是,**A 机房的存储都是主库,而 B 机房都是从库**。 + +这种方案,我们把它叫做「**热备**」。 + +热的意思是指,B 机房处于「待命」状态,A 故障后 B 可以随时「接管」流量,继续提供服务。热备相比于冷备最大的优点是:**随时可切换**。 + +无论是冷备还是热备,因为它们都处于「备用」状态,所以我们把这两个方案统称为:**同城灾备**。 + +同城灾备的最大优势在于,我们再也不用担心「机房」级别的故障了,一个机房发生风险,我们只需把流量切换到另一个机房即可,可用性再次提高,是不是很爽?(后面还有更爽的) + +# 06 同城双活 + +我们继续来看这个架构。 + +虽然我们有了应对机房故障的解决方案,但这里有个问题是我们不能忽视的:**A 机房挂掉,全部流量切到 B 机房,B 机房能否真的如我们所愿,正常提供服务?** + +这是个值得思考的问题。 + +这就好比有两支军队 A 和 B,A 军队历经沙场,作战经验丰富,而 B 军队只是后备军,除了有军人的基本素养之外,并没有实战经验,战斗经验基本为 0。 + +如果 A 军队丧失战斗能力,需要 B 军队立即顶上时,作为指挥官的你,肯定也会担心 B 军队能否真的担此重任吧? + +我们的架构也是如此,此时的 B 机房虽然是随时「待命」状态,但 A 机房真的发生故障,我们要把全部流量切到 B 机房,其实是不敢百分百保证它可以「如期」工作的。 + +你想,我们在一个机房内部署服务,还总是发生各种各样的问题,例如:发布应用的版本不一致、系统资源不足、操作系统参数不一样等等。现在多部署一个机房,这些问题只会增多,不会减少。 + +另外,从「成本」的角度来看,我们新部署一个机房,需要购买服务器、内存、硬盘、带宽资源,花费成本也是非常高昂的,只让它当一个后备军,未免也太「大材小用」了! + +因此,我们需要让 B 机房也接入流量,实时提供服务,这样做的好处,**一是可以实时训练这支后备军,让它达到与 A 机房相同的作战水平,随时可切换,二是 B 机房接入流量后,可以分担 A 机房的流量压力**。这才是把 B 机房资源优势,发挥最大化的最好方案! + +那怎么让 B 机房也接入流量呢?很简单,就是把 B 机房的接入层 IP 地址,加入到 DNS 中,这样,B 机房从上层就可以有流量进来了。 + +![](https://kaito-blog-1253469779.cos.ap-beijing.myqcloud.com/2021/10/16342320382121.jpg) + +但这里有一个问题:别忘了,B 机房的存储,现在可都是 A 机房的「从库」,从库默认可都是「不可写」的,B 机房的写请求打到本机房存储上,肯定会报错,这还是不符合我们预期。怎么办? + +这时,你就需要在「业务应用」层做改造了。 + +你的业务应用在操作数据库时,需要区分「读写分离」(一般用中间件实现),即两个机房的「读」流量,可以读任意机房的存储,但「写」流量,只允许写 A 机房,因为主库在 A 机房。 + +![img](https://kaito-blog-1253469779.cos.ap-beijing.myqcloud.com/2021/10/16342338343503.jpg) + +这会涉及到你用的所有存储,例如项目中用到了 MySQL、Redis、MongoDB 等等,操作这些数据库,都需要区分读写请求,所以这块需要一定的业务「改造」成本。 + +因为 A 机房的存储都是主库,所以我们把 A 机房叫做「主机房」,B 机房叫「从机房」。 + +两个机房部署在「同城」,物理距离比较近,而且两个机房用「专线」网络连接,虽然跨机房访问的延迟,比单个机房内要大一些,但整体的延迟还是可以接受的。 + +业务改造完成后,B 机房可以慢慢接入流量,从 10%、30%、50% 逐渐覆盖到 100%,你可以持续观察 B 机房的业务是否存在问题,有问题及时修复,逐渐让 B 机房的工作能力,达到和 A 机房相同水平。 + +现在,因为 B 机房实时接入了流量,此时如果 A 机房挂了,那我们就可以「大胆」地把 A 的流量,全部切换到 B 机房,完成快速切换! + +到这里你可以看到,我们部署的 B 机房,在物理上虽然与 A 有一定距离,但整个系统从「逻辑」上来看,我们是把这两个机房看做一个「整体」来规划的,也就是说,相当于把 2 个机房当作 1 个机房来用。 + +这种架构方案,比前面的同城灾备更「进了一步」,B 机房实时接入了流量,还能应对随时的故障切换,这种方案我们把它叫做「**同城双活**」。 + +因为两个机房都能处理业务请求,这对我们系统的内部维护、改造、升级提供了更多的可实施空间(流量随时切换),现在,整个系统的弹性也变大了,是不是更爽了? + +那这种架构有什么问题呢? + +# 07 两地三中心 + +还是回到风险上来说。 + +虽然我们把 2 个机房当做一个整体来规划,但这 2 个机房在物理层面上,还是处于「一个城市」内,如果是整个城市发生自然灾害,例如地震、水灾(河南水灾刚过去不久),那 2 个机房依旧存在「全局覆没」的风险。 + +真是防不胜防啊?怎么办?没办法,继续冗余。 + +但这次冗余机房,就不能部署在同一个城市了,你需要把它放到距离更远的地方,部署在「异地」。 + +> 通常建议两个机房的距离要在 1000 公里以上,这样才能应对城市级别的灾难。 + +假设之前的 A、B 机房在北京,那这次新部署的 C 机房可以放在上海。 + +按照前面的思路,把 C 机房用起来,最简单粗暴的方案还就是做「冷备」,即定时把 A、B 机房的数据,在 C 机房做备份,防止数据丢失。 + +![](http://img.topjavaer.cn/img/202306290850748.png) + +这种方案,就是我们经常听到的「**两地三中心**」。 + +**两地是指 2 个城市,三中心是指有 3 个机房,其中 2 个机房在同一个城市,并且同时提供服务,第 3 个机房部署在异地,只做数据灾备。** + +这种架构方案,通常用在银行、金融、政企相关的项目中。它的问题还是前面所说的,启用灾备机房需要时间,而且启用后的服务,不确定能否如期工作。 + +所以,要想真正的抵御城市级别的故障,越来越多的互联网公司,开始实施「**异地双活**」。 + +# 08 伪异地双活 + +这里,我们还是分析 2 个机房的架构情况。我们不再把 A、B 机房部署在同一个城市,而是分开部署,例如 A 机房放在北京,B 机房放在上海。 + +前面我们讲了同城双活,那异地双活是不是直接「照搬」同城双活的模式去部署就可以了呢? + +事情没你想的那么简单。 + +如果还是按照同城双活的架构来部署,那异地双活的架构就是这样的: + +![](http://img.topjavaer.cn/img/202306290850564.png) + +注意看,两个机房的网络是通过「跨城专线」连通的。 + +此时两个机房都接入流量,那上海机房的请求,可能要去读写北京机房的存储,这里存在一个很大的问题:**网络延迟**。 + +因为两个机房距离较远,受到物理距离的限制,现在,两地之间的网络延迟就变成了「**不可忽视**」的因素了。 + +北京到上海的距离大约 1300 公里,即使架设一条高速的「网络专线」,光纤以光速传输,一个来回也需要近 10ms 的延迟。 + +况且,网络线路之间还会经历各种路由器、交换机等网络设备,实际延迟可能会达到 30ms ~ 100ms,如果网络发生抖动,延迟甚至会达到 1 秒。 + +> 不止是延迟,远距离的网络专线质量,是远远达不到机房内网络质量的,专线网络经常会发生延迟、丢包、甚至中断的情况。总之,不能过度信任和依赖「跨城专线」。 + +你可能会问,这点延迟对业务影响很大吗?影响非常大! + +试想,一个客户端请求打到上海机房,上海机房要去读写北京机房的存储,一次跨机房访问延迟就达到了 30ms,这大致是机房内网网络(0.5 ms)访问速度的 60 倍(30ms / 0.5ms),一次请求慢 60 倍,来回往返就要慢 100 倍以上。 + +而我们在 App 打开一个页面,可能会访问后端几十个 API,每次都跨机房访问,整个页面的响应延迟有可能就达到了**秒级**,这个性能简直惨不忍睹,难以接受。 + +看到了么,虽然我们只是简单的把机房部署在了「异地」,但「同城双活」的架构模型,在这里就不适用了,还是按照这种方式部署,这是「伪异地双活」! + +那如何做到真正的异地双活呢? + +# 09 真正的异地双活 + +既然「跨机房」调用延迟是不容忽视的因素,那我们只能尽量避免跨机房「调用」,规避这个延迟问题。 + +也就是说,上海机房的应用,不能再「跨机房」去读写北京机房的存储,只允许读写上海本地的存储,实现「就近访问」,这样才能避免延迟问题。 + +还是之前提到的问题:上海机房存储都是从库,不允许写入啊,除非我们只允许上海机房接入「读流量」,不接收「写流量」,否则无法满足不再跨机房的要求。 + +很显然,只让上海机房接收读流量的方案不现实,因为很少有项目是只有读流量,没有写流量的。所以这种方案还是不行,这怎么办? + +此时,你就必须在「**存储层**」做改造了。 + +要想上海机房读写本机房的存储,那上海机房的存储不能再是北京机房的从库,而是也要变为「主库」。 + +你没看错,两个机房的存储必须都是「**主库**」,而且两个机房的数据还要「**互相同步**」数据,即客户端无论写哪一个机房,都能把这条数据同步到另一个机房。 + +因为只有两个机房都拥有「全量数据」,才能支持任意切换机房,持续提供服务。 + +怎么实现这种「双主」架构呢?它们之间如何互相同步数据? + +如果你对 MySQL 有所了解,MySQL 本身就提供了双主架构,它支持双向复制数据,但平时用的并不多。而且 Redis、MongoDB 等数据库并没有提供这个功能,所以,你必须开发对应的「数据同步中间件」来实现双向同步的功能。 + +此外,除了数据库这种有状态的软件之外,你的项目通常还会使用到消息队列,例如 RabbitMQ、Kafka,这些也是有状态的服务,所以它们也需要开发双向同步的中间件,支持任意机房写入数据,同步至另一个机房。 + +看到了么,这一下子复杂度就上来了,单单针对每个数据库、队列开发同步中间件,就需要投入很大精力了。 + +> 业界也开源出了很多数据同步中间件,例如阿里的 Canal、RedisShake、MongoShake,可分别在两个机房同步 MySQL、Redis、MongoDB 数据。 +> +> 很多有能力的公司,也会采用自研同步中间件的方式来做,例如饿了么、携程、美团都开发了自己的同步中间件。 +> +> 我也有幸参与设计开发了 MySQL、Redis/Codis、MongoDB 的同步中间件,有时间写一篇文章详细聊聊实现细节,欢迎持续关注。:) + +现在,整个架构就变成了这样: + +![](http://img.topjavaer.cn/img/202306290850052.png) + +注意看,两个机房的存储层都互相同步数据的。有了数据同步中间件,就可以达到这样的效果: + +- 北京机房写入 X = 1 +- 上海机房写入 Y = 2 +- 数据通过中间件双向同步 +- 北京、上海机房都有 X = 1、Y = 2 的数据 + +这里我们用中间件双向同步数据,就不用再担心专线问题,专线出问题,我们的中间件可以自动重试,直到成功,达到数据最终一致。 + +但这里还会遇到一个问题,两个机房都可以写,操作的不是同一条数据那还好,如果修改的是同一条的数据,发生冲突怎么办? + +- 用户短时间内发了 2 个修改请求,都是修改同一条数据 +- 一个请求落在北京机房,修改 X = 1(还未同步到上海机房) +- 另一个请求落在上海机房,修改 X = 2(还未同步到北京机房) +- 两个机房以哪个为准? + +也就是说,在很短的时间内,同一个用户修改同一条数据,两个机房无法确认谁先谁后,数据发生「冲突」。 + +这是一个很严重的问题,系统发生故障并不可怕,可怕的是数据发生「错误」,因为修正数据的成本太高了。我们一定要避免这种情况的发生。解决这个问题,有 2 个方案。 + +**第一个方案**,数据同步中间件要有自动「合并」数据、解决「冲突」的能力。 + +这个方案实现起来比较复杂,要想合并数据,就必须要区分出「先后」顺序。我们很容易想到的方案,就是以「时间」为标尺,以「后到达」的请求为准。 + +但这种方案需要两个机房的「时钟」严格保持一致才行,否则很容易出现问题。例如: + +- 第 1 个请求落到北京机房,北京机房时钟是 10:01,修改 X = 1 +- 第 2 个请求落到上海机房,上海机房时钟是 10:00,修改 X = 2 + +因为北京机房的时间「更晚」,那最终结果就会是 X = 1。但这里其实应该以第 2 个请求为准,X = 2 才对。 + +可见,完全「依赖」时钟的冲突解决方案,不太严谨。 + +所以,通常会采用第二种方案,从「源头」就避免数据冲突的发生。 + +# 10 如何实施异地双活 + +既然自动合并数据的方案实现成本高,那我们就要想,能否从源头就「避免」数据冲突呢? + +这个思路非常棒! + +从源头避免数据冲突的思路是:**在最上层接入流量时,就不要让冲突的情况发生**。 + +具体来讲就是,要在最上层就把用户「区分」开,部分用户请求固定打到北京机房,其它用户请求固定打到上海 机房,进入某个机房的用户请求,之后的所有业务操作,都在这一个机房内完成,从根源上避免「跨机房」。 + +所以这时,你需要在接入层之上,再部署一个「路由层」(通常部署在云服务器上),自己可以配置路由规则,把用户「分流」到不同的机房内。 + +![](http://img.topjavaer.cn/img/202306290851907.png) + +但这个路由规则,具体怎么定呢?有很多种实现方式,最常见的我总结了 3 类: + +1. 按业务类型分片 +2. 直接哈希分片 +3. 按地理位置分片 + +**1、按业务类型分片** + +这种方案是指,按应用的「业务类型」来划分。 + +举例:假设我们一共有 4 个应用,北京和上海机房都部署这些应用。但应用 1、2 只在北京机房接入流量,在上海机房只是热备。应用 3、4 只在上海机房接入流量,在北京机房是热备。 + +这样一来,应用 1、2 的所有业务请求,只读写北京机房存储,应用 3、4 的所有请求,只会读写上海机房存储。 + +![](http://img.topjavaer.cn/img/202306290851532.png) + +这样按业务类型分片,也可以避免同一个用户修改同一条数据。 + +> 这里按业务类型在不同机房接入流量,还需要考虑多个应用之间的依赖关系,要尽可能的把完成「相关」业务的应用部署在同一个机房,避免跨机房调用。 +> +> 例如,订单、支付服务有依赖关系,会产生互相调用,那这 2 个服务在 A 机房接入流量。社区、发帖服务有依赖关系,那这 2 个服务在 B 机房接入流量。 + +**2、直接哈希分片** + +这种方案就是,最上层的路由层,会根据用户 ID 计算「哈希」取模,然后从路由表中找到对应的机房,之后把请求转发到指定机房内。 + +举例:一共 200 个用户,根据用户 ID 计算哈希值,然后根据路由规则,把用户 1 - 100 路由到北京机房,101 - 200 用户路由到上海机房,这样,就避免了同一个用户修改同一条数据的情况发生。 + +![](http://img.topjavaer.cn/img/202306290851605.png) + +**3、按地理位置分片** + +这种方案,非常适合与地理位置密切相关的业务,例如打车、外卖服务就非常适合这种方案。 + +拿外卖服务举例,你要点外卖肯定是「就近」点餐,整个业务范围相关的有商家、用户、骑手,它们都是在相同的地理位置内的。 + +针对这种特征,就可以在最上层,按用户的「地理位置」来做分片,分散到不同的机房。 + +举例:北京、河北地区的用户点餐,请求只会打到北京机房,而上海、浙江地区的用户,请求则只会打到上海机房。这样的分片规则,也能避免数据冲突。 + +![](http://img.topjavaer.cn/img/202306290851996.png) + +> 提醒:这 3 种常见的分片规则,第一次看不太好理解,建议配合图多理解几遍。搞懂这 3 个分片规则,你才能真正明白怎么做异地多活。 + +总之,分片的核心思路在于,**让同一个用户的相关请求,只在一个机房内完成所有业务「闭环」,不再出现「跨机房」访问**。 + +阿里在实施这种方案时,给它起了个名字,叫做「**单元化**」。 + +> 当然,最上层的路由层把用户分片后,理论来说同一个用户只会落在同一个机房内,但不排除程序 Bug 导致用户会在两个机房「漂移」。 +> +> 安全起见,每个机房在写存储时,还需要有一套机制,能够检测「数据归属」,应用层操作存储时,需要通过中间件来做「兜底」,避免不该写本机房的情况发生。(篇幅限制,这里不展开讲,理解思路即可) + +现在,两个机房就可以都接收「读写」流量(做好分片的请求),底层存储保持「双向」同步,两个机房都拥有全量数据,当任意机房故障时,另一个机房就可以「接管」全部流量,实现快速切换,简直不要太爽。 + +不仅如此,因为机房部署在异地,我们还可以更细化地「优化」路由规则,让用户访问就近的机房,这样整个系统的性能也会大大提升。 + +> 这里还有一种情况,是无法做数据分片的:**全局数据**。例如系统配置、商品库存这类需要强一致的数据,这类服务依旧只能采用写主机房,读从机房的方案,不做双活。 +> +> 双活的重点,是要优先保证「核心」业务先实现双活,并不是「全部」业务实现双活。 + +至此,我们才算实现了真正的「**异地双活**」! + +> 到这里你可以看出,完成这样一套架构,需要投入的成本是巨大的。 +> +> 路由规则、路由转发、数据同步中间件、数据校验兜底策略,不仅需要开发强大的中间件,同时还要业务配合改造(业务边界划分、依赖拆分)等一些列工作,没有足够的人力物力,这套架构很难实施。 + +# 11 异地多活 + +理解了异地双活,那「异地多活」顾名思义,就是在异地双活的基础上,部署多个机房即可。架构变成了这样: + +![](http://img.topjavaer.cn/img/202306290852204.png) + +这些服务按照「单元化」的部署方式,可以让每个机房部署在任意地区,随时扩展新机房,你只需要在最上层定义好分片规则就好了。 + +但这里还有一个小问题,随着扩展的机房越来越多,当一个机房写入数据后,需要同步的机房也越来越多,这个实现复杂度会比较高。 + +所以业界又把这一架构又做了进一步优化,把「网状」架构升级为「星状」: + +![](http://img.topjavaer.cn/img/202306290852633.png) + +这种方案必须设立一个「中心机房」,任意机房写入数据后,都只同步到中心机房,再由中心机房同步至其它机房。 + +这样做的好处是,一个机房写入数据,只需要同步数据到中心机房即可,不需要再关心一共部署了多少个机房,实现复杂度大大「简化」。 + +但与此同时,这个中心机房的「稳定性」要求会比较高。不过也还好,即使中心机房发生故障,我们也可以把任意一个机房,提升为中心机房,继续按照之前的架构提供服务。 + +至此,我们的系统彻底实现了「**异地多活**」! + +多活的优势在于,**可以任意扩展机房「就近」部署。任意机房发生故障,可以完成快速「切换」**,大大提高了系统的可用性。 + +同时,我们也再也不用担心系统规模的增长,因为这套架构具有极强的「**扩展能力**」。 + +怎么样?我们从一个最简单的应用,一路优化下来,到最终的架构方案,有没有帮你彻底理解异地多活呢? + +# 总结 + +好了,总结一下这篇文章的重点。 + +1、一个好的软件架构,应该遵循高性能、高可用、易扩展 3 大原则,其中「高可用」在系统规模变得越来越大时,变得尤为重要 + +2、系统发生故障并不可怕,能以「最快」的速度恢复,才是高可用追求的目标,异地多活是实现高可用的有效手段 + +3、提升高可用的核心是「冗余」,备份、主从副本、同城灾备、同城双活、两地三中心、异地双活,异地多活都是在做冗余 + +4、同城灾备分为「冷备」和「热备」,冷备只备份数据,不提供服务,热备实时同步数据,并做好随时切换的准备 + +5、同城双活比灾备的优势在于,两个机房都可以接入「读写」流量,提高可用性的同时,还提升了系统性能。虽然物理上是两个机房,但「逻辑」上还是当做一个机房来用 + +6、两地三中心是在同城双活的基础上,额外部署一个异地机房做「灾备」,用来抵御「城市」级别的灾害,但启用灾备机房需要时间 + +7、异地双活才是抵御「城市」级别灾害的更好方案,两个机房同时提供服务,故障随时可切换,可用性高。但实现也最复杂,理解了异地双活,才能彻底理解异地多活 + +8、异地多活是在异地双活的基础上,任意扩展多个机房,不仅又提高了可用性,还能应对更大规模的流量的压力,扩展性最强,是实现高可用的最终方案 + +# 后记 + +这篇文章我从「宏观」层面,向你介绍了异地多活架构的「核心」思路,整篇文章的信息量还是很大的,如果不太好理解,我建议你多读几遍。 + +因为篇幅限制,很多细节我并没有展开来讲。这篇文章更像是讲异地多活的架构之「道」,而真正实施的「术」,要考虑的点其实也非常繁多,因为它需要开发强大的「基础设施」才可以完成实施。 + +不仅如此,要想真正实现异地多活,还需要遵循一些原则,例如业务梳理、业务分级、数据分类、数据最终一致性保障、机房切换一致性保障、异常处理等等。同时,相关的运维设施、监控体系也要能跟得上才行。 + +宏观上需要考虑业务(微服务部署、依赖、拆分、SDK、Web 框架)、基础设施(服务发现、流量调度、持续集成、同步中间件、自研存储),微观上要开发各种中间件,还要关注中间件的高性能、高可用、容错能力,其复杂度之高,只有亲身参与过之后才知道。 + +我曾经有幸参与过,存储层同步中间件的设计与开发,实现过「跨机房」同步 MySQL、Redis、MongoDB 的中间件,踩过的坑也非常多。当然,这些中间件的设计思路也非常有意思,有时间单独分享一下这些中间件的设计思路。 + +值得提醒你的是,只有真正理解了「异地双活」,才能彻底理解「异地多活」。在我看来,从同城双活演变为异地双活的过程,是最为复杂的,最核心的东西包括,**业务单元化划分、存储层数据双向同步、最上层的分片逻辑**,这些是实现异地多活的重中之重。 + diff --git a/docs/advance/excellent-article/31-mysql-data-sync-es.md b/docs/advance/excellent-article/31-mysql-data-sync-es.md new file mode 100644 index 0000000..8d43d94 --- /dev/null +++ b/docs/advance/excellent-article/31-mysql-data-sync-es.md @@ -0,0 +1,503 @@ +--- +sidebar: heading +title: MySQL数据如何实时同步到ES +category: 优质文章 +tag: + - MySQL +head: + - - meta + - name: keywords + content: MySQL,ES,elasticsearch,数据同步 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + + +## 前言 + +我们一般会使用MySQL用来存储数据,用Es来做全文检索和特殊查询,那么如何将数据优雅的从MySQL同步到Es呢?我们一般有以下几种方式: + +1.**双写**。在代码中先向MySQL中写入数据,然后紧接着向Es中写入数据。这个方法的缺点是代码严重耦合,需要手动维护MySQL和Es数据关系,非常不便于维护。 + +2.**发MQ,异步执行**。在执行完向Mysql中写入数据的逻辑后,发送MQ,告诉消费端这个数据需要写入Es,消费端收到消息后执行向Es写入数据的逻辑。这个方式的优点是Mysql和Es数据维护分离,开发Mysql和Es的人员只需要关心各自的业务。缺点是依然需要维护发送、接收MQ的逻辑,并且引入了MQ组件,增加了系统的复杂度。 + +3.**使用Datax进行全量数据同步**。这个方式优点是可以完全不用写维护数据关系的代码,各自只需要关心自己的业务,对代码侵入性几乎为零。缺点是Datax是一种全量同步数据的方式,不使用实时同步。如果系统对数据时效性不强,可以考虑此方式。 + +4.**使用Canal进行实时数据同步**。这个方式具有跟Datax一样的优点,可以完全不用写维护数据关系的代码,各自只需要关心自己的业务,对代码侵入性几乎为零。与Datax不同的是Canal是一种实时同步数据的方式,对数据时效性较强的系统,我们会采用Canal来进行实时数据同步。 + +那么就让我们来看看Canal是如何使用的。 + +## 官网 + +https://github.com/alibaba/canal + +## 1.Canal简介 + +![](http://img.topjavaer.cn/img/202306262359509.png) + +**canal [kə'næl]** ,译意为水道/管道/沟渠,主要用途是基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费 + +早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。 + +基于日志增量订阅和消费的业务包括 + +- 数据库镜像 +- 数据库实时备份 +- 索引构建和实时维护(拆分异构索引、倒排索引等) +- 业务 cache 刷新 +- 带业务逻辑的增量数据处理 + +当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x + +### **MySQL主备复制原理** + +![](http://img.topjavaer.cn/img/202306262359485.png) + +- MySQL master 将数据变更写入二进制日志( binary log, 其中记录叫做二进制日志事件binary log events,可以通过 show binlog events 进行查看) +- MySQL slave 将 master 的 binary log events 拷贝到它的中继日志(relay log) +- MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据 + +### **canal工作原理** + +- canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议 +- MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ) +- canal 解析 binary log 对象(原始为 byte 流) + +## 2.开启MySQL Binlog + +- 对于自建 MySQL , 需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下 + +```ini +[mysqld] +log-bin=mysql-bin # 开启 binlog +binlog-format=ROW # 选择 ROW 模式 +server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复 +lua +## 复制代码注意:针对阿里云 RDS for MySQL , 默认打开了 binlog , 并且账号默认具有 binlog dump 权限 , 不需要任何权限或者 binlog 设置,可以直接跳过这一步 +``` + +- 授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant + +```sql +CREATE USER canal IDENTIFIED BY 'canal'; +GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%'; +-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ; +FLUSH PRIVILEGES; +``` + +注意:Mysql版本为8.x时启动canal可能会出现“caching_sha2_password Auth failed”错误,这是因为8.x创建用户时默认的密码加密方式为**caching_sha2_password**,与canal的方式不一致,所以需要将canal用户的密码加密方式修改为**mysql_native_password** + +```sql +ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY 'canal'; #更新一下用户密码 +FLUSH PRIVILEGES; #刷新权限 +``` + +## 3.安装Canal + +### 3.1 下载Canal + +**点击下载地址,选择版本后点击canal.deployer文件下载** + +![](http://img.topjavaer.cn/img/202306262359063.png) + +### 3.2 修改配置文件 + +打开目录下conf/example/instance.properties文件,主要修改以下内容 + +```ini +## mysql serverId,不要和 mysql 的 server_id 重复 +canal.instance.mysql.slaveId = 10 +#position info,需要改成自己的数据库信息 +canal.instance.master.address = 127.0.0.1:3306 +#username/password,需要改成自己的数据库信息,与刚才添加的用户保持一致 +canal.instance.dbUsername = canal +canal.instance.dbPassword = canal +``` + +### 3.3 启动和关闭 + +```bash +#进入文件目录下的bin文件夹 +#启动 +sh startup.sh +#关闭 +sh stop.sh +``` + +## 4.Springboot集成Canal + +### 4.1 Canal数据结构 + +![](http://img.topjavaer.cn/img/202306270000776.png) + +### 4.2 引入依赖 + +```xml + + + com.alibaba.otter + canal.client + 1.1.6 + + + + + + com.alibaba.otter + canal.protocol + 1.1.6 + + + + + co.elastic.clients + elasticsearch-java + 8.4.3 + + + + + jakarta.json + jakarta.json-api + 2.0.1 + +``` + +### 4.3 application.yaml + +```yaml +custom: + elasticsearch: + host: localhost #主机 + port: 9200 #端口 + username: elastic #用户名 + password: 3bf24a76 #密码 +``` + +### 4.4 EsClient + +```java +@Setter +@ConfigurationProperties(prefix = "custom.elasticsearch") +@Configuration +public class EsClient { + + /** + * 主机 + */ + private String host; + + /** + * 端口 + */ + private Integer port; + + /** + * 用户名 + */ + private String username; + + /** + * 密码 + */ + private String password; + + + @Bean + public ElasticsearchClient elasticsearchClient() { + CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); + credentialsProvider.setCredentials( + AuthScope.ANY, new UsernamePasswordCredentials(username, password)); + + // Create the low-level client + RestClient restClient = RestClient.builder(new HttpHost(host, port)) + .setHttpClientConfigCallback(httpAsyncClientBuilder -> + httpAsyncClientBuilder.setDefaultCredentialsProvider(credentialsProvider)) + .build(); + // Create the transport with a Jackson mapper + RestClientTransport transport = new RestClientTransport( + restClient, new JacksonJsonpMapper()); + // Create the transport with a Jackson mapper + return new ElasticsearchClient(transport); + } +} +``` + +### 4.5 Music实体类 + +```java +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Music { + + /** + * id + */ + private String id; + + /** + * 歌名 + */ + private String name; + + /** + * 歌手名 + */ + private String singer; + + /** + * 封面图地址 + */ + private String imageUrl; + + /** + * 歌曲地址 + */ + private String musicUrl; + + /** + * 歌词地址 + */ + private String lrcUrl; + + /** + * 歌曲类型id + */ + private String typeId; + + /** + * 是否被逻辑删除,1 是,0 否 + */ + private Integer isDeleted; + + /** + * 创建时间 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date createTime; + + /** + * 更新时间 + */ + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private Date updateTime; + +} +``` + +### 4.6 CanalClient + +```java +@Slf4j +@Component +public class CanalClient { + + @Resource + private ElasticsearchClient client; + + + /** + * 实时数据同步程序 + * + * @throws InterruptedException + * @throws InvalidProtocolBufferException + */ + public void run() throws InterruptedException, IOException { + CanalConnector connector = CanalConnectors.newSingleConnector(new InetSocketAddress( + "localhost", 11111), "example", "", ""); + + while (true) { + //连接 + connector.connect(); + //订阅数据库 + connector.subscribe("cloudmusic_music.music"); + //获取数据 + Message message = connector.get(100); + + List entryList = message.getEntries(); + if (CollectionUtils.isEmpty(entryList)) { + //没有数据,休息一会 + TimeUnit.SECONDS.sleep(2); + } else { + for (CanalEntry.Entry entry : entryList) { + //获取类型 + CanalEntry.EntryType entryType = entry.getEntryType(); + + //判断类型是否为ROWDATA + if (CanalEntry.EntryType.ROWDATA.equals(entryType)) { + //获取序列化后的数据 + ByteString storeValue = entry.getStoreValue(); + //反序列化数据 + CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(storeValue); + //获取当前事件操作类型 + CanalEntry.EventType eventType = rowChange.getEventType(); + //获取数据集 + List rowDataList = rowChange.getRowDatasList(); + + if (eventType == CanalEntry.EventType.INSERT) { + log.info("------新增操作------"); + + List musicList = new ArrayList<>(); + for (CanalEntry.RowData rowData : rowDataList) { + musicList.add(createMusic(rowData.getAfterColumnsList())); + } + //es批量新增文档 + index(musicList); + //打印新增集合 + log.info(Arrays.toString(musicList.toArray())); + } else if (eventType == CanalEntry.EventType.UPDATE) { + log.info("------更新操作------"); + + List beforeMusicList = new ArrayList<>(); + List afterMusicList = new ArrayList<>(); + for (CanalEntry.RowData rowData : rowDataList) { + //更新前 + beforeMusicList.add(createMusic(rowData.getBeforeColumnsList())); + //更新后 + afterMusicList.add(createMusic(rowData.getAfterColumnsList())); + } + //es批量更新文档 + index(afterMusicList); + //打印更新前集合 + log.info("更新前:{}", Arrays.toString(beforeMusicList.toArray())); + //打印更新后集合 + log.info("更新后:{}", Arrays.toString(afterMusicList.toArray())); + } else if (eventType == CanalEntry.EventType.DELETE) { + //删除操作 + log.info("------删除操作------"); + + List idList = new ArrayList<>(); + for (CanalEntry.RowData rowData : rowDataList) { + for (CanalEntry.Column column : rowData.getBeforeColumnsList()) { + if("id".equals(column.getName())) { + idList.add(column.getValue()); + break; + } + } + } + //es批量删除文档 + delete(idList); + //打印删除id集合 + log.info(Arrays.toString(idList.toArray())); + } + } + } + } + } + } + + /** + * 根据canal获取的数据创建Music对象 + * + * @param columnList + * @return + */ + private Music createMusic(List columnList) { + Music music = new Music(); + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + for (CanalEntry.Column column : columnList) { + switch (column.getName()) { + case "id" -> music.setId(column.getValue()); + case "name" -> music.setName(column.getValue()); + case "singer" -> music.setSinger(column.getValue()); + case "image_url" -> music.setImageUrl(column.getValue()); + case "music_url" -> music.setMusicUrl(column.getValue()); + case "lrc_url" -> music.setLrcUrl(column.getValue()); + case "type_id" -> music.setTypeId(column.getValue()); + case "is_deleted" -> music.setIsDeleted(Integer.valueOf(column.getValue())); + case "create_time" -> + music.setCreateTime(Date.from(LocalDateTime.parse(column.getValue(), formatter).atZone(ZoneId.systemDefault()).toInstant())); + case "update_time" -> + music.setUpdateTime(Date.from(LocalDateTime.parse(column.getValue(), formatter).atZone(ZoneId.systemDefault()).toInstant())); + default -> { + } + } + } + + return music; + } + + /** + * es批量新增、更新文档(不存在:新增, 存在:更新) + * + * @param musicList 音乐集合 + * @throws IOException + */ + private void index(List musicList) throws IOException { + BulkRequest.Builder br = new BulkRequest.Builder(); + + musicList.forEach(music -> br + .operations(op -> op + .index(idx -> idx + .index("music") + .id(music.getId()) + .document(music)))); + + client.bulk(br.build()); + } + + /** + * es批量删除文档 + * + * @param idList 音乐id集合 + * @throws IOException + */ + private void delete(List idList) throws IOException { + BulkRequest.Builder br = new BulkRequest.Builder(); + + idList.forEach(id -> br + .operations(op -> op + .delete(idx -> idx + .index("music") + .id(id)))); + + client.bulk(br.build()); + } + +} +``` + +### 4.7 ApplicationContextAware + +```java +@Component +public class ApplicationContextUtil implements ApplicationContextAware { + + private static ApplicationContext applicationContext; + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + ApplicationContextUtil.applicationContext = applicationContext; + } + + public static T getBean (Class classType) { + return applicationContext.getBean(classType); + } + +} +``` + +### 4.8 main + +```java +@Slf4j +@SpringBootApplication +public class CanalApplication { + public static void main(String[] args) throws InterruptedException, IOException { + SpringApplication.run(CanalApplication.class, args); + log.info("数据同步程序启动"); + + CanalClient client = ApplicationContextUtil.getBean(CanalClient.class); + client.run(); + } +} +``` + +## 5.总结 + +那么以上就是Canal组件的介绍啦,希望大家都能有所收获~ + diff --git a/docs/learn/ghelper.md b/docs/learn/ghelper.md index a640f63..f5de885 100644 --- a/docs/learn/ghelper.md +++ b/docs/learn/ghelper.md @@ -1,4 +1,17 @@ -# 科学上网教程 +--- +sidebar: heading +title: 科学上网教程 +category: 工具 +tag: + - 工具 +head: + - - meta + - name: keywords + content: ghelper + - - meta + - name: description + content: 提高工作效率的工具 +--- @@ -64,10 +77,16 @@ Ghelper可以在google play商店进行下载,需要访问google商店,无 -> 最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、架构、分布式、微服务、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ -![](http://img.topjavaer.cn/img/Image.png) -![](http://img.topjavaer.cn/img/image-20221030094126118.png) +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ -**Github地址**:https://github.com/Tyson0314/java-books \ No newline at end of file +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/note/docker-note.md b/docs/note/docker-note.md new file mode 100644 index 0000000..68a8f62 --- /dev/null +++ b/docs/note/docker-note.md @@ -0,0 +1,15 @@ +![](http://img.topjavaer.cn/img/202308020050981.png) + +![](http://img.topjavaer.cn/img/202308020050993.png) + +![](http://img.topjavaer.cn/img/202308020050859.png) + +![5](http://img.topjavaer.cn/img/202308020050614.png) + +![6](http://img.topjavaer.cn/img/202308020051473.png) + +![7](http://img.topjavaer.cn/img/202308020051939.png) + +![8](http://img.topjavaer.cn/img/202308020051588.png) + +![](http://img.topjavaer.cn/img/202308020853566.png) \ No newline at end of file diff --git a/docs/note/write-sql.md b/docs/note/write-sql.md new file mode 100644 index 0000000..14faa3a --- /dev/null +++ b/docs/note/write-sql.md @@ -0,0 +1,93 @@ +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 21个写SQL的好习惯 + +![](http://img.topjavaer.cn/img/202308030823049.png) + +![2](http://img.topjavaer.cn/img/202308030823148.png) + +![3](http://img.topjavaer.cn/img/202308030823229.png) + +![4](http://img.topjavaer.cn/img/202308030823834.png) + +![5](http://img.topjavaer.cn/img/202308030824651.png) + +![6](http://img.topjavaer.cn/img/202308030824461.png) + +![7](http://img.topjavaer.cn/img/202308030824900.png) + +![8](http://img.topjavaer.cn/img/202308030824354.png) + +![9](http://img.topjavaer.cn/img/202308030824755.png) + + + + + +## MySQL面试题 + +下面分享MySQL常考的**面试题目**。 + +- [事务的四大特性?](https://topjavaer.cn/database/mysql.html#%E4%BA%8B%E5%8A%A1%E7%9A%84%E5%9B%9B%E5%A4%A7%E7%89%B9%E6%80%A7) +- [数据库的三大范式](https://topjavaer.cn/database/mysql.html#%E6%95%B0%E6%8D%AE%E5%BA%93%E7%9A%84%E4%B8%89%E5%A4%A7%E8%8C%83%E5%BC%8F) +- [事务隔离级别有哪些?](https://topjavaer.cn/database/mysql.html#%E4%BA%8B%E5%8A%A1%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E6%9C%89%E5%93%AA%E4%BA%9B) +- [生产环境数据库一般用的什么隔离级别呢?](https://topjavaer.cn/database/mysql.html#%E7%94%9F%E4%BA%A7%E7%8E%AF%E5%A2%83%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%80%E8%88%AC%E7%94%A8%E7%9A%84%E4%BB%80%E4%B9%88%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E5%91%A2) +- [编码和字符集的关系](https://topjavaer.cn/database/mysql.html#%E7%BC%96%E7%A0%81%E5%92%8C%E5%AD%97%E7%AC%A6%E9%9B%86%E7%9A%84%E5%85%B3%E7%B3%BB) +- [utf8和utf8mb4的区别](https://topjavaer.cn/database/mysql.html#utf8%E5%92%8Cutf8mb4%E7%9A%84%E5%8C%BA%E5%88%AB) +- [什么是索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E7%B4%A2%E5%BC%95) +- [索引的优缺点?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BC%98%E7%BC%BA%E7%82%B9) +- [索引的作用?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BD%9C%E7%94%A8) +- [什么情况下需要建索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E9%9C%80%E8%A6%81%E5%BB%BA%E7%B4%A2%E5%BC%95) +- [什么情况下不建索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%83%85%E5%86%B5%E4%B8%8B%E4%B8%8D%E5%BB%BA%E7%B4%A2%E5%BC%95) +- [索引的数据结构](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84) +- [Hash索引和B+树索引的区别?](https://topjavaer.cn/database/mysql.html#hash%E7%B4%A2%E5%BC%95%E5%92%8Cb%E6%A0%91%E7%B4%A2%E5%BC%95%E7%9A%84%E5%8C%BA%E5%88%AB) +- [为什么B+树比B树更适合实现数据库索引?](https://topjavaer.cn/database/mysql.html#%E4%B8%BA%E4%BB%80%E4%B9%88b%E6%A0%91%E6%AF%94b%E6%A0%91%E6%9B%B4%E9%80%82%E5%90%88%E5%AE%9E%E7%8E%B0%E6%95%B0%E6%8D%AE%E5%BA%93%E7%B4%A2%E5%BC%95) +- [索引有什么分类?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E6%9C%89%E4%BB%80%E4%B9%88%E5%88%86%E7%B1%BB) +- [什么是最左匹配原则?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E6%9C%80%E5%B7%A6%E5%8C%B9%E9%85%8D%E5%8E%9F%E5%88%99) +- [什么是聚集索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%81%9A%E9%9B%86%E7%B4%A2%E5%BC%95) +- [什么是覆盖索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E8%A6%86%E7%9B%96%E7%B4%A2%E5%BC%95) +- [索引的设计原则?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E7%9A%84%E8%AE%BE%E8%AE%A1%E5%8E%9F%E5%88%99) +- [索引什么时候会失效?](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E4%BB%80%E4%B9%88%E6%97%B6%E5%80%99%E4%BC%9A%E5%A4%B1%E6%95%88) +- [什么是前缀索引?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E5%89%8D%E7%BC%80%E7%B4%A2%E5%BC%95) +- [索引下推](https://topjavaer.cn/database/mysql.html#%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8) +- [常见的存储引擎有哪些?](https://topjavaer.cn/database/mysql.html#%E5%B8%B8%E8%A7%81%E7%9A%84%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E%E6%9C%89%E5%93%AA%E4%BA%9B) +- [MyISAM和InnoDB的区别?](https://topjavaer.cn/database/mysql.html#myisam%E5%92%8Cinnodb%E7%9A%84%E5%8C%BA%E5%88%AB) +- [MySQL有哪些锁?](https://topjavaer.cn/database/mysql.html#mysql%E6%9C%89%E5%93%AA%E4%BA%9B%E9%94%81) +- [MVCC 实现原理?](https://topjavaer.cn/database/mysql.html#mvcc-%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86) +- [快照读和当前读](https://topjavaer.cn/database/mysql.html#%E5%BF%AB%E7%85%A7%E8%AF%BB%E5%92%8C%E5%BD%93%E5%89%8D%E8%AF%BB) +- [共享锁和排他锁](https://topjavaer.cn/database/mysql.html#%E5%85%B1%E4%BA%AB%E9%94%81%E5%92%8C%E6%8E%92%E4%BB%96%E9%94%81) +- [bin log/redo log/undo log](https://topjavaer.cn/database/mysql.html#bin-logredo-logundo-log) +- [bin log和redo log有什么区别?](https://topjavaer.cn/database/mysql.html#bin-log%E5%92%8Credo-log%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) +- [讲一下MySQL架构?](https://topjavaer.cn/database/mysql.html#%E8%AE%B2%E4%B8%80%E4%B8%8Bmysql%E6%9E%B6%E6%9E%84) +- [分库分表](https://topjavaer.cn/database/mysql.html#%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8) +- [什么是分区表?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AF%E5%88%86%E5%8C%BA%E8%A1%A8) +- [分区表类型](https://topjavaer.cn/database/mysql.html#%E5%88%86%E5%8C%BA%E8%A1%A8%E7%B1%BB%E5%9E%8B) +- [分区的问题?](https://topjavaer.cn/database/mysql.html#%E5%88%86%E5%8C%BA%E7%9A%84%E9%97%AE%E9%A2%98) +- [查询语句执行流程?](https://topjavaer.cn/database/mysql.html#%E6%9F%A5%E8%AF%A2%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B) +- [更新语句执行过程?](https://topjavaer.cn/database/mysql.html#%E6%9B%B4%E6%96%B0%E8%AF%AD%E5%8F%A5%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B) +- [exist和in的区别?](https://topjavaer.cn/database/mysql.html#exist%E5%92%8Cin%E7%9A%84%E5%8C%BA%E5%88%AB) +- [MySQL中int()和char()的区别?](https://topjavaer.cn/database/mysql.html#mysql%E4%B8%ADint10%E5%92%8Cchar10%E7%9A%84%E5%8C%BA%E5%88%AB) +- [truncate、delete与drop区别?](https://topjavaer.cn/database/mysql.html#truncatedelete%E4%B8%8Edrop%E5%8C%BA%E5%88%AB) +- [having和where区别?](https://topjavaer.cn/database/mysql.html#having%E5%92%8Cwhere%E5%8C%BA%E5%88%AB) +- [什么是MySQL主从同步?](https://topjavaer.cn/database/mysql.html#%E4%BB%80%E4%B9%88%E6%98%AFmysql%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5) +- [为什么要做主从同步?](https://topjavaer.cn/database/mysql.html#%E4%B8%BA%E4%BB%80%E4%B9%88%E8%A6%81%E5%81%9A%E4%B8%BB%E4%BB%8E%E5%90%8C%E6%AD%A5) +- [乐观锁和悲观锁是什么?](https://topjavaer.cn/database/mysql.html#%E4%B9%90%E8%A7%82%E9%94%81%E5%92%8C%E6%82%B2%E8%A7%82%E9%94%81%E6%98%AF%E4%BB%80%E4%B9%88) +- [用过processlist吗?](https://topjavaer.cn/database/mysql.html#%E7%94%A8%E8%BF%87processlist%E5%90%97) +- [MySQL查询 limit 1000,10 和limit 10 速度一样快吗?](https://topjavaer.cn/database/mysql.html#mysql%E6%9F%A5%E8%AF%A2-limit-100010-%E5%92%8Climit-10-%E9%80%9F%E5%BA%A6%E4%B8%80%E6%A0%B7%E5%BF%AB%E5%90%97) +- [深分页怎么优化?](https://topjavaer.cn/database/mysql.html#%E6%B7%B1%E5%88%86%E9%A1%B5%E6%80%8E%E4%B9%88%E4%BC%98%E5%8C%96) +- [高度为3的B+树,可以存放多少数据?](https://topjavaer.cn/database/mysql.html#%E9%AB%98%E5%BA%A6%E4%B8%BA3%E7%9A%84b%E6%A0%91%E5%8F%AF%E4%BB%A5%E5%AD%98%E6%94%BE%E5%A4%9A%E5%B0%91%E6%95%B0%E6%8D%AE) +- [MySQL单表多大进行分库分表?](https://topjavaer.cn/database/mysql.html#mysql%E5%8D%95%E8%A1%A8%E5%A4%9A%E5%A4%A7%E8%BF%9B%E8%A1%8C%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8) +- [大表查询慢怎么优化?](https://topjavaer.cn/database/mysql.html#%E5%A4%A7%E8%A1%A8%E6%9F%A5%E8%AF%A2%E6%85%A2%E6%80%8E%E4%B9%88%E4%BC%98%E5%8C%96) +- [说说count()、count()和count()的区别](https://topjavaer.cn/database/mysql.html#%E8%AF%B4%E8%AF%B4count1count%E5%92%8Ccount%E5%AD%97%E6%AE%B5%E5%90%8D%E7%9A%84%E5%8C%BA%E5%88%AB) +- [MySQL中DATETIME 和 TIMESTAMP有什么区别?](https://topjavaer.cn/database/mysql.html#mysql%E4%B8%ADdatetime-%E5%92%8C-timestamp%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB) +- [说说为什么不建议用外键?](https://topjavaer.cn/database/mysql.html#%E8%AF%B4%E8%AF%B4%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E5%BB%BA%E8%AE%AE%E7%94%A8%E5%A4%96%E9%94%AE) +- [使用自增主键有什么好处?](https://topjavaer.cn/database/mysql.html#%E4%BD%BF%E7%94%A8%E8%87%AA%E5%A2%9E%E4%B8%BB%E9%94%AE%E6%9C%89%E4%BB%80%E4%B9%88%E5%A5%BD%E5%A4%84) +- [自增主键保存在什么地方?](https://topjavaer.cn/database/mysql.html#%E8%87%AA%E5%A2%9E%E4%B8%BB%E9%94%AE%E4%BF%9D%E5%AD%98%E5%9C%A8%E4%BB%80%E4%B9%88%E5%9C%B0%E6%96%B9) +- [自增主键一定是连续的吗?](https://topjavaer.cn/database/mysql.html#%E8%87%AA%E5%A2%9E%E4%B8%BB%E9%94%AE%E4%B8%80%E5%AE%9A%E6%98%AF%E8%BF%9E%E7%BB%AD%E7%9A%84%E5%90%97) +- [InnoDB的自增值为什么不能回收利用?](https://topjavaer.cn/database/mysql.html#innodb%E7%9A%84%E8%87%AA%E5%A2%9E%E5%80%BC%E4%B8%BA%E4%BB%80%E4%B9%88%E4%B8%8D%E8%83%BD%E5%9B%9E%E6%94%B6%E5%88%A9%E7%94%A8) +- [MySQL数据如何同步到Redis缓存?](https://topjavaer.cn/database/mysql.html#mysql%E6%95%B0%E6%8D%AE%E5%A6%82%E4%BD%95%E5%90%8C%E6%AD%A5%E5%88%B0redis%E7%BC%93%E5%AD%98) \ No newline at end of file diff --git a/docs/zsxq/article/select-max-rows.md b/docs/zsxq/article/select-max-rows.md new file mode 100644 index 0000000..9ffabc2 --- /dev/null +++ b/docs/zsxq/article/select-max-rows.md @@ -0,0 +1,225 @@ +## 问题 + +一条这样的 SQL 语句能查询出多少条记录? + +```SQL +select * from user +``` + +表中有 100 条记录的时候能全部查询出来返回给客户端吗? + +如果记录数是 1w 呢? 10w 呢? 100w 、1000w 呢? + +虽然在实际业务操作中我们不会这么干,尤其对于数据量大的表不会这样干,但这是个值得想一想的问题。 + +## 寻找答案 + +前提:以下所涉及资料全部基于 MySQL 8 + +### max_allowed_packet + +在查询资料的过程中发现了这个参数 `max_allowed_packet` + +![](http://img.topjavaer.cn/img/202307231140420.png) + +上图参考了 MySQL 的官方文档,根据文档我们知道: + +- MySQL 客户端 `max_allowed_packet` 值的默认大小为 16M(不同的客户端可能有不同的默认值,但最大不能超过 1G) +- MySQL 服务端 `max_allowed_packet` 值的默认大小为 64M +- `max_allowed_packet` 值最大可以设置为 1G(1024 的倍数) + +然而 根据上图的文档中所述 + +> The maximum size of one packet or any generated/intermediate string,or any parameter sent by the mysql_smt_send_long_data() C API function + +- one packet +- generated/intermediate string +- any parameter sent by the mysql_smt_send_long_data() C API function + +这三个东东具体都是什么呢? `packet` 到底是结果集大小,还是网络包大小还是什么? 于是 google 了一下,搜索排名第一的是这个: + +![](http://img.topjavaer.cn/img/202307231140408.png) + +根据 “Packet Too Large” 的说明, 通信包 (communication packet) 是 + +- 一个被发送到 MySQL 服务器的单个 SQL 语句 +- 或者是一个被发送到客户端的**单行记录** +- 或者是一个从主服务器 (replication source server) 被发送到从属服务器 (replica) 的二进制日志事件。 + +1、3 点好理解,这也同时解释了,如果你发送的一条 SQL 语句特别大可能会执行不成功的原因,尤其是`insert` `update` 这种,单个 SQL 语句不是没有上限的,不过这种情况一般不是因为 SQL 语句写的太长,主要是由于某个字段的值过大,比如有 BLOB 字段。 + +那么第 2 点呢,单行记录,默认值是 64M,会不会太大了啊,一行记录有可能这么大的吗? 有必要设置这么大吗? 单行最大存储空间限制又是多少呢? + +### 单行最大存储空间 + +MySQL 单行最大宽度是 65535 个字节,也就是 64KB 。无论是 InnoDB 引擎还是 MyISAM 引擎。 + +![](http://img.topjavaer.cn/img/202307231141021.png) + +通过上图可以看到 超过 65535 不行,不过请注意其中的错误提示:“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535” ,如果字段是变长类型的如 BLOB 和 TEXT 就不包括了,那么我们试一下用和上图一样的字段长度,只把最后一个字段的类型改成 BLOB 和 TEXT + +```SQL +mysql> CREATE TABLE t (a VARCHAR(10000), b VARCHAR(10000), + c VARCHAR(10000), d VARCHAR(10000), e VARCHAR(10000), + f VARCHAR(10000), g TEXT(6000)) ENGINE=InnoDB CHARACTER SET latin1; +Query OK, 0 rows affected (0.02 sec) +``` + +可见无论 是改成 BLOB 还是 TEXT 都可以成功。但这里请注意,字符集是 `latin1` 可以成功,如果换成 `utf8mb4` 或者 `utf8mb3` 就不行了,会报错,仍然是 :“Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535.” 为什么呢? + +**因为虽然不包括 TEXT 和 BLOB, 但总长度还是超了!** + +我们先看一下这个熟悉的 VARCHAR(255) , 你有没有想过为什么用 255,不用 256? + +> 在 4.0 版本以下,varchar(255) 指的是 255 个字节,使用 1 个字节存储长度即可。当大于等于 256 时,要使用 2 个字节存储长度。所以定义 varchar(255) 比 varchar(256) 更好。 +> +> 但是在 5.0 版本以上,varchar(255) 指的是 255 个字符,每个字符可能占用多个字节,例如使用 UTF8 编码时每个汉字占用 3 字节,使用 GBK 编码时每个汉字占 2 字节。 + +例子中我们用的是 MySQL8 ,由于字符集是 utf8mb3 ,存储一个字要用三个字节, 长度为 255 的话(列宽),总长度要 765 字节 ,再加上用 2 个字节存储长度,那么这个列的总长度就是 767 字节。所以用 latin1 可以成功,是因为一个字符对应一个字节,而 utf8mb3 或 utf8mb4 一个字符对应三个或四个字节,VARCHAR(10000) 就可能等于要占用 30000 多 40000 多字节,比原来大了 3、4 倍,肯定放不下了。 + +**另外,还有一个要求**,列的宽度不要超过 MySQL 页大小 (默认 16K)的一半,要比一半小一点儿。 例如,对于默认的 16KB `InnoDB` 页面大小,最大行大小略小于 8KB。 + +下面这个例子就是超过了一半,所以报错,当然解决办法也在提示中给出了。 + +```SQL +mysql> CREATE TABLE t4 ( + c1 CHAR(255),c2 CHAR(255),c3 CHAR(255), + c4 CHAR(255),c5 CHAR(255),c6 CHAR(255), + c7 CHAR(255),c8 CHAR(255),c9 CHAR(255), + c10 CHAR(255),c11 CHAR(255),c12 CHAR(255), + c13 CHAR(255),c14 CHAR(255),c15 CHAR(255), + c16 CHAR(255),c17 CHAR(255),c18 CHAR(255), + c19 CHAR(255),c20 CHAR(255),c21 CHAR(255), + c22 CHAR(255),c23 CHAR(255),c24 CHAR(255), + c25 CHAR(255),c26 CHAR(255),c27 CHAR(255), + c28 CHAR(255),c29 CHAR(255),c30 CHAR(255), + c31 CHAR(255),c32 CHAR(255),c33 CHAR(255) + ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1; +ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB may help. +In current row format, BLOB prefix of 0 bytes is stored inline. +``` + +**那么为什么是 8K,不是 7K,也不是 9K 呢?** 这么设计的原因可能是:MySQL 想让一个数据页中能存放更多的数据行,至少也得要存放两行数据(16K)。否则就失去了 B+Tree 的意义。B+Tree 会退化成一个低效的链表。 + +**你可能还会奇怪,不超过 8K ?你前面的例子明明都快 64K 也能存下,那 8K 到 64K 中间这部分怎么解释?** + +答:如果包含可变长度列的行超过 `InnoDB` 最大行大小, `InnoDB` 会选择可变长度列进行页外存储,直到该行适合 `InnoDB` ,这也就是为什么前面有超过 8K 的也能成功,那是因为用的是`VARCHAR`这种可变长度类型。 + +![](http://img.topjavaer.cn/img/202307231141870.png) + +当你往这个数据页中写入一行数据时,即使它很大将达到了数据页的极限,但是通过行溢出机制。依然能保证你的下一条数据还能写入到这个数据页中。 + +**我们通过 Compact 格式,简单了解一下什么是 `页外存储` 和 `行溢出`** + +MySQL8 InnoDB 引擎目前有 4 种 行记录格式: + +- REDUNDANT +- COMPACT +- DYNAMIC(默认 default 是这个) +- COMPRESSED + +`行记录格式` 决定了其行的物理存储方式,这反过来又会影响查询和 DML 操作的性能。 + +![](http://img.topjavaer.cn/img/202307231141440.png) + +Compact 格式的实现思路是:当列的类型为 VARCHAR、 VARBINARY、 BLOB、TEXT 时,该列超过 768byte 的数据放到其他数据页中去。 + +![](http://img.topjavaer.cn/img/202307231141558.png) + +在 MySQL 设定中,当 varchar 列长度达到 768byte 后,会将该列的前 768byte 当作当作 prefix 存放在行中,多出来的数据溢出存放到溢出页中,然后通过一个偏移量指针将两者关联起来,这就是 `行溢出`机制 + +> **假如你要存储的数据行很大超过了 65532byte 那么你是写入不进去的。假如你要存储的单行数据小于 65535byte 但是大于 16384byte,这时你可以成功 insert,但是一个数据页又存储不了你插入的数据。这时肯定会行溢出!** + +MySQL 这样做,有效的防止了单个 varchar 列或者 Text 列太大导致单个数据页中存放的行记录过少的情况,避免了 IO 飙升的窘境。 + +### 单行最大列数限制 + +**mysql 单表最大列数也是有限制的,是 4096 ,但 InnoDB 是 1017** + +![](http://img.topjavaer.cn/img/202307231141116.png) + +### 实验 + +前文中我们疑惑 `max_allowed_packet` 在 MySQL8 的默认值是 64M,又说这是限制单行数据的,单行数据有这么大吗? 在前文我们介绍了行溢出, 由于有了 `行溢出` ,单行数据确实有可能比较大。 + +那么还剩下一个问题,`max_allowed_packet` 限制的确定是单行数据吗,难道不是查询结果集的大小吗 ? 下面我们做个实验,验证一下。 + +建表 + +```SQL +CREATE TABLE t1 ( + c1 CHAR(255),c2 CHAR(255),c3 CHAR(255), + c4 CHAR(255),c5 CHAR(255),c6 CHAR(255), + c7 CHAR(255),c8 CHAR(255),c9 CHAR(255), + c10 CHAR(255),c11 CHAR(255),c12 CHAR(255), + c13 CHAR(255),c14 CHAR(255),c15 CHAR(255), + c16 CHAR(255),c17 CHAR(255),c18 CHAR(255), + c19 CHAR(255),c20 CHAR(255),c21 CHAR(255), + c22 CHAR(255),c23 CHAR(255),c24 CHAR(255), + c25 CHAR(255),c26 CHAR(255),c27 CHAR(255), + c28 CHAR(255),c29 CHAR(255),c30 CHAR(255), + c31 CHAR(255),c32 CHAR(192) + ) ENGINE=InnoDB ROW_FORMAT=DYNAMIC DEFAULT CHARSET latin1; +``` + +经过测试虽然提示的是 `Row size too large (> 8126)` 但如果全部长度加起来是 8126 建表不成功,最终我试到 8097 是能建表成功的。为什么不是 8126 呢 ?可能是还需要存储一些其他的东西占了一些字节吧,比如隐藏字段什么的。 + +用存储过程造一些测试数据,把表中的所有列填满 + +```SQL +create + definer = root@`%` procedure generate_test_data() +BEGIN + DECLARE i INT DEFAULT 0; + DECLARE col_value TEXT DEFAULT REPEAT('a', 255); + WHILE i < 5 DO + INSERT INTO t1 VALUES + ( + col_value, col_value, col_value, + col_value, REPEAT('b', 192) + ); + SET i = i + 1; + END WHILE; +END; +``` + +将 `max_allowed_packet` 设置的小一些,先用 `show VARIABLES like '%max_allowed_packet%';` 看一下当前的大小,我的是 `67108864` 这个单位是字节,等于 64M,然后用 `set global max_allowed_packet =1024` 将它设置成允许的最小值 1024 byte。 设置好后,关闭当前查询窗口再新建一个,然后再查看: + +![](http://img.topjavaer.cn/img/202307231141361.png) + +这时我用 `select * from t1;` 查询表数据时就会报错: + +![](http://img.topjavaer.cn/img/202307231142956.png) + +因为我们一条记录的大小就是 8K 多了,所以肯定超过 1024byte。可见文档的说明是对的, `max_allowed_packet` 确实是可以约束单行记录大小的。 + +## 答案 + +文章写到这里,我有点儿写不下去了,一是因为懒,另外一个原因是关于这个问题:“一条 SQL 最多能查询出来多少条记录?” 肯定没有标准答案 + +目前我们可以知道的是: + +- 你的单行记录大小不能超过 `max_allowed_packet` +- 一个表最多可以创建 1017 列 (InnoDB) +- 建表时定义列的固定长度不能超过 页的一半(8k,16k...) +- 建表时定义列的总长度不能超过 65535 个字节 + +如果这些条件我们都满足了,然后发出了一个没有 where 条件的全表查询 `select *` 那么..... + +首先,你我都知道,这种情况不会发生在生产环境的,如果真发生了,一定是你写错了,忘了加条件。因为几乎没有这种要查询出所有数据的需求。如果有,也不能开发,因为这不合理。 + +我考虑的也就是个理论情况,从理论上讲能查询出多少数据不是一个确定的值,除了前文提到的一些条件外,它肯定与以下几项有直接的关系 + +- 数据库的可用内存 +- 数据库内部的缓存机制,比如缓存区的大小 +- 数据库的查询超时机制 +- 应用的可用物理内存 +- ...... + +说到这儿,我确实可以再做个实验验证一下,但因为懒就不做了,大家有兴趣可以自己设定一些条件做个实验试一下,比如在特定内存和特定参数的情况下,到底能查询出多少数据,就能看得出来了。 + +虽然我没能给出文章开头问题的答案,但通过寻找答案也弄清楚了 MySQL 的一些限制条件,并加以了验证,也算是有所收获了。 + + + +**参考**链接:https://juejin.cn/post/7255478273652834360 diff --git a/docs/zsxq/article/sql-optimize.md b/docs/zsxq/article/sql-optimize.md new file mode 100644 index 0000000..952c970 --- /dev/null +++ b/docs/zsxq/article/sql-optimize.md @@ -0,0 +1,196 @@ +在应用开发的早期,数据量少,开发人员开发功能时更重视功能上的实现,随着生产数据的增长,很多SQL语句开始暴露出性能问题,对生产的影响也越来越大,有时可能这些有问题的SQL就是整个系统性能的瓶颈。 + +## SQL优化一般步骤 + +#### 1、通过慢查日志等定位那些执行效率较低的SQL语句 + +#### 2、explain 分析SQL的执行计划 + +需要重点关注type、rows、filtered、extra。 + +type由上至下,效率越来越高 + +- ALL 全表扫描 +- index 索引全扫描 +- range 索引范围扫描,常用语<,<=,>=,between,in等操作 +- ref 使用非唯一索引扫描或唯一索引前缀扫描,返回单条记录,常出现在关联查询中 +- eq_ref 类似ref,区别在于使用的是唯一索引,使用主键的关联查询 +- const/system 单条记录,系统会把匹配行中的其他列作为常数处理,如主键或唯一索引查询 +- null MySQL不访问任何表或索引,直接返回结果 虽然上至下,效率越来越高,但是根据cost模型,假设有两个索引idx1(a, b, c),idx2(a, c),SQL为"select * from t where a = 1 and b in (1, 2) order by c";如果走idx1,那么是type为range,如果走idx2,那么type是ref;当需要扫描的行数,使用idx2大约是idx1的5倍以上时,会用idx1,否则会用idx2 + +Extra + +- Using filesort:MySQL需要额外的一次传递,以找出如何按排序顺序检索行。通过根据联接类型浏览所有行并为所有匹配WHERE子句的行保存排序关键字和行的指针来完成排序。然后关键字被排序,并按排序顺序检索行。 +- Using temporary:使用了临时表保存中间结果,性能特别差,需要重点优化 +- Using index:表示相应的 select 操作中使用了覆盖索引(Coveing Index),避免访问了表的数据行,效率不错!如果同时出现 using where,意味着无法直接通过索引查找来查询到符合条件的数据。 +- Using index condition:MySQL5.6之后新增的ICP,using index condtion就是使用了ICP(索引下推),在存储引擎层进行数据过滤,而不是在服务层过滤,利用索引现有的数据减少回表的数据。 + +#### 3、show profile 分析 + +了解SQL执行的线程的状态及消耗的时间。默认是关闭的,开启语句“set profiling = 1;” + +``` +SHOW PROFILES ; +SHOW PROFILE FOR QUERY #{id}; +``` + +#### 4、trace + +trace分析优化器如何选择执行计划,通过trace文件能够进一步了解为什么优惠券选择A执行计划而不选择B执行计划。 + +``` +set optimizer_trace="enabled=on"; +set optimizer_trace_max_mem_size=1000000; +select * from information_schema.optimizer_trace; +``` + +#### 5、确定问题并采用相应的措施 + +- 优化索引 +- 优化SQL语句:修改SQL、IN 查询分段、时间查询分段、基于上一次数据过滤 +- 改用其他实现方式:ES、数仓等 +- 数据碎片处理 + +## 场景分析 + +#### 案例1、最左匹配 + +索引 + +``` +KEY `idx_shopid_orderno` (`shop_id`,`order_no`) +``` + +SQL语句 + +``` +select * from _t where orderno='' +``` + +查询匹配从左往右匹配,要使用order_no走索引,必须查询条件携带shop_id或者索引(`shop_id`,`order_no`)调换前后顺序。 + +#### 案例2、隐式转换 + +索引 + +``` +KEY `idx_mobile` (`mobile`) +``` + +SQL语句 + +``` +select * from _user where mobile=12345678901 +``` + +隐式转换相当于在索引上做运算,会让索引失效。mobile是字符类型,使用了数字,应该使用字符串匹配,否则MySQL会用到隐式替换,导致索引失效。 + +#### 案例3、大分页 + +索引 + +``` +KEY `idx_a_b_c` (`a`, `b`, `c`) +``` + +SQL语句 + +``` +select * from _t where a = 1 and b = 2 order by c desc limit 10000, 10; +``` + +对于大分页的场景,可以优先让产品优化需求,如果没有优化的,有如下两种优化方式, 一种是把上一次的最后一条数据,也即上面的c传过来,然后做“c < xxx”处理,但是这种一般需要改接口协议,并不一定可行。 + +另一种是采用延迟关联的方式进行处理,减少SQL回表,但是要记得索引需要完全覆盖才有效果,SQL改动如下 + +``` +select t1.* from _t t1, (select id from _t where a = 1 and b = 2 order by c desc limit 10000, 10) t2 where t1.id = t2.id; +``` + +#### 案例4、in + order by + +索引 + +``` +KEY `idx_shopid_status_created` (`shop_id`, `order_status`, `created_at`) +``` + +SQL语句 + +``` +select * from _order where shop_id = 1 and order_status in (1, 2, 3) order by created_at desc limit 10 +``` + +in查询在MySQL底层是通过n*m的方式去搜索,类似union,但是效率比union高。in查询在进行cost代价计算时(代价 = 元组数 * IO平均值),是通过将in包含的数值,一条条去查询获取元组数的,因此这个计算过程会比较的慢,所以MySQL设置了个临界值(eq_range_index_dive_limit),5.6之后超过这个临界值后该列的cost就不参与计算了。 + +因此会导致执行计划选择不准确。默认是200,即in条件超过了200个数据,会导致in的代价计算存在问题,可能会导致Mysql选择的索引不准确。 + +处理方式,可以(`order_status`, `created_at`)互换前后顺序,并且调整SQL为延迟关联。 + +#### 案例5、范围查询阻断,后续字段不能走索引 + +索引 + +``` +KEY `idx_shopid_created_status` (`shop_id`, `created_at`, `order_status`) +``` + +SQL语句 + +``` +select * from _order where shop_id = 1 and created_at > '2021-01-01 00:00:00' and order_status = 10 +``` + +范围查询还有“IN、between” + +#### 案例6、不等于、不包含不能用到索引的快速搜索。(可以用到ICP) + +``` +select * from _order where shop_id=1 and order_status not in (1,2) +select * from _order where shop_id=1 and order_status != 1 +``` + +在索引上,避免使用NOT、!=、<>、!<、!>、NOT EXISTS、NOT IN、NOT LIKE等 + +#### 案例7、优化器选择不使用索引的情况 + +如果要求访问的数据量很小,则优化器还是会选择辅助索引,但是当访问的数据占整个表中数据的蛮大一部分时(一般是20%左右),优化器会选择通过聚集索引来查找数据。 + +``` +select * from _order where order_status = 1 +``` + +查询出所有未支付的订单,一般这种订单是很少的,即使建了索引,也没法使用索引。 + +#### 案例8、复杂查询 + +``` +select sum(amt) from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01'; +select * from _t where a = 1 and b in (1, 2, 3) and c > '2020-01-01' limit 10; +``` + +如果是统计某些数据,可能改用数仓进行解决; + +如果是业务上就有那么复杂的查询,可能就不建议继续走SQL了,而是采用其他的方式进行解决,比如使用ES等进行解决。 + +#### 案例9、asc和desc混用 + +``` +select * from _t where a=1 order by b desc, c asc +``` + +desc 和asc混用时会导致索引失效 + +#### 案例10、大数据 + +对于推送业务的数据存储,可能数据量会很大,如果在方案的选择上,最终选择存储在MySQL上,并且做7天等有效期的保存。 + +那么需要注意,频繁的清理数据,会照成数据碎片,需要联系DBA进行数据碎片处理。 + +参考资料: + +- 深入浅出MySQL:数据库开发、优化与管理维护(唐汉明 / 翟振兴 / 关宝军 / 王洪权) +- MySQL技术内幕——InnoDB存储引擎(姜承尧) +- https://dev.mysql.com/doc/refman/5.7/en/explain-output.html +- https://dev.mysql.com/doc/refman/5.7/en/cost-model.html +- https://www.yuque.com/docs/share/3463148b-05e9-40ce-a551-ce93a53a2c66 \ No newline at end of file diff --git a/docs/zsxq/introduce.md b/docs/zsxq/introduce.md index e3c4f85..bae53f0 100644 --- a/docs/zsxq/introduce.md +++ b/docs/zsxq/introduce.md @@ -181,7 +181,7 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, ## 怎么进入星球? -如果你下定决心要加入的话,可以直接扫下面这个二维码。星球定价**158**元,减去**50**元的优惠券,等于说只需要**108**元(**拒绝割韭菜**)的价格就可以加入,服务期一年,**每天不到三毛钱**(0.29元),相比培训班几万块的学费,非常值了,星球提供的服务**远超**门票价格了。 +如果你下定决心要加入的话,可以直接扫下面这个二维码。星球定价**178**元,减去**50**元的优惠券,等于说只需要**128**元(**拒绝割韭菜**)的价格就可以加入,服务期一年,**每天只要三毛钱**(0.35元),相比培训班几万块的学费,非常值了,星球提供的服务**远超**门票价格了。 随着星球内容不断积累,星球定价也会不断**上涨**,所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 diff --git a/docs/zsxq/question/qa-or-java.md b/docs/zsxq/question/qa-or-java.md new file mode 100644 index 0000000..919a3da --- /dev/null +++ b/docs/zsxq/question/qa-or-java.md @@ -0,0 +1,29 @@ +大彬老师您好 + +我是参加今年24届秋招的研二学生,本科就读于某211信息安全专业,硕士就读于中国科学院大学 网络空间安全专业,最近开始了秋招,但是在职业规划上十分迷茫,想听听您的建议。 + +我得求职期望是,在北京找一个高薪的岗位,工作几年后回家乡,找一个国企躺平。 + +目前我已经排除了算法岗,虽然已经发表了两篇论文,但是研究方向小众,不足以支撑我找到一个算法岗的工作。所以我在以下几个岗位中有所纠结: + +1. java开发岗:为了准备开发岗,我跟着教程自己做了一个项目,整体的感受是工程能力很薄弱,并且只有一个学习项目,没有真正的工程项目。 +2. 安全岗位:虽然我本硕都是安全专业,但是研究方向都比较小众,目前互联网需要的安全人才都是攻防方向的,我的竞争力不够。 +3. 测试开发岗:这是我比较有把握的一个求职方向,因为之前有过一段在字节的测开实习经历,加上一直准备java开发,很多八股都是通用的。但是我比较担心测开岗位会不会不太好跳槽到国企,对未来比较担忧。 + +所以我目前采取的策略是:大厂投测开、中小厂投开发、国企/银行投安全 + +这令我十分地疲惫。很羞愧,已经7月份了,我竟让自己陷入了如此被动的局面,加上这两年互联网寒冬,我对秋招很担心。实际上我从很早之前就在准备找工作了,科研之余每天刷题、学java基础、学框架、写项目,但是时至今日我还是不够,始终比不上那些硕士阶段一直在参加工程的同学。 + +以上是我的基本情况 +- 想听听老师对我的现状有没有什么建议 +- 您是否了解测试开发岗位的发展前景,它的薪资和开发是差不多的嘛? +- 测开有没有可能从互联网顺利转国企 +- 现阶段我是继续补充java基础卷java岗,还是把重点放在测开上呢? + +原谅我现在处于焦虑状态因此问题比较多,图片是我的简历方便老师了解我的基本情况,期待老师回复🍓 +谢谢老师~ + +1、建议投测开,你这个简历在测开岗里面算是比较好的,相反在Java开发岗中算是比较一般的,没有很大的竞争力。有了字节的测开实习,在测开岗里面应该领先一大波人了。 +2、测开发展前景个人觉得也是不错的,随着互联网行业的发展,用户对产品的质量要求也越来越高,软件的性能测试、需求测试等方面的需求目前看是只增不减的。薪资方面,同职级测开跟开发基本持平。 +3、测开有没有可能从互联网顺利转国企?完全可以,没问题 +4、把重点放在测开,不建议同时准备两个岗位,可能顾此失彼 \ No newline at end of file diff --git a/docs/zsxq/share/completable-future-bug.md b/docs/zsxq/share/completable-future-bug.md new file mode 100644 index 0000000..6ff1be0 --- /dev/null +++ b/docs/zsxq/share/completable-future-bug.md @@ -0,0 +1,250 @@ +--- +sidebar: heading +title: 记一次生产中使用CompletableFuture遇到的坑 +category: 优质文章 +tag: + - 生产问题 +head: + - - meta + - name: keywords + content: CompletableFuture,生产问题,bug,异步调用 + - - meta + - name: description + content: 努力打造最优质的Java学习网站 +--- + +#### 为什么使用CompletableFuture + +业务功能描述:有一个功能是需要调用基础平台接口组装我们需要的数据,在这个功能里面我们要调用多次基础平台的接口,我们的入参是一个id,但是这个id是一个集合。我们都是使用RPC调用,一般常规的想法去遍历循环这个idList,但是呢这个id集合里面的数据可能会有500个左右。说多不多,说少也不少,主要是在for循环里面多次去RPC调用是一件特别费时的事情。 + +我用代码大致描述一下这个需求: + +```java +public List buildBasicInfo(List ids) { + List basicInfoList = new ArrayList<>(); + for (Long id : ids) { + getBasicData(basicInfoList, id); + } + } + + private List getBasicData(List basicInfoList, Long id) { + BasicInfo basicInfo = rpcGetBasicInfo(id); + return basicInfoList.add(basicInfo); + } + + public BasicInfo rpcGetBasicInfo(Long id) { + // 第一次RPC 调用 + rpcInvoking_1()........... + + // 拿到第一次的结果进行第二次RPC 调用 + rpcInvoking_2()........... + + // 拿到第二次的结果进行第三次RPC 调用、 + rpcInvoking_3()........... + + // 拿到第三次的结果进行第四次RPC 调用、 + rpcInvoking_4()........... + + // 组装结果返回 + + return BasicInfo; + } +``` + +是的,这个数据的获取就是这么的扯淡。。。如果使用循环的方式,当ids数据量在500个左右的时候,这个接口返回的时间再8s左右,这是万万不能接受的,那如果ids数据更多呢?所以不能用for循环去遍历ids呀,这样确实是太费时了。 + +既然远程调用避免不了,那就想办法让这个接口快一点,这时候就想到了多线程去处理,然后就想到使用CompletableFuture异步调用: + +#### CompletableFuture多线程异步调用 + +```java +List basicInfoList = new ArrayList<>(); + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + ids.forEach(id -> { + getBasicData(basicInfoList, id); + }); + return basicInfoList; + }); + try { + List basicInfos = future.get(); + } catch (Exception e) { + e.printStackTrace(); + } +``` + +> 这里补充一点:**CompletableFuture是否使用默认线程池的依据,和机器的CPU核心数有关。当CPU核心数减1大于1时,才会使用默认的线程池(ForkJoinPool),否则将会为每个CompletableFuture的任务创建一个新线程去执行**。即,CompletableFuture的默认线程池,只有在**双核以上的机器**内才会使用。在双核及以下的机器中,会为每个任务创建一个新线程,**等于没有使用线程池,且有资源耗尽的风险**。 + +默认线程池,**池内的核心线程数,也为机器核心数减1**,这里我们的机器是8核的,也就是会创建7个线程去执行。 + +上面这种方式虽然实现了多线程异步执行,但是如果ids集合很多话,依然会很慢,因为`future.get();`也是堵塞的,必须等待所有的线程执行完成才能返回结果。 + +#### 改进CompletableFuture多线程异步调用 + +想让速度更快一点,就想到了把ids进行分隔: + +```ini +ini复制代码 int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1; + List> partitionAssetsIdList = Lists.partition(ids, pageSize); +``` + +因为我们CPU核数为8核,所有当ids的大小小于8时,就开启8个线程,每个线程分一个。这里的>>3(右移运算)相当于ids的大小除以2的3次方也就是除以8;右移运算符相比除效率会高。毕竟现在是在优化提升速度。 + +如果这里的ids的大小是500个,就是开启9个线程,其中8个线程是处理62个数据,另一个线程处理4个数据,因为有余数会另开一个线程处理。具体代码如下: + +```java +int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1; + List> partitionIdList = Lists.partition(ids, pageSize); + List> futures = new ArrayList<>(); + //如果ids为500,这里会分隔成9份,也就是partitionIdList.size()=9;遍历9次,也相当于创建了9个CompletableFuture对象,前8个CompletableFuture对象处理62个数据。第9个处理4个数据。 + partitionIdList.forEach(partitionIds -> { + List basicInfoList = new ArrayList<>(); + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + partitionIds.forEach(id -> { + getBasicData(basicInfoList, id); + }); + return basicInfoList; + }); + futures.add(future); + }); + // 把所有线程执行的结果进行汇总 + List basicInfoResult = new ArrayList<>(); + for (CompletableFuture future : futures) { + try { + basicInfoResult.addAll((List)future.get()); + } catch (Exception e) { + e.printStackTrace(); + } + } +``` + +如果ids的大小等于500,就会被分隔成9份,创建9个CompletableFuture对象,前8个CompletableFuture对象处理62个数据(id),第9个处理4个数据(id)。这62个数据又会被分成7个线程去执行(CPU核数减1个线程)。经过分隔之后充分利用了CPU。速度也从8s减到1-2s。得到了总监和同事的夸赞,同时也被写到正向事件中;哈哈哈哈。 + +#### 在生产环境中遇到的坑 + +上面说了那么多还没有说到坑在哪里,下面我们就说说坑在哪里? + +本地和测试都没有啥问题,那就找个时间上生产呗,升级到生产环境,发现这个接口堵塞了,超时了。。。 + +![](http://img.topjavaer.cn/img/202308070010341.png) + +刚被记录到正向事件,可不想在被记录个负向时间。感觉去看日志。 + +发现日志就执行了将ids进行分隔,后面循环去创建CompletableFuture对象之后的代码都没有在执行了。然后我第一感觉测试是future.get()获取结果的时候堵塞了,所以一直没有结果返回。 + +#### 排查问题过程 + +我们要解决这个问题就要看看问题出现在哪里? + +当执行到这个接口时候我们第一时间看了看CPU的使用率: + +![](http://img.topjavaer.cn/img/202308070010837.png) + +这是访问接口之前: + +![](http://img.topjavaer.cn/img/202308070010992.png) + +发现执行这个接口时PID为10348的这个进程的CPU突然的高了起来。 + +紧接着使用`jps -l` :打印出我们服务进程的PID + +![](http://img.topjavaer.cn/img/202308070011114.png) + +PID为10348正式我们现在执行这个服务。 + +接着我就详细的看一下这个PID为10348的进程下哪里线程占用的高: + +发现这几个占用的相对高一点: + +![](http://img.topjavaer.cn/img/202308070011121.png) + +![](http://img.topjavaer.cn/img/202308070011008.png) + +紧接着使用jstack命令生成java虚拟机当前时刻的线程快照,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。 线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源 + +`jstack -l 10348 >/tmp/10348.log`,使用此命令将PID为10348的进程下所有线程快照输出到log文件中。 + +同时我们将线程比较的PID转换成16进制:printf "%x\n" 10411 + +![](http://img.topjavaer.cn/img/202308070012886.png) + +我们将转换成16进制的数值28ab,28a9在10348.log中搜索一下: + +![](http://img.topjavaer.cn/img/202308070012229.png) + +![](http://img.topjavaer.cn/img/202308070012075.png) + +看到线程的快照发现这不是本次修改的接口呀。看到日志4处但是也是用了CompletableFuture。找到对应4处的代码发现这是监听mq消息,然后异步去执行,代码类型这样: + +![](http://img.topjavaer.cn/img/202308070013815.png) + +经过查看日志发现这个mq消息处理很频繁,每秒都会有很多的数据上来。 + +![](http://img.topjavaer.cn/img/202308070013136.png) + +我们知道CompletableFuture默认是使用ForkJoinPool作为线程池。难道mq使用ForkJoinPool和我当前接口使用的都是同一个线程池中的线程?难道是共用的吗? + +MQ监听使用的线程池: + +![](http://img.topjavaer.cn/img/202308070013857.png) + +我们当前接口使用的线程池: + +![](http://img.topjavaer.cn/img/202308070013168.png) + +![](http://img.topjavaer.cn/img/202308070014642.png) + +![](http://img.topjavaer.cn/img/202308070014684.png) + +![](http://img.topjavaer.cn/img/202308070014348.png) + +它们使用的都是ForkJoinPool.commonPool()公共线程池中的线程! + +看到这个结论就很好理解了,我们目前修改的接口使用的线程池中的线程全部都被MQ消息处理占用,我们修改优化的接口得不到资源,所以一直处于等待。 + +同时我们在线程快照10348.log日志中也看到我们优化的接口对应的线程处于WAITING状态! + +![image-20230807001443599](http://img.topjavaer.cn/img/202308070014683.png) + +这里`- parking to wait for <0x00000000fe2081d8>`肯定也是MQ消费线程中的某一个。由于MQ消费消息比较多,每秒都会监听到大量的数据,线程的快照日志收集不全。所以在10348.log中没有找到,这不影响我们修改bug。问题的原因已经找到了。 + +#### 解决问题 + +上面我们知道两边使用的都是公共静态线程池,我们只要让他们各用各的就行了:自定义一个线程池:`ForkJoinPool pool = new ForkJoinPool();` + +```java +int pageSize = ids.size() > 8 ? ids.size() >> 3 : 1; + List> partitionIdList = Lists.partition(ids, pageSize); + List> futures = new ArrayList<>(); + partitionIdList.forEach(partitionIds -> { + List basicInfoList = new ArrayList<>(); + //重新创建一个ForkJoinPool对象就可以了 + ForkJoinPool pool = new ForkJoinPool(); + CompletableFuture> future = CompletableFuture.supplyAsync(() -> { + partitionIds.forEach(id -> { + getMonitoringCoverage(basicInfoList, id); + }); + return basicInfoList; + //在这里使用 + },pool); + futures.add(future); + }); + // 把所有线程执行的结果进行汇总 + List basicInfoResult = new ArrayList<>(); + for (CompletableFuture future : futures) { + try { + basicInfoResult.addAll((List)future.get()); + } catch (Exception e) { + e.printStackTrace(); + } + } +``` + +这样他们就各自用各自的线程池中的线程了。不会存在资源的等待现场了。 + +#### 总结: + +之所以测试环境和开发环境没有出现这样的问题是因为这两个环境mq没有监听到消息。大量的消息都在生产环境中才会出现。由于测试环境的数据量达不到生产环境的数据量,所以有些问题在测试环境体验不出来。 + + + +> 原文链接:https://juejin.cn/post/7165704556540755982 \ No newline at end of file diff --git a/docs/zsxq/share/oom.md b/docs/zsxq/share/oom.md new file mode 100644 index 0000000..d378bbf --- /dev/null +++ b/docs/zsxq/share/oom.md @@ -0,0 +1,13 @@ +记录一次内存泄露排查的过程。 + +最近经常告警有空指针异常报出,于是找到运维查日志定位到具体是哪一行代码抛出的空指针异常,发现是在解析cookie的一个方法内,调用`HttpServletRequest.getServerName()`获取不到抛出的NPE,这个获取服务名获取不到,平时都没有出现过的问题,最近也没有发版,那初步怀疑应该前端或者传输过程有问题导致获取不到参数。 + +后续找了运维查了ng的日志,确实存在状态码为**499**的错误码,查了一下这个是**客户端主动关闭请求或者客户端网络断掉时**报的错误码,那也就是前端断开了请求。 + +继续排查为啥前端会中断请求的原因,问了前端同学说是超时时间设置了10秒,又看了日志,确实是有处理时间超过10秒的,那问题大概定位到了。 + +接下来就是分析处理时长为什么会那么长,看了报错的时候请求量并没有很大,后续让运维查了机器有几台cpu用量处于30%左右,明显高于另外几台3%,而且499错误的集中在cpu用量高的几台,怀疑是否是内存问题导致,让运维跑了`jstat -gcutil`看了一下,确实存在full GC问题,又跑了`jmap -dump`下了dump文件,定位到是ip限流的方法,有一个清除Map的方法在多线程并发情况下没有生效,导致内存泄露。 + +知道问题后反推感觉就一切的疑问有了结果,ng报499是前置超时,超时是服务频繁full gc导致stw,无法处理请求导致耗时增加,ng探活接口在机器stw期间无法响应产生了error.log,而有几台机器cpu不高是因为之前重启过所以释放掉了内存没有触发full gc。 + +后续处理是改进了ip限流的方法,测试环境复现问题和改动限流方法,通过guaua的LoadingCache监听器的方式过期自动处理,这次问题嵌套问题比较多,由于上报量低,ng侧报了499没有纳入监控范围,而且机器由于重启没有快速发现问题,后续改进是代码侧能复用规范代码最好复用,不要重复造轮子。 \ No newline at end of file diff --git a/docs/zsxq/share/slow-query.md b/docs/zsxq/share/slow-query.md new file mode 100644 index 0000000..4d3ab7f --- /dev/null +++ b/docs/zsxq/share/slow-query.md @@ -0,0 +1,62 @@ +**最近的生产慢查询问题分析与总结** + +**1.问题描述** + +前几天凌晨出现大量慢查询告警,经DBA定位为某个子系统涉及的一条查询语句出现慢查询,引起数据库服务器的cpu使用率突增,触发大量告警,查看生产执行计划发现慢查询为索引跳变引起;具体出现问题的sql语句如下: + +``` +select * from ( select t.goods id as cardid,p.validate as validate, p.create timecreateTime + p.repay_coupon type as repaycoupontype,p.require anount as requireAmount, p.goods tag as goodsTag, + p.deduction anount as deductionAmount, p.repay coupon remark as repay(ouponkemark, + p.sale_point as salepoint from user_prizes p join trans_order t + on p.user_id = t.user_id and p.order_no = t.order_no + where p.user_id = #{user_id} and p.wmkt_type = '6' and t.status = 's' and p.equity_code_status = 'N' + and p.create_time >= #{beginDate} + order by p.create_time dese limit 1000) as cc group by cc.cardid: +``` + + 该sql为查询三个月内满足条件的还款代金券列表,其中user_prizes表和trans_order表都是大表,数据量达到亿级别,user_prizes表有如下几个索引: + +```sql +PRIMARY KEY ('merchant_order_no ,partition key ) USING BTREE, +KEY “thirdOrderNo" ("third_order_no","app_id"), +KEY "createTime" ("create_time"), +KEY "userId" ("user_id"), +KEY "time_activity" ("create_time', 'activity_name') +``` + + 正常情况下该语句走的userId索引,当天零点后该sql语句出现索引跳变,走了createTime索引,导致出现慢查询。 + +**2.问题处理方式** + + 生产定位到问题后,因该sql的查询场景为前端触发,当天为账单日,请求量大,DBA通过kill脚本临时进行处理,同时准备增加强制索引优化的紧急版本,通过加上强制索引force index(userId)处理索引跳变,DBA也同步在备库删除createTime索引观察效果,准备进行主备切换尝试解决,但在备库执行索引删除后查看执行计划发现又走了time_activity索引,最后通过发布增加强制索引优化的紧急版本进行解决。 + +**3.问题分析** + +MySQL优化器选择索引的目的是找到一个最优的执行方案,并用最小的代价去执行语句,扫描行数是影响执行代价的重要因素,扫描行数越少,意味着访问磁盘数据次数越少,消耗的cpu资源越少,除此之外,优化器还会结合是否使用临时表,以及是否排序等因素综合判断。 + +出现索引跳变的这个sql有order by create_time,且create_time为索引,user_prizes表是按月分区的,数据总量为1亿400多万,通过统计生产数据量分布情况发现,近几个月的分区数据量如下: + +![](http://img.topjavaer.cn/img/202307102303308.png) + + 该sql7月1日零点后查询的是4月1日之后的数据,3月份分区的数据量为958万多,4月之后分区数量都保持在500多万。4月份之后分区数据减少了,可能是这个原因导致优化器认为走时间索引createTime的区分度更高,同时还可以避免排序,因而选择了时间索引。查看索引跳变后的执行计划如下: + +![](http://img.topjavaer.cn/img/202307102303994.png) + +走createTime索引虽然可以避免排序,但从执行计划的type=range可看出为索引范围的扫描,根据索引createTime扫描记录,通过索引叶子节点的主键值回表查找完整记录,然后判断记录中满足sql过滤条件的数据,再将结果进行返回,而该语句为查找满足条件的1000条数据,正常情况下一个user_id满足条件的数据量不会超过1000条,要找到所有满足条件的记录就是索引范围的扫描加回表查询,加上查询范围内的数据量大,因此走createTime索引就会非常慢。 + +**4.总结** + +由于mysql在真正执行语句的时候,并不能准确的知道满足这个条件的记录有多少,只能通过统计信息来估算记录,而优化器并不是非常智能的,就有可能发生索引跳变的情况,这种情况很难在测试的时候复现出来,生产也可能是突然出现,所以我们只能在使用上尽量的去降低索引发生跳变的可能性,尽量避免出现该问题。我们可以在创建索引和使用sql的时候通过以下几个点进行检视。 + +(1) 索引的创建 + + 创建索引的时候要注意尽量避免创建单列的时间字段(createTime、updateTime)索引,避免留坑,因为很多场景都可能用到时间字段进行排序,有排序的情况若排序字段又是单列索引字段,就可能引起索引跳变,如果需要使用时间字段作为索引时,尽量使用联合索引,且时间字段放在后面;高效的索引应遵循高区分度字段+避免排序的原则。 + + 创建索引的时候也要尽量避免索引重复,且一张表的索引个数也要控制好,索引过多也会影响增删改的效率。 + +(2) sql的检视 + + 检视历史和新增的sql是否有order by,且order by的第一个字段是否有单列索引,这种存在索引跳变的风险,需要具体分析后进行优化; + + 写sql语句的时候,尽可能简单化,像union、排序等尽量少在sql中实现,减少sql慢查询的风险。 \ No newline at end of file diff --git a/docs/zsxq/share/spring-upgrade-copy-problem.md b/docs/zsxq/share/spring-upgrade-copy-problem.md new file mode 100644 index 0000000..07b2f5c --- /dev/null +++ b/docs/zsxq/share/spring-upgrade-copy-problem.md @@ -0,0 +1,146 @@ +最近内部组件升级到spring5.3.x的时候对象拷贝内容不全,定位分析总结如下(可直接拉到最后看结论和解决办法): + +**1.现象:** + +源对象的类里有个内部类的成员变量,是List类型,,List的元素类型是自己的内部静态类 + +目标对象的类里有个内部类的成员变量,也是List类型,List的元素类型是自己的内部静态类 + +源对象的代码示例如下(省略了get set方法): + +```java +/** + * 源对象 + * @author dabin + * + */ +public class Rsp_07300240_01 { + private int totals; + private List contracts;//合同列表 + static public class Contract{ + private String constractId;//合同编号 + private String constractName;//合同名称 + private String type;//合同类型 + private String fileId;//fps文件id + private String fileHash;//fps文件hash + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Rsp_07300240_01.Contract [constractId="); + builder.append(constractId); + builder.append(", constractName="); + builder.append(constractName); + builder.append(", type="); + builder.append(type); + builder.append(", fileId="); + builder.append(fileId); + builder.append(", fileHash="); + builder.append(fileHash); + builder.append("]"); + return builder.toString(); + } + } +} +``` + +目标对象的代码示例如下(省略了get set方法): + +```java +public class Rsp_04301099_01 { + @RmbField(seq = 1, title = "总条数") + private int totals; + @RmbField(seq = 2, title = "合同列表") + // 这里是自己的内部类 + private List contracts;// 合同列表 + static public class Contract{ + private String constractId;//合同编号 + private String constractName;//合同名称 + private String type;//合同类型 + private String fileId;//fps文件id + private String fileHash;//fps文件hash +@Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append("Rsp_04301099_01.Contract [constractId="); + builder.append(constractId); + builder.append(", constractName="); + builder.append(constractName); + builder.append(", type="); + builder.append(type); + builder.append(", fileId="); + builder.append(fileId); + builder.append(", fileHash="); + builder.append(fileHash); + builder.append("]"); + return builder.toString(); + } + } +} +``` + +单元测试验证代码如下: + +```java +public class SpringBeanCopyUtilTest { + @Test + public void testBeanCopy() { + Rsp_07300240_01 orgResp = new Rsp_07300240_01(); + orgResp.setTotals(100); + List contracts = new ArrayList<>(); + Rsp_07300240_01.Contract cc = new Rsp_07300240_01.Contract(); + cc.setConstractId("aaa"); + contracts.add(cc); + orgResp.setContracts(contracts); + Rsp_04301099_01 destResp = new Rsp_04301099_01(); + System.out.println("源对象的值:" + orgResp); + System.out.println("复制前的值:" + destResp); + BeanUtils.copyProperties(orgResp, destResp); + System.out.println("Spring版本" + BeanUtils.class.getPackage().getImplementationVersion()); + System.out.println("复制后的值:" + destResp); +// if (destResp.getContracts() != null && destResp.getContracts().size() > 0) { +// System.out.println("复制后的list成员类型是:" + destResp.getContracts().get(0)); +// } + } +} +``` + +分别依赖spring 5.2.4和5.3.9版本,运行结果如下: + +```java +源对象的值:Rsp_07300240_01 [totals=100, contracts=[Rsp_07300240_01.Contract [constractId=aaa, constractName=null, type=null, fileId=null, fileHash=null]]] +复制前的值:Rsp_04301099_01 [totals=0, contracts=null] + +Spring版本5.2.4.RELEASE +复制后的值:Rsp_04301099_01 [totals=100, contracts=[Rsp_07300240_01.Contract [constractId=aaa, constractName=null, type=null, fileId=null, fileHash=null]]] + +Spring版本5.3.9 +复制后的值:Rsp_04301099_01 [totals=100, contracts=null] +``` + +**2.分析** + +可以看到在依赖spring 5.3.x的时候,contracts的值是没有复制过来的。 + +但是也可以看到在依赖spring 5.2.x的时候,contracts的值是直接设置的引用,List的成员变量类型是 Rsp_07300240_01.Contract,Rsp_04301099_01.Contract。 + +这个其实也是有问题的。但是为啥在业务逻辑中没有暴雷呢? + +经核实,业务代码中,是在返回应答对象之前执行的 org.springframework.beans.BeanUtils.copyProperties 操作,执行完之后,立即返回了对象,然后内部使用的框架,直接使用jackson进行系列化,此时类型信息已经擦除,不涉及类型转换,所以正常生成了json字符串。 + +而上面示例中被注释的代码里,如果启用的话,测试的时候就会立即报错,提示类型转换异常。 + +对比代码可以发现: + +![](https://article-images.zsxq.com/FvjCdaKbUj3E3fLVKRkvtve2yIa6) + +经过一番搜索,原来是在2019年的时候就有人向Spring社区提了bug,然后spring增加了泛型判断逻辑,杜绝了错误的赋值,在5.3.x中修复了这个bug。 + +https://github.com/spring-projects/spring-framework/issues/24187 + +由于平时大部分使用场景都是执行BeanUtils.copyProperties后立即取出里面的对象进行操作,这种情况下,就会提前触发bug,然后调用方自己想办法规避掉spring bug。 + +恰好内部使用的xx框架在BeanUtils.copyProperties之后没有显式的操作成员对象,因此一直没有触发bug,直到升级到spring 5.3.x时才暴雷。 + +**3.解决办法:** + +先回退版本,然后检视所有调用BeanUtils.copyProperties的地方,针对触发bug的这种场景优化代码,比如把两个内部静态class合并使用一个公共的class。 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 45fa65f..57fab08 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2351,6 +2351,13 @@ "integrity": "sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==", "dev": true }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true, + "peer": true + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.7.tgz", @@ -2363,6 +2370,25 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "node_modules/@types/react": { + "version": "18.2.7", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.2.7.tgz", + "integrity": "sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react/node_modules/csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true, + "peer": true + }, "node_modules/@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz", @@ -2381,6 +2407,13 @@ "@types/node": "*" } }, + "node_modules/@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true, + "peer": true + }, "node_modules/@types/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.0.tgz", @@ -5906,6 +5939,19 @@ "node": ">=10" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz", @@ -6324,6 +6370,33 @@ "eve-raphael": "0.5.0" } }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.0.tgz", @@ -6601,6 +6674,16 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "peer": true, + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/section-matter": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", @@ -9675,6 +9758,13 @@ "integrity": "sha512-I4BD3L+6AWiUobfxZ49DlU43gtI+FTHSv9pE2Zekg6KjMpre4ByusaljW3vYSLJrvQ1ck1hUaeVu8HVlY3vzHg==", "dev": true }, + "@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true, + "peer": true + }, "@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmmirror.com/@types/qs/-/qs-6.9.7.tgz", @@ -9687,6 +9777,27 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "@types/react": { + "version": "18.2.7", + "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.2.7.tgz", + "integrity": "sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==", + "dev": true, + "peer": true, + "requires": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + }, + "dependencies": { + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", + "dev": true, + "peer": true + } + } + }, "@types/resolve": { "version": "1.17.1", "resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.17.1.tgz", @@ -9705,6 +9816,13 @@ "@types/node": "*" } }, + "@types/scheduler": { + "version": "0.16.3", + "resolved": "https://registry.npmmirror.com/@types/scheduler/-/scheduler-0.16.3.tgz", + "integrity": "sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==", + "dev": true, + "peer": true + }, "@types/serve-static": { "version": "1.15.0", "resolved": "https://registry.npmmirror.com/@types/serve-static/-/serve-static-1.15.0.tgz", @@ -12534,6 +12652,16 @@ "is-unicode-supported": "^0.1.0" } }, + "loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dev": true, + "peer": true, + "requires": { + "js-tokens": "^3.0.0 || ^4.0.0" + } + }, "magic-string": { "version": "0.25.9", "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz", @@ -12876,6 +13004,27 @@ "eve-raphael": "0.5.0" } }, + "react": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, + "react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + } + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmmirror.com/readable-stream/-/readable-stream-3.6.0.tgz", @@ -13102,6 +13251,16 @@ "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, + "scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dev": true, + "peer": true, + "requires": { + "loose-envify": "^1.1.0" + } + }, "section-matter": { "version": "1.0.0", "resolved": "https://registry.npmmirror.com/section-matter/-/section-matter-1.0.0.tgz", From 7e7fb51291824c30cb7020a5db3ae4f8f7cdb208 Mon Sep 17 00:00:00 2001 From: dabin <1713476357@qq.com> Date: Sat, 23 Sep 2023 17:56:46 +0800 Subject: [PATCH 2/8] =?UTF-8?q?spring=E6=BA=90=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../advance/distributed/2-distributed-lock.md | 6 + docs/advance/distributed/4-micro-service.md | 10 +- docs/advance/system-design/README.md | 4 +- docs/computer-basic/network.md | 4 +- docs/database/mysql.md | 34 + docs/java/java-basic.md | 2 + docs/java/java-concurrent.md | 119 +++ docs/java/jvm.md | 4 +- docs/note/README.md | 6 + docs/note/redis-note.md | 28 + docs/other/site-diary.md | 3 + docs/redis/redis.md | 12 + ...64\344\275\223\346\236\266\346\236\204.md" | 73 ++ ...20\357\274\210\344\270\212\357\274\211.md" | 864 ++++++++++++++++ ...20\357\274\210\344\270\213\357\274\211.md" | 501 +++++++++ ...72\346\234\254\345\256\236\347\216\260.md" | 979 ++++++++++++++++++ ...35\350\265\226\345\244\204\347\220\206.md" | 178 ++++ ...36\346\200\247\345\241\253\345\205\205.md" | 563 ++++++++++ ...64\344\275\223\346\236\266\346\236\204.md" | 73 ++ ...n \347\232\204\345\212\240\350\275\275.md" | 650 ++++++++++++ ...44\271\213bean\345\210\233\345\273\272.md" | 702 +++++++++++++ ...07\347\255\276\350\247\243\346\236\220.md" | 602 +++++++++++ docs/zsxq/introduce.md | 4 +- ...\214 PO\357\274\214 DO\357\274\214 DTO.md" | 31 + docs/zsxq/question/tech/service-expansion.md | 7 + ...00\350\275\254\345\220\216\347\253\257.md" | 61 ++ ...57\346\211\276\345\267\245\344\275\234.md" | 22 + ...52\345\260\217\345\273\272\350\256\256.md" | 332 ++++++ ...347\272\247SQL\345\206\231\346\263\225.md" | 219 ++++ ...345\274\225\345\217\221\347\232\204bug.md" | 49 + 30 files changed, 6135 insertions(+), 7 deletions(-) create mode 100644 docs/note/README.md create mode 100644 docs/note/redis-note.md create mode 100644 "docs/source/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\212\357\274\211.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\213\357\274\211.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC \345\256\271\345\231\250\345\237\272\346\234\254\345\256\236\347\216\260.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\345\276\252\347\216\257\344\276\235\350\265\226\345\244\204\347\220\206.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\345\261\236\346\200\247\345\241\253\345\205\205.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" create mode 100644 "docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC-\345\274\200\345\220\257 bean \347\232\204\345\212\240\350\275\275.md" create mode 100644 "docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean\345\210\233\345\273\272.md" create mode 100644 "docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276\350\247\243\346\236\220.md" create mode 100644 "docs/zsxq/question/VO\357\274\214 BO\357\274\214 PO\357\274\214 DO\357\274\214 DTO.md" create mode 100644 docs/zsxq/question/tech/service-expansion.md create mode 100644 "docs/zsxq/question/\344\270\211\345\271\264\346\265\213\345\274\200\350\275\254\345\220\216\347\253\257.md" create mode 100644 "docs/zsxq/question/\350\257\273\345\215\232\350\277\230\346\230\257\346\211\276\345\267\245\344\275\234.md" create mode 100644 "docs/zsxq/share/Java\345\274\200\345\217\221\347\232\20416\344\270\252\345\260\217\345\273\272\350\256\256.md" create mode 100644 "docs/zsxq/share/\345\210\206\344\272\25310\344\270\252\351\253\230\347\272\247SQL\345\206\231\346\263\225.md" create mode 100644 "docs/zsxq/share/\347\224\261\342\200\234 YYYY-MM-dd \342\200\235\345\274\225\345\217\221\347\232\204bug.md" diff --git a/docs/advance/distributed/2-distributed-lock.md b/docs/advance/distributed/2-distributed-lock.md index a269865..1016022 100644 --- a/docs/advance/distributed/2-distributed-lock.md +++ b/docs/advance/distributed/2-distributed-lock.md @@ -181,6 +181,8 @@ public class RedisTest { 前面的方案是基于**Redis单机版**的分布式锁讨论,还不是很完美。因为Redis一般都是集群部署的。 +![](http://img.topjavaer.cn/img/202308202338736.png) + 如果线程一在`Redis`的`master`节点上拿到了锁,但是加锁的`key`还没同步到`slave`节点。恰好这时,`master`节点发生故障,一个`slave`节点就会升级为`master`节点。线程二就可以顺理成章获取同个`key`的锁啦,但线程一也已经拿到锁了,锁的安全性就没了。 为了解决这个问题,Redis作者antirez提出一种高级的分布式锁算法:**Redlock**。它的核心思想是这样的: @@ -189,6 +191,8 @@ public class RedisTest { 我们假设当前有5个Redis master节点,在5台服务器上面运行这些Redis实例。 +![](http://img.topjavaer.cn/img/202308202339712.png) + RedLock的实现步骤: 1. 获取当前时间,以毫秒为单位。 @@ -204,6 +208,8 @@ RedLock的实现步骤: - 如果大于等于3个节点加锁成功,并且使用的时间小于锁的有效期,即可认定加锁成功啦。 - 如果获取锁失败,解锁! +Redisson 实现了 redLock 版本的锁,有兴趣的小伙伴,可以去了解一下。 + ### 基于ZooKeeper的实现方式 ZooKeeper是一个为分布式应用提供一致性服务的开源组件,它内部是一个分层的文件系统目录树结构,规定同一个目录下只能有一个唯一文件名。基于ZooKeeper实现分布式锁的步骤如下: diff --git a/docs/advance/distributed/4-micro-service.md b/docs/advance/distributed/4-micro-service.md index 286e61b..aec218d 100644 --- a/docs/advance/distributed/4-micro-service.md +++ b/docs/advance/distributed/4-micro-service.md @@ -47,9 +47,15 @@ head: ## 分布式和微服务的区别 -从概念理解,分布式服务架构强调的是服务化以及服务的**分散化**,微服务则更强调服务的**专业化和精细分工**; +微服务解决的是系统复杂度问题,一般来说是业务问题,即在一个系统中承担职责太多了,需要打散,便于理解和维护,进而提升系统的开发效率和运行效率,微服务一般来说是针对应用层面的。 -从实践的角度来看,**微服务架构通常是分布式服务架构**,反之则未必成立。 +分布式解决的是系统性能问题,即解决系统部署上单点的问题,尽量让组成系统的子系统分散在不同的机器上进而提高系统的吞吐能力。 + +两者概念层面也是不一样的,微服务是设计层面的东西,一般考虑如何将系统从逻辑上进行拆分,也就是垂直拆分; + +而分布式是部署层面的东西,即强调物理层面的组成,即系统的各子系统部署在不同计算机上。 + +微服务可以是分布式的,即可以将不同服务部署在不同计算机上,当然如果量小也可以部署在单机上。 一句话概括:分布式:分散部署;微服务:分散能力。 diff --git a/docs/advance/system-design/README.md b/docs/advance/system-design/README.md index 79b8eb1..8f7b4c1 100644 --- a/docs/advance/system-design/README.md +++ b/docs/advance/system-design/README.md @@ -26,6 +26,8 @@ 怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -**扫描以下二维码**领取50元的优惠券即可加入。星球定价**158**元,减去**50**元的优惠券,等于说只需要**108**元的价格就可以加入,服务期一年,**每天不到三毛钱**(0.29元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 ![](http://img.topjavaer.cn/img/202304212233017.png) \ No newline at end of file diff --git a/docs/computer-basic/network.md b/docs/computer-basic/network.md index 465d1bc..127e6ab 100644 --- a/docs/computer-basic/network.md +++ b/docs/computer-basic/network.md @@ -41,6 +41,8 @@ head: 怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -**扫描以下二维码**领取50元的优惠券即可加入。星球定价**158**元,减去**50**元的优惠券,等于说只需要**108**元的价格就可以加入,服务期一年,**每天不到三毛钱**(0.29元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 ![](http://img.topjavaer.cn/img/202304212233017.png) diff --git a/docs/database/mysql.md b/docs/database/mysql.md index 9559ede..4edc5fb 100644 --- a/docs/database/mysql.md +++ b/docs/database/mysql.md @@ -1180,4 +1180,38 @@ COUNT(`*`)是SQL92定义的标准统计行数的语法,效率高,MySQL对它 所以,建议使用COUNT(\*)查询表的行数! +## 存储MD5值应该用VARCHAR还是用CHAR? + +首先说说CHAR和VARCHAR的区别: + +1、存储长度: + +CHAR类型的长度是固定的 + +当我们当定义CHAR(10),输入的值是"abc",但是它占用的空间一样是10个字节,会包含7个空字节。当输入的字符长度超过指定的数时,CHAR会截取超出的字符。而且,当存储为CHAR的时候,MySQL会自动删除输入字符串末尾的空格。 + +VARCHAR的长度是可变的 + +比如VARCHAR(10),然后输入abc三个字符,那么实际存储大小为3个字节。 + +除此之外,VARCHAR还会保留1个或2个额外的字节来记录字符串的实际长度。如果定义的最大长度小于等于255个字节,那么,就会预留1个字节;如果定义的最大长度大于255个字节,那么就会预留2个字节。 + +2、存储效率 + +CHAR类型每次修改后的数据长度不变,效率更高。 + +VARCHAR每次修改的数据要更新数据长度,效率更低。 + +3、存储空间 + +CHAR存储空间是初始的预计长度字符串再加上一个记录字符串长度的字节,可能会存在多余的空间。 + +VARCHAR存储空间的时候是实际字符串再加上一个记录字符串长度的字节,占用空间较小。 + + + +根据以上的分析,由于MD5是一个定长的值,所以MD5值适合使用CHAR存储。对于固定长度的非常短的列,CHAR比VARCHAR效率也更高。 + + + ![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/java/java-basic.md b/docs/java/java-basic.md index 5a4b19a..b3ef235 100644 --- a/docs/java/java-basic.md +++ b/docs/java/java-basic.md @@ -274,6 +274,8 @@ Integer x = 1; // 装箱 调⽤ Integer.valueOf(1) int y = x; // 拆箱 调⽤了 X.intValue() ``` +## 两个Integer 用== 比较不相等的原因 + 下面看一道常见的面试题: ```java diff --git a/docs/java/java-concurrent.md b/docs/java/java-concurrent.md index bb62dcc..e1c611a 100644 --- a/docs/java/java-concurrent.md +++ b/docs/java/java-concurrent.md @@ -231,6 +231,14 @@ executor提供一个原生函数isTerminated()来判断线程池中的任务是 - 调用new Thread()创建的线程缺乏管理,可以无限制的创建,线程之间的相互竞争会导致过多占用系统资源而导致系统瘫痪 - 直接使用new Thread()启动的线程不利于扩展,比如定时执行、定期执行、定时定期执行、线程中断等都不好实现 +## execute和submit的区别 + +execute只能提交Runnable类型的任务,无返回值。submit既可以提交Runnable类型的任务,也可以提交Callable类型的任务,会有一个类型为Future的返回值,但当任务类型为Runnable时,返回值为null。 + +execute在执行任务时,如果遇到异常会直接抛出,而submit不会直接抛出,只有在使用Future的get方法获取返回值时,才会抛出异常 + +execute所属顶层接口是Executor,submit所属顶层接口是ExecutorService,实现类ThreadPoolExecutor重写了execute方法,抽象类AbstractExecutorService重写了submit方法。 + ## 进程线程 进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间。 @@ -1238,4 +1246,115 @@ epoll的时间复杂度O(1)。epoll可以理解为event poll,不同于忙轮 > 参考链接:https://blog.csdn.net/u014209205/article/details/80598209 +## ReadWriteLock 和 StampedLock 的区别 + +在多线程编程中,对于共享资源的访问控制是一个非常重要的问题。在并发环境下,多个线程同时访问共享资源可能会导致数据不一致的问题,因此需要一种机制来保证数据的一致性和并发性。 + +Java提供了多种机制来实现并发控制,其中 ReadWriteLock 和 StampedLock 是两个常用的锁类。本文将分别介绍这两个类的特性、使用场景以及示例代码。 + +**ReadWriteLock** + +ReadWriteLock 是Java提供的一个接口,全类名:`java.util.concurrent.locks.ReentrantLock`。它允许多个线程同时读取共享资源,但只允许一个线程写入共享资源。这种机制可以提高读取操作的并发性,但写入操作需要独占资源。 + +**特性** + +- 多个线程可以同时获取读锁,但只有一个线程可以获取写锁。 +- 当一个线程持有写锁时,其他线程无法获取读锁和写锁,读写互斥。 +- 当一个线程持有读锁时,其他线程可以同时获取读锁,读读共享。 + +**使用场景** + +**ReadWriteLock** 适用于读多写少的场景,例如缓存系统、数据库连接池等。在这些场景中,读取操作占据大部分时间,而写入操作较少。 + +**示例代码** + +下面是一个使用 ReadWriteLock 的示例,实现了一个简单的缓存系统: + +```java +public class Cache { + private Map data = new HashMap<>(); + private ReadWriteLock lock = new ReentrantReadWriteLock(); + + public Object get(String key) { + lock.readLock().lock(); + try { + return data.get(key); + } finally { + lock.readLock().unlock(); + } + } + + public void put(String key, Object value) { + lock.writeLock().lock(); + try { + data.put(key, value); + } finally { + lock.writeLock().unlock(); + } + } +} +``` + +在上述示例中,Cache 类使用 ReadWriteLock 来实现对 data 的并发访问控制。get 方法获取读锁并读取数据,put 方法获取写锁并写入数据。 + +**StampedLock** + +StampedLock 是Java 8 中引入的一种新的锁机制,全类名:`java.util.concurrent.locks.StampedLock`,它提供了一种乐观读的机制,可以进一步提升读取操作的并发性能。 + +**特性** + +- 与 ReadWriteLock 类似,StampedLock 也支持多个线程同时获取读锁,但只允许一个线程获取写锁。 +- 与 ReadWriteLock 不同的是,StampedLock 还提供了一个乐观读锁(Optimistic Read Lock),即不阻塞其他线程的写操作,但在读取完成后需要验证数据的一致性。 + +**使用场景** + +StampedLock 适用于读远远大于写的场景,并且对数据的一致性要求不高,例如统计数据、监控系统等。 + +**示例代码** + +下面是一个使用 StampedLock 的示例,实现了一个计数器: + +```java +public class Counter { + private int count = 0; + private StampedLock lock = new StampedLock(); + + public int getCount() { + long stamp = lock.tryOptimisticRead(); + int value = count; + if (!lock.validate(stamp)) { + stamp = lock.readLock(); + try { + value = count; + } finally { + lock.unlockRead(stamp); + } + } + return value; + } + + public void increment() { + long stamp = lock.writeLock(); + try { + count++; + } finally { + lock.unlockWrite(stamp); + } + } +} +``` + +在上述示例中,Counter 类使用 StampedLock 来实现对计数器的并发访问控制。getCount 方法首先尝试获取乐观读锁,并读取计数器的值,然后通过 validate 方法验证数据的一致性。如果验证失败,则获取悲观读锁,并重新读取计数器的值。increment 方法获取写锁,并对计数器进行递增操作。 + +**总结** + +**ReadWriteLock** 和 **StampedLock** 都是Java中用于并发控制的重要机制。 + +- **ReadWriteLock** 适用于读多写少的场景; +- **StampedLock** 则适用于读远远大于写的场景,并且对数据的一致性要求不高; + +在实际应用中,我们需要根据具体场景来选择合适的锁机制。通过合理使用这些锁机制,我们可以提高并发程序的性能和可靠性。 + + + ![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/java/jvm.md b/docs/java/jvm.md index 8690bfc..c51ab23 100644 --- a/docs/java/jvm.md +++ b/docs/java/jvm.md @@ -26,6 +26,8 @@ 怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -**扫描以下二维码**领取50元的优惠券即可加入。星球定价**158**元,减去**50**元的优惠券,等于说只需要**108**元的价格就可以加入,服务期一年,**每天不到三毛钱**(0.29元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 + +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 ![](http://img.topjavaer.cn/img/202304212233017.png) \ No newline at end of file diff --git a/docs/note/README.md b/docs/note/README.md new file mode 100644 index 0000000..3a19bc2 --- /dev/null +++ b/docs/note/README.md @@ -0,0 +1,6 @@ +- [一小时彻底吃透Redis](https://topjavaer.cn/note/redis-note.html) +- [21个写SQL的好习惯](https://topjavaer.cn/note/write-sql.html) +- [Docker详解与部署微服务实战](https://topjavaer.cn/note/docker-note.html) +- [计算机专业的同学都看看这几点建议](https://topjavaer.cn/note/computor-advice.html) +- [建议计算机专业同学都看看这门课](https://topjavaer.cn/note/computor-advice.html) + diff --git a/docs/note/redis-note.md b/docs/note/redis-note.md new file mode 100644 index 0000000..02e5569 --- /dev/null +++ b/docs/note/redis-note.md @@ -0,0 +1,28 @@ +::: tip 这是一则或许对你有帮助的信息 + +- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) +- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) + +::: + +## 一小时彻底吃透Redis + +![1](http://img.topjavaer.cn/img/202308210012725.png) + +![](http://img.topjavaer.cn/img/202308210012963.png) + +![3](http://img.topjavaer.cn/img/202308210013864.png) + + + +![4](http://img.topjavaer.cn/img/202308210013844.png) + +![5](http://img.topjavaer.cn/img/202308210013997.png) + +![6](http://img.topjavaer.cn/img/202308210013008.png) + +![7](http://img.topjavaer.cn/img/202308210013799.png) + +![8](http://img.topjavaer.cn/img/202308210013103.png) + +![9](http://img.topjavaer.cn/img/202308210013210.png) \ No newline at end of file diff --git a/docs/other/site-diary.md b/docs/other/site-diary.md index d9b9ac9..711b790 100644 --- a/docs/other/site-diary.md +++ b/docs/other/site-diary.md @@ -8,7 +8,10 @@ sidebar: heading ## 更新记录 +- 2023.08.10,分布式锁实现-[RedLock](http://topjavaer.cn/advance/distributed/2-distributed-lock.html)补充图片 + - 2023.05.04,导航栏增加图标。 + - 2023.05.01,新增[Tomcat基础知识总结](/web/tomcat.html) - 2023.03.25,新增[TCP面试题](/computer-basic/tcp.html)、[MongoDB面试题](/database/mongodb.html)、[ZooKeeper面试题](/zookeeper/zk.html) diff --git a/docs/redis/redis.md b/docs/redis/redis.md index 8a6d471..ff4dc4e 100644 --- a/docs/redis/redis.md +++ b/docs/redis/redis.md @@ -787,4 +787,16 @@ Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Se 对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。 + + +## Redis遇到哈希冲突怎么办? + +当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突(collision)。 + +Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 每个哈希表节点都有一个 `next` 指针, 多个哈希表节点可以用 `next` 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。 + +原理跟 Java 的 HashMap 类似,都是数组+链表的结构。当发生 hash 碰撞时将会把元素追加到链表上。 + + + ![](http://img.topjavaer.cn/img/20220612101342.png) diff --git "a/docs/source/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" "b/docs/source/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" new file mode 100644 index 0000000..ce9103c --- /dev/null +++ "b/docs/source/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" @@ -0,0 +1,73 @@ +## 概述 + +Spring是一个开放源代码的设计层面框架,他解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson创建。简单来说,Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。 + +## spring的整体架构 + +Spring框架是一个分层架构,它包含一系列的功能要素,并被分为大约20个模块,如下图所示: + +![](http://img.topjavaer.cn/img/202309161608576.png) + +从上图spring framework整体架构图可以看到,这些模块被总结为以下几个部分: + +### 1. Core Container + +Core Container(核心容器)包含有Core、Beans、Context和Expression Language模块 + +Core和Beans模块是框架的基础部分,提供IoC(转控制)和依赖注入特性。这里的基础概念是BeanFactory,它提供对Factory模式的经典实现来消除对程序性单例模式的需要,并真正地允许你从程序逻辑中分离出依赖关系和配置。 + +Core模块主要包含Spring框架基本的核心工具类 + +Beans模块是所有应用都要用到的,它包含访问配置文件、创建和管理bean以及进行Inversion of Control/Dependency Injection(Ioc/DI)操作相关的所有类 + +Context模块构建于Core和Beans模块基础之上,提供了一种类似于JNDI注册器的框架式的对象访问方法。Context模块继承了Beans的特性,为Spring核心提供了大量扩展,添加了对国际化(如资源绑定)、事件传播、资源加载和对Context的透明创建的支持。 + +ApplicationContext接口是Context模块的关键 + +Expression Language模块提供了一个强大的表达式语言用于在运行时查询和操纵对象,该语言支持设置/获取属性的值,属性的分配,方法的调用,访问数组上下文、容器和索引器、逻辑和算术运算符、命名变量以及从Spring的IoC容器中根据名称检索对象 + + + +### 2. Data Access/Integration + +JDBC模块提供了一个JDBC抽象层,它可以消除冗长的JDBC编码和解析数据库厂商特有的错误代码,这个模块包含了Spring对JDBC数据访问进行封装的所有类 + +ORM模块为流行的对象-关系映射API,如JPA、JDO、Hibernate、iBatis等,提供了一个交互层,利用ORM封装包,可以混合使用所有Spring提供的特性进行O/R映射,如前边提到的简单声明性事务管理 + +OXM模块提供了一个Object/XML映射实现的抽象层,Object/XML映射实现抽象层包括JAXB,Castor,XMLBeans,JiBX和XStream +JMS(java Message Service)模块主要包含了一些制造和消费消息的特性 + +Transaction模块支持编程和声明式事物管理,这些事务类必须实现特定的接口,并且对所有POJO都适用 + + + +### 3. Web + +Web上下文模块建立在应用程序上下文模块之上,为基于Web的应用程序提供了上下文,所以Spring框架支持与Jakarta Struts的集成。 + +Web模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。Web层包含了Web、Web-Servlet、Web-Struts和Web、Porlet模块 + +Web模块:提供了基础的面向Web的集成特性,例如,多文件上传、使用Servlet listeners初始化IoC容器以及一个面向Web的应用上下文,它还包含了Spring远程支持中Web的相关部分 + +Web-Servlet模块web.servlet.jar:该模块包含Spring的model-view-controller(MVC)实现,Spring的MVC框架使得模型范围内的代码和web forms之间能够清楚地分离开来,并与Spring框架的其他特性基础在一起 + +Web-Struts模块:该模块提供了对Struts的支持,使得类在Spring应用中能够与一个典型的Struts Web层集成在一起 + +Web-Porlet模块:提供了用于Portlet环境和Web-Servlet模块的MVC的实现 + + + +### 4. AOP + +AOP模块提供了一个符合AOP联盟标准的面向切面编程的实现,它让你可以定义例如方法拦截器和切点,从而将逻辑代码分开,降低它们之间的耦合性,利用source-level的元数据功能,还可以将各种行为信息合并到你的代码中 + +Spring AOP模块为基于Spring的应用程序中的对象提供了事务管理服务,通过使用Spring AOP,不用依赖EJB组件,就可以将声明性事务管理集成到应用程序中 + + + +### 5. Test + +Test模块支持使用Junit和TestNG对Spring组件进行测试 + + + diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\212\357\274\211.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\212\357\274\211.md" new file mode 100644 index 0000000..45712c4 --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\212\357\274\211.md" @@ -0,0 +1,864 @@ +## 概述 + +本文主要研究Spring标签的解析,Spring的标签中有默认标签和自定义标签,两者的解析有着很大的不同,这次重点说默认标签的解析过程。 + +默认标签的解析是在DefaultBeanDefinitionDocumentReader.parseDefaultElement函数中进行的,分别对4种不同的标签(import,alias,bean和beans)做了不同处理。我们先看下此函数的源码: + +```java +private void parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate) { + if (delegate.nodeNameEquals(ele, IMPORT_ELEMENT)) { + importBeanDefinitionResource(ele); + } + else if (delegate.nodeNameEquals(ele, ALIAS_ELEMENT)) { + processAliasRegistration(ele); + } + else if (delegate.nodeNameEquals(ele, BEAN_ELEMENT)) { + processBeanDefinition(ele, delegate); + } + else if (delegate.nodeNameEquals(ele, NESTED_BEANS_ELEMENT)) { + // recurse + doRegisterBeanDefinitions(ele); + } +} +``` + +## Bean标签的解析及注册 + +在4中标签中对bean标签的解析最为复杂也最为重要,所以从此标签开始深入分析,如果能理解这个标签的解析过程,其他标签的解析就迎刃而解了。对于bean标签的解析用的是processBeanDefinition函数,首先看看函数processBeanDefinition(ele,delegate),其代码如下: + +```java +protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) { + BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele); + if (bdHolder != null) { + bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder); + try { + // Register the final decorated instance. + BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry()); + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error("Failed to register bean definition with name '" + + bdHolder.getBeanName() + "'", ele, ex); + } + // Send registration event. + getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder)); + } +} +``` + +刚开始看这个函数体时一头雾水,没有以前的函数那样的清晰的逻辑,我们细致的理下逻辑,大致流程如下: + +- 首先委托BeanDefinitionDelegate类的parseBeanDefinitionElement方法进行元素的解析,返回BeanDefinitionHolder类型的实例bdHolder,经过这个方法后bdHolder实例已经包含了我们配置文件中的各种属性了,例如class,name,id,alias等。 + +- 当返回的dbHolder不为空的情况下若存在默认标签的子节点下再有自定义属性,还需要再次对自定义标签进行解析 +- 当解析完成后,需要对解析后的bdHolder进行注册,注册过程委托给了BeanDefinitionReaderUtils的registerBeanDefinition方法。 +- 最后发出响应事件,通知相关的监听器已经加载完这个Bean了。 + +### 解析BeanDefinition + +接下来我们就针对具体的方法进行分析,首先我们从元素解析及信息提取开始,也就是BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele),进入 BeanDefinitionDelegate 类的 parseBeanDefinitionElement 方法。我们看下源码: + +```java +public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { + // 解析 ID 属性 + String id = ele.getAttribute(ID_ATTRIBUTE); + // 解析 name 属性 + String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); + + // 分割 name 属性 + List aliases = new ArrayList<>(); + if (StringUtils.hasLength(nameAttr)) { + String[] nameArr = StringUtils.tokenizeToStringArray(nameAttr, MULTI_VALUE_ATTRIBUTE_DELIMITERS); + aliases.addAll(Arrays.asList(nameArr)); + } + + String beanName = id; + if (!StringUtils.hasText(beanName) && !aliases.isEmpty()) { + beanName = aliases.remove(0); + if (logger.isDebugEnabled()) { + logger.debug("No XML 'id' specified - using '" + beanName + + "' as bean name and " + aliases + " as aliases"); + } + } + + // 检查 name 的唯一性 + if (containingBean == null) { + checkNameUniqueness(beanName, aliases, ele); + } + + // 解析 属性,构造 AbstractBeanDefinition + AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean); + if (beanDefinition != null) { + // 如果 beanName 不存在,则根据条件构造一个 beanName + if (!StringUtils.hasText(beanName)) { + try { + if (containingBean != null) { + beanName = BeanDefinitionReaderUtils.generateBeanName( + beanDefinition, this.readerContext.getRegistry(), true); + } + else { + beanName = this.readerContext.generateBeanName(beanDefinition); + String beanClassName = beanDefinition.getBeanClassName(); + if (beanClassName != null && + beanName.startsWith(beanClassName) && beanName.length() > beanClassName.length() && + !this.readerContext.getRegistry().isBeanNameInUse(beanClassName)) { + aliases.add(beanClassName); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Neither XML 'id' nor 'name' specified - " + + "using generated bean name [" + beanName + "]"); + } + } + catch (Exception ex) { + error(ex.getMessage(), ele); + return null; + } + } + String[] aliasesArray = StringUtils.toStringArray(aliases); + + // 封装 BeanDefinitionHolder + return new BeanDefinitionHolder(beanDefinition, beanName, aliasesArray); + } + + return null; +} +``` + +上述方法就是对默认标签解析的全过程了,我们分析下当前层完成的工作: + +(1)提取元素中的id和name属性 +(2)进一步解析其他所有属性并统一封装到GenericBeanDefinition类型的实例中 +(3)如果检测到bean没有指定beanName,那么使用默认规则为此bean生成beanName。 +(4)将获取到的信息封装到BeanDefinitionHolder的实例中。 + +代码:`AbstractBeanDefinition beanDefinition = parseBeanDefinitionElement(ele, beanName, containingBean);`是用来对标签中的其他属性进行解析,我们详细看下源码: + +```java +public AbstractBeanDefinition parseBeanDefinitionElement( + Element ele, String beanName, @Nullable BeanDefinition containingBean) { + + this.parseState.push(new BeanEntry(beanName)); + + String className = null; + //解析class属性 + if (ele.hasAttribute(CLASS_ATTRIBUTE)) { + className = ele.getAttribute(CLASS_ATTRIBUTE).trim(); + } + String parent = null; + //解析parent属性 + if (ele.hasAttribute(PARENT_ATTRIBUTE)) { + parent = ele.getAttribute(PARENT_ATTRIBUTE); + } + + try { + //创建用于承载属性的AbstractBeanDefinition类型的GenericBeanDefinition实例 + AbstractBeanDefinition bd = createBeanDefinition(className, parent); + //硬编码解析bean的各种属性 + parseBeanDefinitionAttributes(ele, beanName, containingBean, bd); + //设置description属性 + bd.setDescription(DomUtils.getChildElementValueByTagName(ele, DESCRIPTION_ELEMENT)); + //解析元素 + parseMetaElements(ele, bd); + //解析lookup-method属性 + parseLookupOverrideSubElements(ele, bd.getMethodOverrides()); + //解析replace-method属性 + parseReplacedMethodSubElements(ele, bd.getMethodOverrides()); + //解析构造函数的参数 + parseConstructorArgElements(ele, bd); + //解析properties子元素 + parsePropertyElements(ele, bd); + //解析qualifier子元素 + parseQualifierElements(ele, bd); + bd.setResource(this.readerContext.getResource()); + bd.setSource(extractSource(ele)); + + return bd; + } + catch (ClassNotFoundException ex) { + error("Bean class [" + className + "] not found", ele, ex); + } + catch (NoClassDefFoundError err) { + error("Class that bean class [" + className + "] depends on not found", ele, err); + } + catch (Throwable ex) { + error("Unexpected failure during bean definition parsing", ele, ex); + } + finally { + this.parseState.pop(); + } + + return null; +} +``` + +接下来我们一步步分析解析过程。 + +## bean详细解析过程 + +### 创建用于承载属性的BeanDefinition + +BeanDefinition是一个接口,在spring中此接口有三种实现:RootBeanDefinition、ChildBeanDefinition已经GenericBeanDefinition。而三种实现都继承了AbstractBeanDefinition,其中BeanDefinition是配置文件元素标签在容器中的内部表示形式。元素标签拥有class、scope、lazy-init等属性,BeanDefinition则提供了相应的beanClass、scope、lazyInit属性,BeanDefinition和中的属性一一对应。其中RootBeanDefinition是最常用的实现类,他对应一般性的元素标签,GenericBeanDefinition是自2.5版本以后新加入的bean文件配置属性定义类,是一站式服务的。 + +在配置文件中可以定义父和字,父用RootBeanDefinition表示,而子用ChildBeanDefinition表示,而没有父的就使用RootBeanDefinition表示。AbstractBeanDefinition对两者共同的类信息进行抽象。 + +Spring通过BeanDefinition将配置文件中的配置信息转换为容器的内部表示,并将这些BeanDefinition注册到BeanDefinitionRegistry中。Spring容器的BeanDefinitionRegistry就像是Spring配置信息的内存数据库,主要是以map的形式保存,后续操作直接从BeanDefinitionResistry中读取配置信息。它们之间的关系如下图所示: + +![](http://img.topjavaer.cn/img/202309171708954.png) + +因此,要解析属性首先要创建用于承载属性的实例,也就是创建GenericBeanDefinition类型的实例。而代码createBeanDefinition(className,parent)的作用就是实现此功能。我们详细看下方法体,代码如下: + +```java +protected AbstractBeanDefinition createBeanDefinition(@Nullable String className, @Nullable String parentName) + throws ClassNotFoundException { + + return BeanDefinitionReaderUtils.createBeanDefinition( + parentName, className, this.readerContext.getBeanClassLoader()); +} +public static AbstractBeanDefinition createBeanDefinition( + @Nullable String parentName, @Nullable String className, @Nullable ClassLoader classLoader) throws ClassNotFoundException { + + GenericBeanDefinition bd = new GenericBeanDefinition(); + bd.setParentName(parentName); + if (className != null) { + if (classLoader != null) { + bd.setBeanClass(ClassUtils.forName(className, classLoader)); + } + else { + bd.setBeanClassName(className); + } + } + return bd; +} +``` + + + +### 各种属性的解析 + + 当创建好了承载bean信息的实例后,接下来就是解析各种属性了,首先我们看下parseBeanDefinitionAttributes(ele, beanName, containingBean, bd);方法,代码如下: + +```java +public AbstractBeanDefinition parseBeanDefinitionAttributes(Element ele, String beanName, + @Nullable BeanDefinition containingBean, AbstractBeanDefinition bd) { + //解析singleton属性 + if (ele.hasAttribute(SINGLETON_ATTRIBUTE)) { + error("Old 1.x 'singleton' attribute in use - upgrade to 'scope' declaration", ele); + } + //解析scope属性 + else if (ele.hasAttribute(SCOPE_ATTRIBUTE)) { + bd.setScope(ele.getAttribute(SCOPE_ATTRIBUTE)); + } + else if (containingBean != null) { + // Take default from containing bean in case of an inner bean definition. + bd.setScope(containingBean.getScope()); + } + //解析abstract属性 + if (ele.hasAttribute(ABSTRACT_ATTRIBUTE)) { +bd.setAbstract(TRUE_VALUE.equals(ele.getAttribute(ABSTRACT_ATTRIBUTE))); + } + //解析lazy_init属性 + String lazyInit = ele.getAttribute(LAZY_INIT_ATTRIBUTE); + if (DEFAULT_VALUE.equals(lazyInit)) { + lazyInit = this.defaults.getLazyInit(); + } + bd.setLazyInit(TRUE_VALUE.equals(lazyInit)); + //解析autowire属性 + String autowire = ele.getAttribute(AUTOWIRE_ATTRIBUTE); + bd.setAutowireMode(getAutowireMode(autowire)); + //解析dependsOn属性 + if (ele.hasAttribute(DEPENDS_ON_ATTRIBUTE)) { + String dependsOn = ele.getAttribute(DEPENDS_ON_ATTRIBUTE); + bd.setDependsOn(StringUtils.tokenizeToStringArray(dependsOn, MULTI_VALUE_ATTRIBUTE_DELIMITERS)); + } + //解析autowireCandidate属性 + String autowireCandidate = ele.getAttribute(AUTOWIRE_CANDIDATE_ATTRIBUTE); + if ("".equals(autowireCandidate) || DEFAULT_VALUE.equals(autowireCandidate)) { + String candidatePattern = this.defaults.getAutowireCandidates(); + if (candidatePattern != null) { + String[] patterns = StringUtils.commaDelimitedListToStringArray(candidatePattern); + bd.setAutowireCandidate(PatternMatchUtils.simpleMatch(patterns, beanName)); + } + } + else { + bd.setAutowireCandidate(TRUE_VALUE.equals(autowireCandidate)); + } + //解析primary属性 + if (ele.hasAttribute(PRIMARY_ATTRIBUTE)) { + bd.setPrimary(TRUE_VALUE.equals(ele.getAttribute(PRIMARY_ATTRIBUTE))); + } + //解析init_method属性 + if (ele.hasAttribute(INIT_METHOD_ATTRIBUTE)) { + String initMethodName = ele.getAttribute(INIT_METHOD_ATTRIBUTE); + bd.setInitMethodName(initMethodName); + } + else if (this.defaults.getInitMethod() != null) { + bd.setInitMethodName(this.defaults.getInitMethod()); + bd.setEnforceInitMethod(false); + } + //解析destroy_method属性 + if (ele.hasAttribute(DESTROY_METHOD_ATTRIBUTE)) { + String destroyMethodName = ele.getAttribute(DESTROY_METHOD_ATTRIBUTE); + bd.setDestroyMethodName(destroyMethodName); + } + else if (this.defaults.getDestroyMethod() != null) { + bd.setDestroyMethodName(this.defaults.getDestroyMethod()); + bd.setEnforceDestroyMethod(false); + } + //解析factory_method属性 + if (ele.hasAttribute(FACTORY_METHOD_ATTRIBUTE)) { + bd.setFactoryMethodName(ele.getAttribute(FACTORY_METHOD_ATTRIBUTE)); + } + //解析factory_bean属性 + if (ele.hasAttribute(FACTORY_BEAN_ATTRIBUTE)) { + bd.setFactoryBeanName(ele.getAttribute(FACTORY_BEAN_ATTRIBUTE)); + } + + return bd; +} +``` + + + +### 解析meta元素 + + 在开始对meta元素解析分析前我们先简单回顾下meta属性的使用,简单的示例代码如下: + +```xml + + + + +``` + +这段代码并不会提现在demo的属性中,而是一个额外的声明,如果需要用到这里面的信息时可以通过BeanDefinition的getAttribute(key)方法获取,对meta属性的解析用的是:parseMetaElements(ele, bd);具体的方法体如下: + +```java +public void parseMetaElements(Element ele, BeanMetadataAttributeAccessor attributeAccessor) { + NodeList nl = ele.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, META_ELEMENT)) { + Element metaElement = (Element) node; + String key = metaElement.getAttribute(KEY_ATTRIBUTE); + String value = metaElement.getAttribute(VALUE_ATTRIBUTE); + BeanMetadataAttribute attribute = new BeanMetadataAttribute(key, value); + attribute.setSource(extractSource(metaElement)); + attributeAccessor.addMetadataAttribute(attribute); + } + } +} +``` + + + +### 解析replaced-method属性 + + 在分析代码前我们还是先简单的了解下replaced-method的用法,其主要功能是方法替换:即在运行时用新的方法替换旧的方法。与之前的lookup-method不同的是此方法不仅可以替换返回的bean,还可以动态的更改原有方法的运行逻辑,我们看下使用: + +```java +//原有的changeMe方法 +public class TestChangeMethod { + public void changeMe() + { + System.out.println("ChangeMe"); + } +} +//新的实现方法 +public class ReplacerChangeMethod implements MethodReplacer { + public Object reimplement(Object o, Method method, Object[] objects) throws Throwable { + System.out.println("I Replace Method"); + return null; + } +} +//新的配置文件 + + + + + + + + +//测试方法 +public class TestDemo { + public static void main(String[] args) { + ApplicationContext context = new ClassPathXmlApplicationContext("replaced-method.xml"); + + TestChangeMethod test =(TestChangeMethod) context.getBean("changeMe"); + + test.changeMe(); + } +} +``` + +接下来我们看下解析replaced-method的方法代码: + +```java +public void parseReplacedMethodSubElements(Element beanEle, MethodOverrides overrides) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, REPLACED_METHOD_ELEMENT)) { + Element replacedMethodEle = (Element) node; + String name = replacedMethodEle.getAttribute(NAME_ATTRIBUTE); + String callback = replacedMethodEle.getAttribute(REPLACER_ATTRIBUTE); + ReplaceOverride replaceOverride = new ReplaceOverride(name, callback); + // Look for arg-type match elements. + List argTypeEles = DomUtils.getChildElementsByTagName(replacedMethodEle, ARG_TYPE_ELEMENT); + for (Element argTypeEle : argTypeEles) { + String match = argTypeEle.getAttribute(ARG_TYPE_MATCH_ATTRIBUTE); + match = (StringUtils.hasText(match) ? match : DomUtils.getTextValue(argTypeEle)); + if (StringUtils.hasText(match)) { + replaceOverride.addTypeIdentifier(match); + } + } + replaceOverride.setSource(extractSource(replacedMethodEle)); + overrides.addOverride(replaceOverride); + } + } +} +``` + +我们可以看到无论是 look-up 还是 replaced-method 是构造了 MethodOverride ,并最终记录在了 AbstractBeanDefinition 中的 methodOverrides 属性中 + +### 解析constructor-arg + +对构造函数的解析式非常常用,也是非常复杂的,我们先从一个简单配置构造函数的例子开始分析,代码如下: + +```java +public void parseConstructorArgElement(Element ele, BeanDefinition bd) { + //提前index属性 + String indexAttr = ele.getAttribute(INDEX_ATTRIBUTE); + //提前type属性 + String typeAttr = ele.getAttribute(TYPE_ATTRIBUTE); + //提取name属性 + String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); + if (StringUtils.hasLength(indexAttr)) { + try { + int index = Integer.parseInt(indexAttr); + if (index < 0) { + error("'index' cannot be lower than 0", ele); + } + else { + try { + this.parseState.push(new ConstructorArgumentEntry(index)); + //解析ele对应的元素属性 + Object value = parsePropertyValue(ele, bd, null); + ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value); + if (StringUtils.hasLength(typeAttr)) { + valueHolder.setType(typeAttr); + } + if (StringUtils.hasLength(nameAttr)) { + valueHolder.setName(nameAttr); + } + valueHolder.setSource(extractSource(ele)); + if (bd.getConstructorArgumentValues().hasIndexedArgumentValue(index)) { + error("Ambiguous constructor-arg entries for index " + index, ele); + } + else { + bd.getConstructorArgumentValues().addIndexedArgumentValue(index, valueHolder); + } + } + finally { + this.parseState.pop(); + } + } + } + catch (NumberFormatException ex) { + error("Attribute 'index' of tag 'constructor-arg' must be an integer", ele); + } + } + else { + try { + this.parseState.push(new ConstructorArgumentEntry()); + Object value = parsePropertyValue(ele, bd, null); + ConstructorArgumentValues.ValueHolder valueHolder = new ConstructorArgumentValues.ValueHolder(value); + if (StringUtils.hasLength(typeAttr)) { + valueHolder.setType(typeAttr); + } + if (StringUtils.hasLength(nameAttr)) { + valueHolder.setName(nameAttr); + } + valueHolder.setSource(extractSource(ele)); + bd.getConstructorArgumentValues().addGenericArgumentValue(valueHolder); + } + finally { + this.parseState.pop(); + } + } +} +``` + +上述代码的流程可以简单的总结为如下: + +(1)首先提取index、type、name等属性 +(2)根据是否配置了index属性解析流程不同 + +如果配置了index属性,解析流程如下: + +(1)使用parsePropertyValue(ele, bd, null)方法读取constructor-arg的子元素 + +(2)使用ConstructorArgumentValues.ValueHolder封装解析出来的元素 + +(3)将index、type、name属性也封装进ValueHolder中,然后将ValueHoder添加到当前beanDefinition的ConstructorArgumentValues的indexedArgumentValues,而indexedArgumentValues是一个map类型 + +如果没有配置index属性,将index、type、name属性也封装进ValueHolder中,然后将ValueHoder添加到当前beanDefinition的ConstructorArgumentValues的genericArgumentValues中 + +```java +public Object parsePropertyValue(Element ele, BeanDefinition bd, @Nullable String propertyName) { + String elementName = (propertyName != null) ? + " element for property '" + propertyName + "'" : + " element"; + + // Should only have one child element: ref, value, list, etc. + NodeList nl = ele.getChildNodes(); + Element subElement = null; + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + //略过description和meta属性 + if (node instanceof Element && !nodeNameEquals(node, DESCRIPTION_ELEMENT) && + !nodeNameEquals(node, META_ELEMENT)) { + // Child element is what we're looking for. + if (subElement != null) { + error(elementName + " must not contain more than one sub-element", ele); + } + else { + subElement = (Element) node; + } + } + } + //解析ref属性 + boolean hasRefAttribute = ele.hasAttribute(REF_ATTRIBUTE); + //解析value属性 + boolean hasValueAttribute = ele.hasAttribute(VALUE_ATTRIBUTE); + if ((hasRefAttribute && hasValueAttribute) || + ((hasRefAttribute || hasValueAttribute) && subElement != null)) { + error(elementName + + " is only allowed to contain either 'ref' attribute OR 'value' attribute OR sub-element", ele); + } + + if (hasRefAttribute) { + String refName = ele.getAttribute(REF_ATTRIBUTE); + if (!StringUtils.hasText(refName)) { + error(elementName + " contains empty 'ref' attribute", ele); + } + //使用RuntimeBeanReference来封装ref对应的bean + RuntimeBeanReference ref = new RuntimeBeanReference(refName); + ref.setSource(extractSource(ele)); + return ref; + } + else if (hasValueAttribute) { + //使用TypedStringValue 来封装value属性 + TypedStringValue valueHolder = new TypedStringValue(ele.getAttribute(VALUE_ATTRIBUTE)); + valueHolder.setSource(extractSource(ele)); + return valueHolder; + } + else if (subElement != null) { + //解析子元素 + return parsePropertySubElement(subElement, bd); + } + else { + // Neither child element nor "ref" or "value" attribute found. + error(elementName + " must specify a ref or value", ele); + return null; + } +} +``` + +上述代码的执行逻辑简单总结为: + +(1)首先略过decription和meta属性 + +(2)提取constructor-arg上的ref和value属性,并验证是否存在 + +(3)存在ref属性时,用RuntimeBeanReference来封装ref + +(4)存在value属性时,用TypedStringValue来封装 + +(5)存在子元素时,对于子元素的处理使用了方法parsePropertySubElement(subElement, bd);,其代码如下: + +```java +public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) { + return parsePropertySubElement(ele, bd, null); +} +public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, @Nullable String defaultValueType) { + //判断是否是默认标签处理 + if (!isDefaultNamespace(ele)) { + return parseNestedCustomElement(ele, bd); + } + //对于bean标签的处理 + else if (nodeNameEquals(ele, BEAN_ELEMENT)) { + BeanDefinitionHolder nestedBd = parseBeanDefinitionElement(ele, bd); + if (nestedBd != null) { + nestedBd = decorateBeanDefinitionIfRequired(ele, nestedBd, bd); + } + return nestedBd; + } + else if (nodeNameEquals(ele, REF_ELEMENT)) { + // A generic reference to any name of any bean. + String refName = ele.getAttribute(BEAN_REF_ATTRIBUTE); + boolean toParent = false; + if (!StringUtils.hasLength(refName)) { + // A reference to the id of another bean in a parent context. + refName = ele.getAttribute(PARENT_REF_ATTRIBUTE); + toParent = true; + if (!StringUtils.hasLength(refName)) { + error("'bean' or 'parent' is required for element", ele); + return null; + } + } + if (!StringUtils.hasText(refName)) { + error(" element contains empty target attribute", ele); + return null; + } + RuntimeBeanReference ref = new RuntimeBeanReference(refName, toParent); + ref.setSource(extractSource(ele)); + return ref; + } + //idref元素处理 + else if (nodeNameEquals(ele, IDREF_ELEMENT)) { + return parseIdRefElement(ele); + } + //value元素处理 + else if (nodeNameEquals(ele, VALUE_ELEMENT)) { + return parseValueElement(ele, defaultValueType); + } + //null元素处理 + else if (nodeNameEquals(ele, NULL_ELEMENT)) { + // It's a distinguished null value. Let's wrap it in a TypedStringValue + // object in order to preserve the source location. + TypedStringValue nullHolder = new TypedStringValue(null); + nullHolder.setSource(extractSource(ele)); + return nullHolder; + } + //array元素处理 + else if (nodeNameEquals(ele, ARRAY_ELEMENT)) { + return parseArrayElement(ele, bd); + } + //list元素处理 + else if (nodeNameEquals(ele, LIST_ELEMENT)) { + return parseListElement(ele, bd); + } + //set元素处理 + else if (nodeNameEquals(ele, SET_ELEMENT)) { + return parseSetElement(ele, bd); + } + //map元素处理 + else if (nodeNameEquals(ele, MAP_ELEMENT)) { + return parseMapElement(ele, bd); + } + //props元素处理 + else if (nodeNameEquals(ele, PROPS_ELEMENT)) { + return parsePropsElement(ele); + } + else { + error("Unknown property sub-element: [" + ele.getNodeName() + "]", ele); + return null; + } +} +``` + + + +### 解析子元素properties + +对于propertie元素的解析是使用的parsePropertyElements(ele, bd);方法,我们看下其源码如下: + +```java +public void parsePropertyElements(Element beanEle, BeanDefinition bd) { + NodeList nl = beanEle.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (isCandidateElement(node) && nodeNameEquals(node, PROPERTY_ELEMENT)) { + parsePropertyElement((Element) node, bd); + } + } +} +``` + +里面实际的解析是用的parsePropertyElement((Element) node, bd);方法,继续跟踪代码: + +```java +public void parsePropertyElement(Element ele, BeanDefinition bd) { + String propertyName = ele.getAttribute(NAME_ATTRIBUTE); + if (!StringUtils.hasLength(propertyName)) { + error("Tag 'property' must have a 'name' attribute", ele); + return; + } + this.parseState.push(new PropertyEntry(propertyName)); + try { + //不允许多次对同一属性配置 + if (bd.getPropertyValues().contains(propertyName)) { + error("Multiple 'property' definitions for property '" + propertyName + "'", ele); + return; + } + Object val = parsePropertyValue(ele, bd, propertyName); + PropertyValue pv = new PropertyValue(propertyName, val); + parseMetaElements(ele, pv); + pv.setSource(extractSource(ele)); + bd.getPropertyValues().addPropertyValue(pv); + } + finally { + this.parseState.pop(); + } +} +``` + +我们看到代码的逻辑非常简单,在获取了propertie的属性后使用PropertyValue 进行封装,然后将其添加到BeanDefinition的propertyValueList中 + +### 解析子元素 qualifier + +对于 qualifier 元素的获取,我们接触更多的是注解的形式,在使用 Spring 架中进行自动注入时,Spring 器中匹配的候选 Bean 数目必须有且仅有一个,当找不到一个匹配的 Bean 时, Spring容器将抛出 BeanCreationException 异常, 并指出必须至少拥有一个匹配的 Bean。 + +Spring 允许我们通过Qualifier 指定注入 Bean的名称,这样歧义就消除了,而对于配置方式使用如: + +```xml + + + +``` + +其解析过程与之前大同小异 这里不再重复叙述 + +至此我们便完成了对 XML 文档到 GenericBeanDefinition 的转换, 就是说到这里, XML 中所有的配置都可以在 GenericBeanDefinition的实例类中应找到对应的配置。 + +GenericBeanDefinition 只是子类实现,而大部分的通用属性都保存在了 bstractBeanDefinition 中,那么我们再次通过 AbstractBeanDefinition 的属性来回顾一 下我们都解析了哪些对应的配置。 + +```java +public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccessor + implements BeanDefinition, Cloneable { + + // 此处省略静态变量以及final变量 + + @Nullable + private volatile Object beanClass; + /** + * bean的作用范围,对应bean属性scope + */ + @Nullable + private String scope = SCOPE_DEFAULT; + /** + * 是否是抽象,对应bean属性abstract + */ + private boolean abstractFlag = false; + /** + * 是否延迟加载,对应bean属性lazy-init + */ + private boolean lazyInit = false; + /** + * 自动注入模式,对应bean属性autowire + */ + private int autowireMode = AUTOWIRE_NO; + /** + * 依赖检查,Spring 3.0后弃用这个属性 + */ + private int dependencyCheck = DEPENDENCY_CHECK_NONE; + /** + * 用来表示一个bean的实例化依靠另一个bean先实例化,对应bean属性depend-on + */ + @Nullable + private String[] dependsOn; + /** + * autowire-candidate属性设置为false,这样容器在查找自动装配对象时, + * 将不考虑该bean,即它不会被考虑作为其他bean自动装配的候选者, + * 但是该bean本身还是可以使用自动装配来注入其他bean的 + */ + private boolean autowireCandidate = true; + /** + * 自动装配时出现多个bean候选者时,将作为首选者,对应bean属性primary + */ + private boolean primary = false; + /** + * 用于记录Qualifier,对应子元素qualifier + */ + private final Map qualifiers = new LinkedHashMap<>(0); + + @Nullable + private Supplier instanceSupplier; + /** + * 允许访问非公开的构造器和方法,程序设置 + */ + private boolean nonPublicAccessAllowed = true; + /** + * 是否以一种宽松的模式解析构造函数,默认为true, + * 如果为false,则在以下情况 + * interface ITest{} + * class ITestImpl implements ITest{}; + * class Main { + * Main(ITest i) {} + * Main(ITestImpl i) {} + * } + * 抛出异常,因为Spring无法准确定位哪个构造函数程序设置 + */ + private boolean lenientConstructorResolution = true; + /** + * 对应bean属性factory-bean,用法: + * + * + */ + @Nullable + private String factoryBeanName; + /** + * 对应bean属性factory-method + */ + @Nullable + private String factoryMethodName; + /** + * 记录构造函数注入属性,对应bean属性constructor-arg + */ + @Nullable + private ConstructorArgumentValues constructorArgumentValues; + /** + * 普通属性集合 + */ + @Nullable + private MutablePropertyValues propertyValues; + /** + * 方法重写的持有者,记录lookup-method、replaced-method元素 + */ + @Nullable + private MethodOverrides methodOverrides; + /** + * 初始化方法,对应bean属性init-method + */ + @Nullable + private String initMethodName; + /** + * 销毁方法,对应bean属性destroy-method + */ + @Nullable + private String destroyMethodName; + /** + * 是否执行init-method,程序设置 + */ + private boolean enforceInitMethod = true; + /** + * 是否执行destroy-method,程序设置 + */ + private boolean enforceDestroyMethod = true; + /** + * 是否是用户定义的而不是应用程序本身定义的,创建AOP时候为true,程序设置 + */ + private boolean synthetic = false; + /** + * 定义这个bean的应用,APPLICATION:用户,INFRASTRUCTURE:完全内部使用,与用户无关, + * SUPPORT:某些复杂配置的一部分 + * 程序设置 + */ + private int role = BeanDefinition.ROLE_APPLICATION; + /** + * bean的描述信息 + */ + @Nullable + private String description; + /** + * 这个bean定义的资源 + */ + @Nullable + private Resource resource; +} +``` \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\213\357\274\211.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\213\357\274\211.md" new file mode 100644 index 0000000..b92d7e7 --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\213\357\274\211.md" @@ -0,0 +1,501 @@ +**正文** + +在上一篇我们已经完成了从xml配置文件到BeanDefinition的转换,转换后的实例是GenericBeanDefinition的实例。本文主要来看看标签解析剩余部分及BeanDefinition的注册。 + +## 默认标签中的自定义标签解析 + +在上篇博文中我们已经分析了对于默认标签的解析,我们继续看戏之前的代码,如下图片中有一个方法:delegate.decorateBeanDefinitionIfRequired(ele, bdHolder) + +![](http://img.topjavaer.cn/img/202309180843559.png) + +这个方法的作用是什么呢?首先我们看下这种场景,如下配置文件: + +```xml + + + + + +``` + +这个配置文件中有个自定义的标签,decorateBeanDefinitionIfRequired方法就是用来处理这种情况的,其中的null是用来传递父级BeanDefinition的,我们进入到其方法体: + +```java +public BeanDefinitionHolder decorateBeanDefinitionIfRequired(Element ele, BeanDefinitionHolder definitionHolder) { + return decorateBeanDefinitionIfRequired(ele, definitionHolder, null); +} +public BeanDefinitionHolder decorateBeanDefinitionIfRequired( + Element ele, BeanDefinitionHolder definitionHolder, @Nullable BeanDefinition containingBd) { + + BeanDefinitionHolder finalDefinition = definitionHolder; + + // Decorate based on custom attributes first. + NamedNodeMap attributes = ele.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node node = attributes.item(i); + finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); + } + + // Decorate based on custom nested elements. + NodeList children = ele.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node node = children.item(i); + if (node.getNodeType() == Node.ELEMENT_NODE) { + finalDefinition = decorateIfRequired(node, finalDefinition, containingBd); + } + } + return finalDefinition; +} +``` + +我们看到上面的代码有两个遍历操作,一个是用于对所有的属性进行遍历处理,另一个是对所有的子节点进行处理,两个遍历操作都用到了decorateIfRequired(node, finalDefinition, containingBd);方法,我们继续跟踪代码,进入方法体: + +```java +public BeanDefinitionHolder decorateIfRequired( + Node node, BeanDefinitionHolder originalDef, @Nullable BeanDefinition containingBd) { + // 获取自定义标签的命名空间 + String namespaceUri = getNamespaceURI(node); + // 过滤掉默认命名标签 + if (namespaceUri != null && !isDefaultNamespace(namespaceUri)) { + // 获取相应的处理器 + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + if (handler != null) { + // 进行装饰处理 + BeanDefinitionHolder decorated = + handler.decorate(node, originalDef, new ParserContext(this.readerContext, this, containingBd)); + if (decorated != null) { + return decorated; + } + } + else if (namespaceUri.startsWith("http://www.springframework.org/")) { + error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", node); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("No Spring NamespaceHandler found for XML schema namespace [" + namespaceUri + "]"); + } + } + } + return originalDef; +} + +public String getNamespaceURI(Node node) { + return node.getNamespaceURI(); +} + +public boolean isDefaultNamespace(@Nullable String namespaceUri) { + //BEANS_NAMESPACE_URI = "http://www.springframework.org/schema/beans"; + return (!StringUtils.hasLength(namespaceUri) || BEANS_NAMESPACE_URI.equals(namespaceUri)); +} +``` + +首先获取自定义标签的命名空间,如果不是默认的命名空间则根据该命名空间获取相应的处理器,最后调用处理器的 `decorate()` 进行装饰处理。具体的装饰过程这里不进行讲述,在后面分析自定义标签时会做详细说明。 + + + +## 注册解析的BeanDefinition + +对于配置文件,解析和装饰完成之后,对于得到的beanDefinition已经可以满足后续的使用要求了,还剩下注册,也就是processBeanDefinition函数中的BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder,getReaderContext().getRegistry())代码的解析了。进入方法体: + +```java +public static void registerBeanDefinition( + BeanDefinitionHolder definitionHolder, BeanDefinitionRegistry registry) + throws BeanDefinitionStoreException { + // Register bean definition under primary name. + //使用beanName做唯一标识注册 + String beanName = definitionHolder.getBeanName(); + registry.registerBeanDefinition(beanName, definitionHolder.getBeanDefinition()); + + // Register aliases for bean name, if any. + //注册所有的别名 + String[] aliases = definitionHolder.getAliases(); + if (aliases != null) { + for (String alias : aliases) { + registry.registerAlias(beanName, alias); + } + } +} +``` + +从上面的代码我们看到是用了beanName作为唯一标示进行注册的,然后注册了所有的别名aliase。而beanDefinition最终都是注册到BeanDefinitionRegistry中,接下来我们具体看下注册流程。 + + + +## 通过beanName注册BeanDefinition + +在spring中除了使用beanName作为key将BeanDefinition放入Map中还做了其他一些事情,我们看下方法registerBeanDefinition代码,BeanDefinitionRegistry是一个接口,他有三个实现类,DefaultListableBeanFactory、SimpleBeanDefinitionRegistry、GenericApplicationContext,其中SimpleBeanDefinitionRegistry非常简单,而GenericApplicationContext最终也是使用的DefaultListableBeanFactory中的实现方法,我们看下代码: + +```java +public void registerBeanDefinition(String beanName, BeanDefinition beanDefinition) + throws BeanDefinitionStoreException { + + // 校验 beanName 与 beanDefinition + Assert.hasText(beanName, "Bean name must not be empty"); + Assert.notNull(beanDefinition, "BeanDefinition must not be null"); + + if (beanDefinition instanceof AbstractBeanDefinition) { + try { + // 校验 BeanDefinition + // 这是注册前的最后一次校验了,主要是对属性 methodOverrides 进行校验 + ((AbstractBeanDefinition) beanDefinition).validate(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, + "Validation of bean definition failed", ex); + } + } + + BeanDefinition oldBeanDefinition; + + // 从缓存中获取指定 beanName 的 BeanDefinition + oldBeanDefinition = this.beanDefinitionMap.get(beanName); + /** + * 如果存在 + */ + if (oldBeanDefinition != null) { + // 如果存在但是不允许覆盖,抛出异常 + if (!isAllowBeanDefinitionOverriding()) { + throw new BeanDefinitionStoreException(beanDefinition.getResourceDescription(), beanName, + "Cannot register bean definition [" + beanDefinition + "] for bean '" + beanName + + "': There is already [" + oldBeanDefinition + "] bound."); + } + // + else if (oldBeanDefinition.getRole() < beanDefinition.getRole()) { + // e.g. was ROLE_APPLICATION, now overriding with ROLE_SUPPORT or ROLE_INFRASTRUCTURE + if (this.logger.isWarnEnabled()) { + this.logger.warn("Overriding user-defined bean definition for bean '" + beanName + + "' with a framework-generated bean definition: replacing [" + + oldBeanDefinition + "] with [" + beanDefinition + "]"); + } + } + // 覆盖 beanDefinition 与 被覆盖的 beanDefinition 不是同类 + else if (!beanDefinition.equals(oldBeanDefinition)) { + if (this.logger.isInfoEnabled()) { + this.logger.info("Overriding bean definition for bean '" + beanName + + "' with a different definition: replacing [" + oldBeanDefinition + + "] with [" + beanDefinition + "]"); + } + } + else { + if (this.logger.isDebugEnabled()) { + this.logger.debug("Overriding bean definition for bean '" + beanName + + "' with an equivalent definition: replacing [" + oldBeanDefinition + + "] with [" + beanDefinition + "]"); + } + } + + // 允许覆盖,直接覆盖原有的 BeanDefinition + this.beanDefinitionMap.put(beanName, beanDefinition); + } + /** + * 不存在 + */ + else { + // 检测创建 Bean 阶段是否已经开启,如果开启了则需要对 beanDefinitionMap 进行并发控制 + if (hasBeanCreationStarted()) { + // beanDefinitionMap 为全局变量,避免并发情况 + synchronized (this.beanDefinitionMap) { + // + this.beanDefinitionMap.put(beanName, beanDefinition); + List updatedDefinitions = new ArrayList<>(this.beanDefinitionNames.size() + 1); + updatedDefinitions.addAll(this.beanDefinitionNames); + updatedDefinitions.add(beanName); + this.beanDefinitionNames = updatedDefinitions; + if (this.manualSingletonNames.contains(beanName)) { + Set updatedSingletons = new LinkedHashSet<>(this.manualSingletonNames); + updatedSingletons.remove(beanName); + this.manualSingletonNames = updatedSingletons; + } + } + } + else { + // 不会存在并发情况,直接设置 + this.beanDefinitionMap.put(beanName, beanDefinition); + this.beanDefinitionNames.add(beanName); + this.manualSingletonNames.remove(beanName); + } + this.frozenBeanDefinitionNames = null; + } + + if (oldBeanDefinition != null || containsSingleton(beanName)) { + // 重新设置 beanName 对应的缓存 + resetBeanDefinition(beanName); + } +} +``` + +处理过程如下: + +- 首先 BeanDefinition 进行校验,该校验也是注册过程中的最后一次校验了,主要是对 AbstractBeanDefinition 的 methodOverrides 属性进行校验 +- 根据 beanName 从缓存中获取 BeanDefinition,如果缓存中存在,则根据 allowBeanDefinitionOverriding 标志来判断是否允许覆盖,如果允许则直接覆盖,否则抛出 BeanDefinitionStoreException 异常 +- 若缓存中没有指定 beanName 的 BeanDefinition,则判断当前阶段是否已经开始了 Bean 的创建阶段(),如果是,则需要对 beanDefinitionMap 进行加锁控制并发问题,否则直接设置即可。对于 `hasBeanCreationStarted()` 方法后续做详细介绍,这里不过多阐述。 +- 若缓存中存在该 beanName 或者 单利 bean 集合中存在该 beanName,则调用 `resetBeanDefinition()` 重置 BeanDefinition 缓存。 + +其实整段代码的核心就在于 `this.beanDefinitionMap.put(beanName, beanDefinition);` 。BeanDefinition 的缓存也不是神奇的东西,就是定义 一个 ConcurrentHashMap,key 为 beanName,value 为 BeanDefinition。 + + + +## 通过别名注册BeanDefinition + +通过别名注册BeanDefinition最终是在SimpleBeanDefinitionRegistry中实现的,我们看下代码: + +```java +public void registerAlias(String name, String alias) { + Assert.hasText(name, "'name' must not be empty"); + Assert.hasText(alias, "'alias' must not be empty"); + synchronized (this.aliasMap) { + if (alias.equals(name)) { + this.aliasMap.remove(alias); + } + else { + String registeredName = this.aliasMap.get(alias); + if (registeredName != null) { + if (registeredName.equals(name)) { + // An existing alias - no need to re-register + return; + } + if (!allowAliasOverriding()) { + throw new IllegalStateException("Cannot register alias '" + alias + "' for name '" + + name + "': It is already registered for name '" + registeredName + "'."); + } + } + //当A->B存在时,若再次出现A->C->B时候则会抛出异常。 + checkForAliasCircle(name, alias); + this.aliasMap.put(alias, name); + } + } +} +``` + +上述代码的流程总结如下: + +(1)alias与beanName相同情况处理,若alias与beanName并名称相同则不需要处理并删除原有的alias + +(2)alias覆盖处理。若aliasName已经使用并已经指向了另一beanName则需要用户的设置进行处理 + +(3)alias循环检查,当A->B存在时,若再次出现A->C->B时候则会抛出异常。 + + + +## alias标签的解析 + +对应bean标签的解析是最核心的功能,对于alias、import、beans标签的解析都是基于bean标签解析的,接下来我就分析下alias标签的解析。我们回到 parseDefaultElement(Element ele, BeanDefinitionParserDelegate delegate)方法,继续看下方法体,如下图所示: + +![](http://img.topjavaer.cn/img/202309180844832.png) + +对bean进行定义时,除了用id来 指定名称外,为了提供多个名称,可以使用alias标签来指定。而所有这些名称都指向同一个bean。在XML配置文件中,可用单独的元素来完成bean别名的定义。我们可以直接使用bean标签中的name属性,如下: + +```xml + + + +``` + +在Spring还有另外一种声明别名的方式: + +```xml + + +``` + +我们具体看下alias标签的解析过程,解析使用的方法processAliasRegistration(ele),方法体如下: + +```java +protected void processAliasRegistration(Element ele) { + //获取beanName + String name = ele.getAttribute(NAME_ATTRIBUTE); + //获取alias + String alias = ele.getAttribute(ALIAS_ATTRIBUTE); + boolean valid = true; + if (!StringUtils.hasText(name)) { + getReaderContext().error("Name must not be empty", ele); + valid = false; + } + if (!StringUtils.hasText(alias)) { + getReaderContext().error("Alias must not be empty", ele); + valid = false; + } + if (valid) { + try { + //注册alias + getReaderContext().getRegistry().registerAlias(name, alias); + } + catch (Exception ex) { + getReaderContext().error("Failed to register alias '" + alias + + "' for bean with name '" + name + "'", ele, ex); + } + getReaderContext().fireAliasRegistered(name, alias, extractSource(ele)); + } +} +``` + +通过代码可以发现解析流程与bean中的alias解析大同小异,都是讲beanName与别名alias组成一对注册到registry中。跟踪代码最终使用了SimpleAliasRegistry中的registerAlias(String name, String alias)方法 + + + +## import标签的解析 + +对于Spring配置文件的编写,经历过大型项目的人都知道,里面有太多的配置文件了。基本采用的方式都是分模块,分模块的方式很多,使用import就是其中一种,例如我们可以构造这样的Spring配置文件: + +```xml + + + + + + + + + +``` + +applicationContext.xml文件中使用import方式导入有模块配置文件,以后若有新模块入加,那就可以简单修改这个文件了。这样大大简化了配置后期维护的复杂度,并使配置模块化,易于管理。我们来看看Spring是如何解析import配置文件的呢。解析import标签使用的是importBeanDefinitionResource(ele),进入方法体: + +```java +protected void importBeanDefinitionResource(Element ele) { + // 获取 resource 的属性值 + String location = ele.getAttribute(RESOURCE_ATTRIBUTE); + // 为空,直接退出 + if (!StringUtils.hasText(location)) { + getReaderContext().error("Resource location must not be empty", ele); + return; + } + + // 解析系统属性,格式如 :"${user.dir}" + location = getReaderContext().getEnvironment().resolveRequiredPlaceholders(location); + + Set actualResources = new LinkedHashSet<>(4); + + // 判断 location 是相对路径还是绝对路径 + boolean absoluteLocation = false; + try { + absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute(); + } + catch (URISyntaxException ex) { + // cannot convert to an URI, considering the location relative + // unless it is the well-known Spring prefix "classpath*:" + } + + // 绝对路径 + if (absoluteLocation) { + try { + // 直接根据地址加载相应的配置文件 + int importCount = getReaderContext().getReader().loadBeanDefinitions(location, actualResources); + if (logger.isDebugEnabled()) { + logger.debug("Imported " + importCount + " bean definitions from URL location [" + location + "]"); + } + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error( + "Failed to import bean definitions from URL location [" + location + "]", ele, ex); + } + } + else { + // 相对路径则根据相应的地址计算出绝对路径地址 + try { + int importCount; + Resource relativeResource = getReaderContext().getResource().createRelative(location); + if (relativeResource.exists()) { + importCount = getReaderContext().getReader().loadBeanDefinitions(relativeResource); + actualResources.add(relativeResource); + } + else { + String baseLocation = getReaderContext().getResource().getURL().toString(); + importCount = getReaderContext().getReader().loadBeanDefinitions( + StringUtils.applyRelativePath(baseLocation, location), actualResources); + } + if (logger.isDebugEnabled()) { + logger.debug("Imported " + importCount + " bean definitions from relative location [" + location + "]"); + } + } + catch (IOException ex) { + getReaderContext().error("Failed to resolve current resource location", ele, ex); + } + catch (BeanDefinitionStoreException ex) { + getReaderContext().error("Failed to import bean definitions from relative location [" + location + "]", + ele, ex); + } + } + // 解析成功后,进行监听器激活处理 + Resource[] actResArray = actualResources.toArray(new Resource[0]); + getReaderContext().fireImportProcessed(location, actResArray, extractSource(ele)); +} +``` + +解析 import 过程较为清晰,整个过程如下: + +1. 获取 source 属性的值,该值表示资源的路径 +2. 解析路径中的系统属性,如”${user.dir}” +3. 判断资源路径 location 是绝对路径还是相对路径 +4. 如果是绝对路径,则调递归调用 Bean 的解析过程,进行另一次的解析 +5. 如果是相对路径,则先计算出绝对路径得到 Resource,然后进行解析 +6. 通知监听器,完成解析 + +**判断路径** + +方法通过以下方法来判断 location 是为相对路径还是绝对路径: + +```java +absoluteLocation = ResourcePatternUtils.isUrl(location) || ResourceUtils.toURI(location).isAbsolute(); +``` + +判断绝对路径的规则如下: + +- 以 classpath*: 或者 classpath: 开头为绝对路径 +- 能够通过该 location 构建出 `java.net.URL`为绝对路径 +- 根据 location 构造 `java.net.URI` 判断调用 `isAbsolute()` 判断是否为绝对路径 + +如果 location 为绝对路径则调用 `loadBeanDefinitions()`,该方法在 AbstractBeanDefinitionReader 中定义。 + +```java +public int loadBeanDefinitions(String location, @Nullable Set actualResources) throws BeanDefinitionStoreException { + ResourceLoader resourceLoader = getResourceLoader(); + if (resourceLoader == null) { + throw new BeanDefinitionStoreException( + "Cannot import bean definitions from location [" + location + "]: no ResourceLoader available"); + } + + if (resourceLoader instanceof ResourcePatternResolver) { + // Resource pattern matching available. + try { + Resource[] resources = ((ResourcePatternResolver) resourceLoader).getResources(location); + int loadCount = loadBeanDefinitions(resources); + if (actualResources != null) { + for (Resource resource : resources) { + actualResources.add(resource); + } + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + loadCount + " bean definitions from location pattern [" + location + "]"); + } + return loadCount; + } + catch (IOException ex) { + throw new BeanDefinitionStoreException( + "Could not resolve bean definition resource pattern [" + location + "]", ex); + } + } + else { + // Can only load single resources by absolute URL. + Resource resource = resourceLoader.getResource(location); + int loadCount = loadBeanDefinitions(resource); + if (actualResources != null) { + actualResources.add(resource); + } + if (logger.isDebugEnabled()) { + logger.debug("Loaded " + loadCount + " bean definitions from location [" + location + "]"); + } + return loadCount; + } +} +``` + + + +整个逻辑比较简单,首先获取 ResourceLoader,然后根据不同的 ResourceLoader 执行不同的逻辑,主要是可能存在多个 Resource,但是最终都会回归到 `XmlBeanDefinitionReader.loadBeanDefinitions()` ,所以这是一个递归的过程。 + +至此,import 标签解析完毕,整个过程比较清晰明了:获取 source 属性值,得到正确的资源路径,然后调用 `loadBeanDefinitions()` 方法进行递归的 BeanDefinition 加载。 \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC \345\256\271\345\231\250\345\237\272\346\234\254\345\256\236\347\216\260.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC \345\256\271\345\231\250\345\237\272\346\234\254\345\256\236\347\216\260.md" new file mode 100644 index 0000000..e8a89a8 --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC \345\256\271\345\231\250\345\237\272\346\234\254\345\256\236\347\216\260.md" @@ -0,0 +1,979 @@ +## 概述 + +上一篇我们了解了Spring的整体架构,这篇我们开始真正的阅读Spring源码。在分析spring的源码之前,我们先来简单回顾下spring核心功能的简单使用。 + +## 容器的基本用法 + +bean是spring最核心的东西。我们简单看下bean的定义,代码如下: + +```java +public class MyTestBean { + private String name = "dabin"; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} +``` + +代码很简单,bean没有特别之处,spring的的目的就是让我们的bean成为一个纯粹的的POJO,这就是spring追求的,接下来就是在配置文件中定义这个bean,配置文件如下: + +```xml + + + + + + +``` + +在上面的配置中我们可以看到bean的声明方式,在spring中的bean定义有N种属性,但是我们只要像上面这样简单的声明就可以使用了。 +具体测试代码如下: + +``` +import com.dabin.spring.MyTestBean; +import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.xml.XmlBeanFactory; +import org.springframework.core.io.ClassPathResource; + +public class AppTest { + @Test + public void MyTestBeanTest() { + BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); + MyTestBean myTestBean = (MyTestBean) bf.getBean("myTestBean"); + System.out.println(myTestBean.getName()); + } +} +``` + +运行上述测试代码就可以看到输出结果如下: + +``` +dabin +``` + +其实直接使用BeanFactory作为容器对于Spring的使用并不多见,因为企业级应用项目中大多会使用的是ApplicationContext(后面我们会讲两者的区别,这里只是测试) + + + +## 功能分析 + +接下来我们分析2中代码完成的功能; + +- 读取配置文件spring-config.xml。 + +- 根据spring-config.xml中的配置找到对应的类的配置,并实例化。 +- 调用实例化后的实例 + +下图是一个最简单spring功能架构,如果想完成我们预想的功能,至少需要3个类: + +![](http://img.topjavaer.cn/img/202309171627312.png) + +**ConfigReader** :用于读取及验证自己直文件 我们妥用配直文件里面的东西,当然首先 要做的就是读取,然后放直在内存中. + +**ReflectionUtil** :用于根据配置文件中的自己直进行反射实例化,比如在上例中 spring-config.xml 出现的``,我们就可以根据 com.dabin.spring.MyTestBean 进行实例化。 + +**App** :用于完成整个逻辑的串联。 + + + +## 工程搭建 + +spring的源码中用于实现上面功能的是spring-bean这个工程,所以我们接下来看这个工程,当然spring-core是必须的。 + +### beans包的层级结构 + +阅读源码最好的方式是跟着示例操作一遍,我们先看看beans工程的源码结构,如下图所示: + +![](http://img.topjavaer.cn/img/202309171650124.png) + +- src/main/java 用于展现Spring的主要逻辑 +- src/main/resources 用于存放系统的配置文件 +- src/test/java 用于对主要逻辑进行单元测试 +- src/test/resources 用于存放测试用的配置文件 + + + +### 核心类介绍 + +接下来我们先了解下spring-bean最核心的两个类:DefaultListableBeanFactory和XmlBeanDefinitionReader + +#### *DefaultListableBeanFactory* + +XmlBeanFactory继承自DefaultListableBeanFactory,而DefaultListableBeanFactory是整个bean加载的核心部分,是Spring注册及加载bean的默认实现,而对于XmlBeanFactory与DefaultListableBeanFactory不同的地方其实是在XmlBeanFactory中使用了自定义的XML读取器XmlBeanDefinitionReader,实现了个性化的BeanDefinitionReader读取,DefaultListableBeanFactory继承了AbstractAutowireCapableBeanFactory并实现了ConfigurableListableBeanFactory以及BeanDefinitionRegistry接口。以下是ConfigurableListableBeanFactory的层次结构图以下相关类图: + +![](http://img.topjavaer.cn/img/202309171654763.png) + +上面类图中各个类及接口的作用如下: +- AliasRegistry:定义对alias的简单增删改等操作 +- SimpleAliasRegistry:主要使用map作为alias的缓存,并对接口AliasRegistry进行实现 +- SingletonBeanRegistry:定义对单例的注册及获取 +- BeanFactory:定义获取bean及bean的各种属性 +- DefaultSingletonBeanRegistry:默认对接口SingletonBeanRegistry各函数的实现 +- HierarchicalBeanFactory:继承BeanFactory,也就是在BeanFactory定义的功能的基础上增加了对parentFactory的支持 +- BeanDefinitionRegistry:定义对BeanDefinition的各种增删改操作 +- FactoryBeanRegistrySupport:在DefaultSingletonBeanRegistry基础上增加了对FactoryBean的特殊处理功能 +- ConfigurableBeanFactory:提供配置Factory的各种方法 +- ListableBeanFactory:根据各种条件获取bean的配置清单 +- AbstractBeanFactory:综合FactoryBeanRegistrySupport和ConfigurationBeanFactory的功能 +- AutowireCapableBeanFactory:提供创建bean、自动注入、初始化以及应用bean的后处理器 +- AbstractAutowireCapableBeanFactory:综合AbstractBeanFactory并对接口AutowireCapableBeanFactory进行实现 +- ConfigurableListableBeanFactory:BeanFactory配置清单,指定忽略类型及接口等 +- DefaultListableBeanFactory:综合上面所有功能,主要是对Bean注册后的处理 +XmlBeanFactory对DefaultListableBeanFactory类进行了扩展,主要用于从XML文档中读取BeanDefinition,对于注册及获取Bean都是使用从父类DefaultListableBeanFactory继承的方法去实现,而唯独与父类不同的个性化实现就是增加了XmlBeanDefinitionReader类型的reader属性。在XmlBeanFactory中主要使用reader属性对资源文件进行读取和注册 + +#### XmlBeanDefinitionReader + +XML配置文件的读取是Spring中重要的功能,因为Spring的大部分功能都是以配置作为切入点的,可以从XmlBeanDefinitionReader中梳理一下资源文件读取、解析及注册的大致脉络,首先看看各个类的功能 + +- ResourceLoader:定义资源加载器,主要应用于根据给定的资源文件地址返回对应的Resource +- BeanDefinitionReader:主要定义资源文件读取并转换为BeanDefinition的各个功能 +- EnvironmentCapable:定义获取Environment方法 +- DocumentLoader:定义从资源文件加载到转换为Document的功能 +- AbstractBeanDefinitionReader:对EnvironmentCapable、BeanDefinitionReader类定义的功能进行实现 +- BeanDefinitionDocumentReader:定义读取Document并注册BeanDefinition功能 +- BeanDefinitionParserDelegate:定义解析Element的各种方法 + +整个XML配置文件读取的大致流程,在XmlBeanDefinitionReader中主要包含以下几步处理 + +![](http://img.topjavaer.cn/img/202309171638903.png) + +(1)通过继承自AbstractBeanDefinitionReader中的方法,来使用ResourceLoader将资源文件路径转换为对应的Resource文件 +(2)通过DocumentLoader对Resource文件进行转换,将Resource文件转换为Document文件 +(3)通过实现接口BeanDefinitionDocumentReader的DefaultBeanDefinitionDocumentReader类对Document进行解析,并使用BeanDefinitionParserDelegate对Element进行解析 + +## 容器的基础XmlBeanFactory + + 通过上面的内容我们对spring的容器已经有了大致的了解,接下来我们详细探索每个步骤的详细实现,接下来要分析的功能都是基于如下代码: + +```java +BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); +``` + +首先调用ClassPathResource的构造函数来构造Resource资源文件的实例对象,这样后续的资源处理就可以用Resource提供的各种服务来操作了。有了Resource后就可以对BeanFactory进行初始化操作,那配置文件是如何封装的呢? + +### 配置文件的封装 + + Spring的配置文件读取是通过ClassPathResource进行封装的,Spring对其内部使用到的资源实现了自己的抽象结构:Resource接口来封装底层资源,如下源码: + +```java +public interface InputStreamSource { + InputStream getInputStream() throws IOException; +} +public interface Resource extends InputStreamSource { + boolean exists(); + default boolean isReadable() { + return true; + } + default boolean isOpen() { + return false; + } + default boolean isFile() { + return false; + } + URL getURL() throws IOException; + URI getURI() throws IOException; + File getFile() throws IOException; + default ReadableByteChannel readableChannel() throws IOException { + return Channels.newChannel(getInputStream()); + } + long contentLength() throws IOException; + long lastModified() throws IOException; + Resource createRelative(String relativePath) throws IOException; + String getFilename(); + String getDescription(); +} +``` + + InputStreamSource封装任何能返回InputStream的类,比如File、Classpath下的资源和Byte Array等, 它只有一个方法定义:getInputStream(),该方法返回一个新的InputStream对象 。 + +Resource接口抽象了所有Spring内部使用到的底层资源:File、URL、Classpath等。首先,它定义了3个判断当前资源状态的方法:存在性(exists)、可读性(isReadable)、是否处于打开状态(isOpen)。另外,Resource接口还提供了不同资源到URL、URI、File类型的转换,以及获取lastModified属性、文件名(不带路径信息的文件名,getFilename())的方法,为了便于操作,Resource还提供了基于当前资源创建一个相对资源的方法:createRelative(),还提供了getDescription()方法用于在错误处理中的打印信息。 + +对不同来源的资源文件都有相应的Resource实现:文件(FileSystemResource)、Classpath资源(ClassPathResource)、URL资源(UrlResource)、InputStream资源(InputStreamResource)、Byte数组(ByteArrayResource)等。 + +在日常开发中我们可以直接使用spring提供的类来加载资源文件,比如在希望加载资源文件时可以使用下面的代码: + +``` +Resource resource = new ClassPathResource("spring-config.xml"); +InputStream is = resource.getInputStream(); +``` + +有了 Resource 接口便可以对所有资源文件进行统一处理 至于实现,其实是非常简单的,以 getlnputStream 为例,ClassPathResource 中的实现方式便是通 class 或者 classLoader 提供的底层方法进行调用,而对于 FileSystemResource 其实更简单,直接使用 FileInputStream 对文件进行实例化。 + +**ClassPathResource.java** + +```java +InputStream is; +if (this.clazz != null) { + is = this.clazz.getResourceAsStream(this.path); +} +else if (this.classLoader != null) { + is = this.classLoader.getResourceAsStream(this.path); +} +else { + is = ClassLoader.getSystemResourceAsStream(this.path); +} +``` + +**FileSystemResource.java** + +```java +public InputStream getinputStream () throws IOException { + return new FilelnputStream(this file) ; +} +``` + +当通过Resource相关类完成了对配置文件进行封装后,配置文件的读取工作就全权交给XmlBeanDefinitionReader来处理了。 +接下来就进入到XmlBeanFactory的初始化过程了,XmlBeanFactory的初始化有若干办法,Spring提供了很多的构造函数,在这里分析的是使用Resource实例作为构造函数参数的办法,代码如下: + +**XmlBeanFactory.java** + +```java +public XmlBeanFactory(Resource resource) throws BeansException { + this(resource, null); +} +public XmlBeanFactory(Resource resource, BeanFactory parentBeanFactory) throws BeansException { + super(parentBeanFactory); + this.reader.loadBeanDefinitions(resource); +} +``` + + + +上面函数中的代码this.reader.loadBeanDefinitions(resource)才是资源加载的真正实现,但是在XmlBeanDefinitionReader加载数据前还有一个调用父类构造函数初始化的过程:super(parentBeanFactory),我们按照代码层级进行跟踪,首先跟踪到如下父类代码: + +```java +public DefaultListableBeanFactory(@Nullable BeanFactory parentBeanFactory) { + super(parentBeanFactory); +} +``` + +然后继续跟踪,跟踪代码到父类AbstractAutowireCapableBeanFactory的构造函数中: + +```java +public AbstractAutowireCapableBeanFactory(@Nullable BeanFactory parentBeanFactory) { + this(); + setParentBeanFactory(parentBeanFactory); +} +public AbstractAutowireCapableBeanFactory() { + super(); + ignoreDependencyInterface(BeanNameAware.class); + ignoreDependencyInterface(BeanFactoryAware.class); + ignoreDependencyInterface(BeanClassLoaderAware.class); +} +``` + +这里有必要提及 ignoreDependencylnterface方法,ignoreDependencylnterface 的主要功能是 忽略给定接口的向动装配功能,那么,这样做的目的是什么呢?会产生什么样的效果呢? + +举例来说,当 A 中有属性 B ,那么当 Spring 在获取 A的 Bean 的时候如果其属性 B 还没有 初始化,那么 Spring 会自动初始化 B,这也是 Spring 提供的一个重要特性 。但是,某些情况 下, B不会被初始化,其中的一种情况就是B 实现了 BeanNameAware 接口 。Spring 中是这样介绍的:自动装配时忽略给定的依赖接口,典型应用是边过其他方式解析 Application 上下文注册依赖,类似于 BeanFactor 通过 BeanFactoryAware 进行注入或者 ApplicationContext 通过 ApplicationContextAware 进行注入。 + +调用ignoreDependencyInterface方法后,被忽略的接口会存储在BeanFactory的名为ignoredDependencyInterfaces的Set集合中: + +```java +public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFactory + implements AutowireCapableBeanFactory { + + private final Set> ignoredDependencyInterfaces = new HashSet<>(); + + public void ignoreDependencyInterface(Class ifc) { + this.ignoredDependencyInterfaces.add(ifc); + } +... +} +``` + +ignoredDependencyInterfaces集合在同类中被使用仅在一处——isExcludedFromDependencyCheck方法中: + +```java +protected boolean isExcludedFromDependencyCheck(PropertyDescriptor pd) { + return (AutowireUtils.isExcludedFromDependencyCheck(pd) || this.ignoredDependencyTypes.contains(pd.getPropertyType()) || AutowireUtils.isSetterDefinedInInterface(pd, this.ignoredDependencyInterfaces)); +} +``` + +而ignoredDependencyInterface的真正作用还得看AutowireUtils类的isSetterDfinedInInterface方法。 + +```java +public static boolean isSetterDefinedInInterface(PropertyDescriptor pd, Set> interfaces) { + //获取bean中某个属性对象在bean类中的setter方法 + Method setter = pd.getWriteMethod(); + if (setter != null) { + // 获取bean的类型 + Class targetClass = setter.getDeclaringClass(); + for (Class ifc : interfaces) { + if (ifc.isAssignableFrom(targetClass) && // bean类型是否接口的实现类 + ClassUtils.hasMethod(ifc, setter.getName(), setter.getParameterTypes())) { // 接口是否有入参和bean类型完全相同的setter方法 + return true; + } + } + } + return false; +} +``` + +ignoredDependencyInterface方法并不是让我们在自动装配时直接忽略实现了该接口的依赖。这个方法的真正意思是忽略该接口的实现类中和接口setter方法入参类型相同的依赖。 + +举个例子。首先定义一个要被忽略的接口。 + +```java +public interface IgnoreInterface { + + void setList(List list); + + void setSet(Set set); +} +``` + +然后需要实现该接口,在实现类中注意要有setter方法入参相同类型的域对象,在例子中就是`List`和`Set`。 + +```java +public class IgnoreInterfaceImpl implements IgnoreInterface { + + private List list; + private Set set; + + @Override + public void setList(List list) { + this.list = list; + } + + @Override + public void setSet(Set set) { + this.set = set; + } + + public List getList() { + return list; + } + + public Set getSet() { + return set; + } +} +``` + +定义xml配置文件: + +```xml + + + + + + + + foo + bar + + + + + + + + foo + bar + + + + + + + +``` + +最后调用ignoreDependencyInterface: + +```xml +beanFactory.ignoreDependencyInterface(IgnoreInterface.class); +``` + +运行结果: + +``` +null +null +``` + +而如果不调用ignoreDependencyInterface,则是: + +``` +[foo, bar] +[bar, foo] +``` + +我们最初理解是在自动装配时忽略该接口的实现,实际上是在自动装配时忽略该接口实现类中和setter方法入参相同的类型,也就是忽略该接口实现类中存在依赖外部的bean属性注入。 + +典型应用就是BeanFactoryAware和ApplicationContextAware接口。 + +首先看该两个接口的源码: + +```java +public interface BeanFactoryAware extends Aware { + void setBeanFactory(BeanFactory beanFactory) throws BeansException; +} + +public interface ApplicationContextAware extends Aware { + void setApplicationContext(ApplicationContext applicationContext) throws BeansException; +} +``` + +在Spring源码中在不同的地方忽略了该两个接口: + +```java +beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); +ignoreDependencyInterface(BeanFactoryAware.class); +``` + +使得我们的BeanFactoryAware接口实现类在自动装配时不能被注入BeanFactory对象的依赖: + +```java +public class MyBeanFactoryAware implements BeanFactoryAware { + private BeanFactory beanFactory; // 自动装配时忽略注入 + + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + + public BeanFactory getBeanFactory() { + return beanFactory; + } +} +``` + +ApplicationContextAware接口实现类中的ApplicationContext对象的依赖同理: + +```java +public class MyApplicationContextAware implements ApplicationContextAware { + private ApplicationContext applicationContext; // 自动装配时被忽略注入 + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + this.applicationContext = applicationContext; + } + + public ApplicationContext getApplicationContext() { + return applicationContext; + } +} +``` + +这样的做法使得ApplicationContextAware和BeanFactoryAware中的ApplicationContext或BeanFactory依赖在自动装配时被忽略,而统一由框架设置依赖,如ApplicationContextAware接口的设置会在ApplicationContextAwareProcessor类中完成: + +```java +private void invokeAwareInterfaces(Object bean) { + if (bean instanceof Aware) { + if (bean instanceof EnvironmentAware) { + ((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment()); + } + if (bean instanceof EmbeddedValueResolverAware) { + ((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver); + } + if (bean instanceof ResourceLoaderAware) { + ((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext); + } + if (bean instanceof ApplicationEventPublisherAware) { + ((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext); + } + if (bean instanceof MessageSourceAware) { + ((MessageSourceAware) bean).setMessageSource(this.applicationContext); + } + if (bean instanceof ApplicationContextAware) { + ((ApplicationContextAware) bean).setApplicationContext(this.applicationContext); + } + } +} +``` + +通过这种方式保证了ApplicationContextAware和BeanFactoryAware中的容器保证是生成该bean的容器。 + + + +### bean加载 + +在之前XmlBeanFactory构造函数中调用了XmlBeanDefinitionReader类型的reader属性提供的方法 + +this.reader.loadBeanDefinitions(resource),而这句代码则是整个资源加载的切入点,这个方法的时序图如下: + +![](http://img.topjavaer.cn/img/202309171650829.png) + +我们来梳理下上述时序图的处理过程: + +(1)封装资源文件。当进入XmlBeanDefinitionReader后首先对参数Resource使用EncodedResource类进行封装 +(2)获取输入流。从Resource中获取对应的InputStream并构造InputSource +(3)通过构造的InputSource实例和Resource实例继续调用函数doLoadBeanDefinitions,loadBeanDefinitions函数具体的实现过程: + +```java +public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefinitionStoreException { + Assert.notNull(encodedResource, "EncodedResource must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("Loading XML bean definitions from " + encodedResource); + } + + Set currentResources = this.resourcesCurrentlyBeingLoaded.get(); + if (currentResources == null) { + currentResources = new HashSet<>(4); + this.resourcesCurrentlyBeingLoaded.set(currentResources); + } + if (!currentResources.add(encodedResource)) { + throw new BeanDefinitionStoreException( + "Detected cyclic loading of " + encodedResource + " - check your import definitions!"); + } + try { + InputStream inputStream = encodedResource.getResource().getInputStream(); + try { + InputSource inputSource = new InputSource(inputStream); + if (encodedResource.getEncoding() != null) { + inputSource.setEncoding(encodedResource.getEncoding()); + } + return doLoadBeanDefinitions(inputSource, encodedResource.getResource()); + } + finally { + inputStream.close(); + } + } + ... +} +``` + +EncodedResource的作用是对资源文件的编码进行处理的,其中的主要逻辑体现在getReader()方法中,当设置了编码属性的时候Spring会使用相应的编码作为输入流的编码,在构造好了encodeResource对象后,再次转入了可复用方法loadBeanDefinitions(new EncodedResource(resource)),这个方法内部才是真正的数据准备阶段,代码如下: + +```java +protected int doLoadBeanDefinitions(InputSource inputSource, Resource resource) + throws BeanDefinitionStoreException { + try { + // 获取 Document 实例 + Document doc = doLoadDocument(inputSource, resource); + // 根据 Document 实例****注册 Bean信息 + return registerBeanDefinitions(doc, resource); + } + ... +} +``` + +核心部分就是 try 块的两行代码。 + +1. 调用 `doLoadDocument()` 方法,根据 xml 文件获取 Document 实例。 +2. 根据获取的 Document 实例注册 Bean 信息 + +其实在`doLoadDocument()`方法内部还获取了 xml 文件的验证模式。如下: + +```java +protected Document doLoadDocument(InputSource inputSource, Resource resource) throws Exception { + return this.documentLoader.loadDocument(inputSource, getEntityResolver(), this.errorHandler, + getValidationModeForResource(resource), isNamespaceAware()); +} +``` + +调用 `getValidationModeForResource()` 获取指定资源(xml)的验证模式。所以 `doLoadBeanDefinitions()`主要就是做了三件事情。 + +1. 调用 `getValidationModeForResource()` 获取 xml 文件的验证模式 + 2. 调用 `loadDocument()` 根据 xml 文件获取相应的 Document 实例。 + 3. 调用 `registerBeanDefinitions()` 注册 Bean 实例。 + +### 获取XML的验证模式 + +#### *DTD和XSD区别* + +DTD(Document Type Definition)即文档类型定义,是一种XML约束模式语言,是XML文件的验证机制,属于XML文件组成的一部分。DTD是一种保证XML文档格式正确的有效方法,可以通过比较XML文档和DTD文件来看文档是否符合规范,元素和标签使用是否正确。一个DTD文档包含:元素的定义规则,元素间关系的定义规则,元素可使用的属性,可使用的实体或符合规则。 + +使用DTD验证模式的时候需要在XML文件的头部声明,以下是在Spring中使用DTD声明方式的代码: + +```xml + + +``` + +XML Schema语言就是XSD(XML Schemas Definition)。XML Schema描述了XML文档的结构,可以用一个指定的XML Schema来验证某个XML文档,以检查该XML文档是否符合其要求,文档设计者可以通过XML Schema指定一个XML文档所允许的结构和内容,并可据此检查一个XML文档是否是有效的。 + +在使用XML Schema文档对XML实例文档进行检验,除了要声明**名称空间**外(**xmlns=http://www.Springframework.org/schema/beans**),还必须指定该名称空间所对应的**XML Schema文档的存储位置**,通过**schemaLocation**属性来指定名称空间所对应的XML Schema文档的存储位置,它包含两个部分,一部分是名称空间的URI,另一部分就该名称空间所标识的XML Schema文件位置或URL地址(xsi:schemaLocation=”http://www.Springframework.org/schema/beans **http://www.Springframework.org/schema/beans/Spring-beans.xsd**“),代码如下: + +```xml + + + + + + +``` + +#### *验证模式的读取* + +在spring中,是通过getValidationModeForResource方法来获取对应资源的验证模式,其源码如下: + +```java +protected int getValidationModeForResource(Resource resource) { + int validationModeToUse = getValidationMode(); + if (validationModeToUse != VALIDATION_AUTO) { + return validationModeToUse; + } + int detectedMode = detectValidationMode(resource); + if (detectedMode != VALIDATION_AUTO) { + return detectedMode; + } + // Hmm, we didn't get a clear indication... Let's assume XSD, + // since apparently no DTD declaration has been found up until + // detection stopped (before finding the document's root tag). + return VALIDATION_XSD; +} +``` + +方法的实现还是很简单的,如果设定了验证模式则使用设定的验证模式(可以通过使用XmlBeanDefinitionReader中的setValidationMode方法进行设定),否则使用自动检测的方式。而自动检测验证模式的功能是在函数detectValidationMode方法中,而在此方法中又将自动检测验证模式的工作委托给了专门处理类XmlValidationModeDetector的validationModeDetector方法,具体代码如下: + +```java +public int detectValidationMode(InputStream inputStream) throws IOException { + // Peek into the file to look for DOCTYPE. + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); + try { + boolean isDtdValidated = false; + String content; + while ((content = reader.readLine()) != null) { + content = consumeCommentTokens(content); + if (this.inComment || !StringUtils.hasText(content)) { + continue; + } + if (hasDoctype(content)) { + isDtdValidated = true; + break; + } + if (hasOpeningTag(content)) { + // End of meaningful data... + break; + } + } + return (isDtdValidated ? VALIDATION_DTD : VALIDATION_XSD); + } + catch (CharConversionException ex) { + // Choked on some character encoding... + // Leave the decision up to the caller. + return VALIDATION_AUTO; + } + finally { + reader.close(); + } +} +``` + +从代码中看,主要是通过读取 XML 文件的内容,判断内容中是否包含有 DOCTYPE ,如果是 则为 DTD,否则为 XSD,当然只会读取到 第一个 “<” 处,因为 验证模式一定会在第一个 “<” 之前。如果当中出现了 CharConversionException 异常,则为 XSD模式。 + + + +### 获取Document + +经过了验证模式准备的步骤就可以进行Document加载了,对于文档的读取委托给了DocumentLoader去执行,这里的DocumentLoader是个接口,而真正调用的是DefaultDocumentLoader,解析代码如下: + +```java +public Document loadDocument(InputSource inputSource, EntityResolver entityResolver, + ErrorHandler errorHandler, int validationMode, boolean namespaceAware) throws Exception { + DocumentBuilderFactory factory = createDocumentBuilderFactory(validationMode, namespaceAware); + if (logger.isDebugEnabled()) { + logger.debug("Using JAXP provider [" + factory.getClass().getName() + "]"); + } + DocumentBuilder builder = createDocumentBuilder(factory, entityResolver, errorHandler); + return builder.parse(inputSource); +} +``` + +分析代码,首选创建DocumentBuildFactory,再通过DocumentBuilderFactory创建DocumentBuilder,进而解析InputSource来返回Document对象。对于参数entityResolver,传入的是通过getEntityResolver()函数获取的返回值,代码如下: + +```java +protected EntityResolver getEntityResolver() { + if (this.entityResolver == null) { + // Determine default EntityResolver to use. + ResourceLoader resourceLoader = getResourceLoader(); + if (resourceLoader != null) { + this.entityResolver = new ResourceEntityResolver(resourceLoader); + } + else { + this.entityResolver = new DelegatingEntityResolver(getBeanClassLoader()); + } + } + return this.entityResolver; +} +``` + +这个entityResolver是做什么用的呢,接下来我们详细分析下。 + +#### EntityResolver 的用法 + +对于解析一个XML,SAX首先读取该XML文档上的声明,根据声明去寻找相应的DTD定义,以便对文档进行一个验证,默认的寻找规则,即通过网络(实现上就是声明DTD的URI地址)来下载相应的DTD声明,并进行认证。下载的过程是一个漫长的过程,而且当网络中断或不可用时,这里会报错,就是因为相应的DTD声明没有被找到的原因. + +EntityResolver的作用是项目本身就可以提供一个如何寻找DTD声明的方法,即由程序来实现寻找DTD声明的过程,比如将DTD文件放到项目中某处,在实现时直接将此文档读取并返回给SAX即可,在EntityResolver的接口只有一个方法声明: + +```java +public abstract InputSource resolveEntity (String publicId, String systemId) + throws SAXException, IOException; +``` + +它接收两个参数publicId和systemId,并返回一个InputSource对象,以特定配置文件来进行讲解 + +(1)如果在解析验证模式为XSD的配置文件,代码如下: + +```xml + + +.... + +``` + +则会读取到以下两个参数 + +- publicId:null +- systemId:[http://www.Springframework.org/schema/beans/Spring-beans.xsd](http://www.springframework.org/schema/beans/Spring-beans.xsd) + +![](http://img.topjavaer.cn/img/202309171647085.png) + +(2)如果解析验证模式为DTD的配置文件,代码如下: + +```xml + + +.... + +``` + +读取到以下两个参数 +- publicId:-//Spring//DTD BEAN 2.0//EN +- systemId:http://www.Springframework.org/dtd/Spring-beans-2.0.dtd + +![](http://img.topjavaer.cn/img/202309171647627.png) + +一般都会把验证文件放置在自己的工程里,如果把URL转换为自己工程里对应的地址文件呢?以加载DTD文件为例来看看Spring是如何实现的。根据之前Spring中通过getEntityResolver()方法对EntityResolver的获取,我们知道,Spring中使用DelegatingEntityResolver类为EntityResolver的实现类,resolveEntity实现方法如下: + +```java +@Override +@Nullable +public InputSource resolveEntity(String publicId, @Nullable String systemId) throws SAXException, IOException { + if (systemId != null) { + if (systemId.endsWith(DTD_SUFFIX)) { + return this.dtdResolver.resolveEntity(publicId, systemId); + } + else if (systemId.endsWith(XSD_SUFFIX)) { + return this.schemaResolver.resolveEntity(publicId, systemId); + } + } + return null; +} +``` + +不同的验证模式使用不同的解析器解析,比如加载DTD类型的BeansDtdResolver的resolveEntity是直接截取systemId最后的xx.dtd然后去当前路径下寻找,而加载XSD类型的PluggableSchemaResolver类的resolveEntity是默认到META-INF/Spring.schemas文件中找到systemId所对应的XSD文件并加载。 BeansDtdResolver 的解析过程如下: + +```java +public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Trying to resolve XML entity with public ID [" + publicId + + "] and system ID [" + systemId + "]"); + } + if (systemId != null && systemId.endsWith(DTD_EXTENSION)) { + int lastPathSeparator = systemId.lastIndexOf('/'); + int dtdNameStart = systemId.indexOf(DTD_NAME, lastPathSeparator); + if (dtdNameStart != -1) { + String dtdFile = DTD_NAME + DTD_EXTENSION; + if (logger.isTraceEnabled()) { + logger.trace("Trying to locate [" + dtdFile + "] in Spring jar on classpath"); + } + try { + Resource resource = new ClassPathResource(dtdFile, getClass()); + InputSource source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + if (logger.isDebugEnabled()) { + logger.debug("Found beans DTD [" + systemId + "] in classpath: " + dtdFile); + } + return source; + } + catch (IOException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Could not resolve beans DTD [" + systemId + "]: not found in classpath", ex); + } + } + } + } + return null; +} +``` + +从上面的代码中我们可以看到加载 DTD 类型的 `BeansDtdResolver.resolveEntity()` 只是对 systemId 进行了简单的校验(从最后一个 / 开始,内容中是否包含 `spring-beans`),然后构造一个 InputSource 并设置 publicId、systemId,然后返回。 PluggableSchemaResolver 的解析过程如下: + +```java +public InputSource resolveEntity(String publicId, @Nullable String systemId) throws IOException { + if (logger.isTraceEnabled()) { + logger.trace("Trying to resolve XML entity with public id [" + publicId + + "] and system id [" + systemId + "]"); + } + + if (systemId != null) { + String resourceLocation = getSchemaMappings().get(systemId); + if (resourceLocation != null) { + Resource resource = new ClassPathResource(resourceLocation, this.classLoader); + try { + InputSource source = new InputSource(resource.getInputStream()); + source.setPublicId(publicId); + source.setSystemId(systemId); + if (logger.isDebugEnabled()) { + logger.debug("Found XML schema [" + systemId + "] in classpath: " + resourceLocation); + } + return source; + } + catch (FileNotFoundException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Couldn't find XML schema [" + systemId + "]: " + resource, ex); + } + } + } + } + return null; +} +``` + +首先调用 getSchemaMappings() 获取一个映射表(systemId 与其在本地的对照关系),然后根据传入的 systemId 获取该 systemId 在本地的路径 resourceLocation,最后根据 resourceLocation 构造 InputSource 对象。 映射表如下(部分): + +![](http://img.topjavaer.cn/img/202309171648620.png) + +### 解析及注册BeanDefinitions + +当把文件转换成Document后,接下来就是对bean的提取及注册,当程序已经拥有了XML文档文件的Document实例对象时,就会被引入到XmlBeanDefinitionReader.registerBeanDefinitions这个方法: + +```java +public int registerBeanDefinitions(Document doc, Resource resource) throws BeanDefinitionStoreException { + BeanDefinitionDocumentReader documentReader = createBeanDefinitionDocumentReader(); + int countBefore = getRegistry().getBeanDefinitionCount(); + documentReader.registerBeanDefinitions(doc, createReaderContext(resource)); + return getRegistry().getBeanDefinitionCount() - countBefore; +} +``` + +其中的doc参数即为上节读取的document,而BeanDefinitionDocumentReader是一个接口,而实例化的工作是在createBeanDefinitionDocumentReader()中完成的,而通过此方法,BeanDefinitionDocumentReader真正的类型其实已经是DefaultBeanDefinitionDocumentReader了,进入DefaultBeanDefinitionDocumentReader后,发现这个方法的重要目的之一就是提取root,以便于再次将root作为参数继续BeanDefinition的注册,如下代码: + +```java +public void registerBeanDefinitions(Document doc, XmlReaderContext readerContext) { + this.readerContext = readerContext; + logger.debug("Loading bean definitions"); + Element root = doc.getDocumentElement(); + doRegisterBeanDefinitions(root); +} +``` + +通过这里我们看到终于到了解析逻辑的核心方法doRegisterBeanDefinitions,接着跟踪源码如下: + +```java +protected void doRegisterBeanDefinitions(Element root) { + BeanDefinitionParserDelegate parent = this.delegate; + this.delegate = createDelegate(getReaderContext(), root, parent); + if (this.delegate.isDefaultNamespace(root)) { + String profileSpec = root.getAttribute(PROFILE_ATTRIBUTE); + if (StringUtils.hasText(profileSpec)) { + String[] specifiedProfiles = StringUtils.tokenizeToStringArray( + profileSpec, BeanDefinitionParserDelegate.MULTI_VALUE_ATTRIBUTE_DELIMITERS); + if (!getReaderContext().getEnvironment().acceptsProfiles(specifiedProfiles)) { + if (logger.isInfoEnabled()) { + logger.info("Skipped XML bean definition file due to specified profiles [" + profileSpec + + "] not matching: " + getReaderContext().getResource()); + } + return; + } + } + } + preProcessXml(root); + parseBeanDefinitions(root, this.delegate); + postProcessXml(root); + this.delegate = parent; +} +``` + +我们看到首先要解析profile属性,然后才开始XML的读取,具体的代码如下: + +```java +protected void parseBeanDefinitions(Element root, BeanDefinitionParserDelegate delegate) { + if (delegate.isDefaultNamespace(root)) { + NodeList nl = root.getChildNodes(); + for (int i = 0; i < nl.getLength(); i++) { + Node node = nl.item(i); + if (node instanceof Element) { + Element ele = (Element) node; + if (delegate.isDefaultNamespace(ele)) { + parseDefaultElement(ele, delegate); + } + else { + delegate.parseCustomElement(ele); + } + } + } + } + else { + delegate.parseCustomElement(root); + } +} +``` + +最终解析动作落地在两个方法处:`parseDefaultElement(ele, delegate)` 和 `delegate.parseCustomElement(root)`。我们知道在 Spring 有两种 Bean 声明方式: + +- 配置文件式声明:`` +- 自定义注解方式:`` + +两种方式的读取和解析都存在较大的差异,所以采用不同的解析方法,如果根节点或者子节点采用默认命名空间的话,则调用 `parseDefaultElement()` 进行解析,否则调用 `delegate.parseCustomElement()` 方法进行自定义解析。 + +而判断是否默认命名空间还是自定义命名空间的办法其实是使用node.getNamespaceURI()获取命名空间,并与Spring中固定的命名空间http://www.springframework.org/schema/beans进行对比,如果一致则认为是默认,否则就认为是自定义。 + +#### profile的用法 + +通过profile标记不同的环境,可以通过设置spring.profiles.active和spring.profiles.default激活指定profile环境。如果设置了active,default便失去了作用。如果两个都没有设置,那么带有profiles的bean都不会生成。 + +配置spring配置文件最下面配置如下beans + +```xml + + + + + + + + + + + + + + +``` + +配置web.xml + +```xml + + + spring.profiles.default + production + + + + + + spring.profiles.active + test + +``` + +这样启动的时候就可以按照切换spring.profiles.active的属性值来进行切换了。 \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\345\276\252\347\216\257\344\276\235\350\265\226\345\244\204\347\220\206.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\345\276\252\347\216\257\344\276\235\350\265\226\345\244\204\347\220\206.md" new file mode 100644 index 0000000..328c93d --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\345\276\252\347\216\257\344\276\235\350\265\226\345\244\204\347\220\206.md" @@ -0,0 +1,178 @@ +## **什么是循环依赖** + +循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图所示: + +![](http://img.topjavaer.cn/img/202309210848022.png) + +注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。 + +Spring中循环依赖场景有: + +(1)构造器的循环依赖 + +(2)field属性的循环依赖。 + +对于构造器的循环依赖,Spring 是无法解决的,只能抛出 BeanCurrentlyInCreationException 异常表示循环依赖,所以下面我们分析的都是基于 field 属性的循环依赖。 + +Spring 只解决 scope 为 singleton 的循环依赖,对于scope 为 prototype 的 bean Spring 无法解决,直接抛出 BeanCurrentlyInCreationException 异常。 + +### **如何检测循环依赖** + +检测循环依赖相对比较容易,Bean在创建的时候可以给该Bean打标,如果递归调用回来发现正在创建中的话,即说明了循环依赖了。 + +### 解决循环依赖 + +我们先从加载 bean 最初始的方法 `doGetBean()` 开始。 + +在 `doGetBean()` 中,首先会根据 beanName 从单例 bean 缓存中获取,如果不为空则直接返回。 + +```java +protected Object getSingleton(String beanName, boolean allowEarlyReference) { + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + synchronized (this.singletonObjects) { + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null && allowEarlyReference) { + ObjectFactory singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + singletonObject = singletonFactory.getObject(); + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + } + return singletonObject; +} +``` + +这个方法主要是从三个缓存中获取,分别是:singletonObjects、earlySingletonObjects、singletonFactories,三者定义如下: + +```java +private final Map singletonObjects = new ConcurrentHashMap<>(256); + +private final Map> singletonFactories = new HashMap<>(16); + +private final Map earlySingletonObjects = new HashMap<>(16); +``` + +这三级缓存分别指: + +(1)singletonFactories : 单例对象工厂的cache + +(2)earlySingletonObjects :提前暴光的单例对象的Cache + +(3)singletonObjects:单例对象的cache + +他们就是 Spring 解决 singleton bean 的关键因素所在,我称他们为三级缓存,第一级为 singletonObjects,第二级为 earlySingletonObjects,第三级为 singletonFactories。这里我们可以通过 `getSingleton()` 看到他们是如何配合的,这分析该方法之前,提下其中的 `isSingletonCurrentlyInCreation()` 和 `allowEarlyReference`。 + +- `isSingletonCurrentlyInCreation()`:判断当前 singleton bean 是否处于创建中。bean 处于创建中也就是说 bean 在初始化但是没有完成初始化,有一个这样的过程其实和 Spring 解决 bean 循环依赖的理念相辅相成,因为 Spring 解决 singleton bean 的核心就在于提前曝光 bean。 +- allowEarlyReference:从字面意思上面理解就是允许提前拿到引用。其实真正的意思是是否允许从 singletonFactories 缓存中通过 `getObject()` 拿到对象,为什么会有这样一个字段呢?原因就在于 singletonFactories 才是 Spring 解决 singleton bean 的诀窍所在,这个我们后续分析。 + +`getSingleton()` 整个过程如下:首先从一级缓存 singletonObjects 获取,如果没有且当前指定的 beanName 正在创建,就再从二级缓存中 earlySingletonObjects 获取,如果还是没有获取到且运行 singletonFactories 通过 `getObject()` 获取,则从三级缓存 singletonFactories 获取,如果获取到则,通过其 `getObject()` 获取对象,并将其加入到二级缓存 earlySingletonObjects 中 从三级缓存 singletonFactories 删除,如下: + +```java +singletonObject = singletonFactory.getObject(); +this.earlySingletonObjects.put(beanName, singletonObject); +this.singletonFactories.remove(beanName); +``` + +这样就从三级缓存升级到二级缓存了。 + +上面是从缓存中获取,但是缓存中的数据从哪里添加进来的呢?一直往下跟会发现在 `doCreateBean()` ( AbstractAutowireCapableBeanFactory ) 中,有这么一段代码: + +```java +boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && isSingletonCurrentlyInCreation(beanName)); +if (earlySingletonExposure) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly caching bean '" + beanName + + "' to allow for resolving potential circular references"); + } + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); +} +``` + +也就是我们上一篇文章中讲的最后一部分,提前将创建好但还未进行属性赋值的的Bean放入缓存中。 + +如果 `earlySingletonExposure == true` 的话,则调用 `addSingletonFactory()` 将他们添加到缓存中,但是一个 bean 要具备如下条件才会添加至缓存中: + +- 单例 +- 运行提前暴露 bean +- 当前 bean 正在创建中 + +`addSingletonFactory()` 代码如下: + +```java +protected void addSingletonFactory(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(singletonFactory, "Singleton factory must not be null"); + synchronized (this.singletonObjects) { + if (!this.singletonObjects.containsKey(beanName)) { + this.singletonFactories.put(beanName, singletonFactory); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } + } +} +``` + +从这段代码我们可以看出 singletonFactories 这个三级缓存才是解决 Spring Bean 循环依赖的诀窍所在。同时这段代码发生在 `createBeanInstance()` 方法之后,也就是说这个 bean 其实已经被创建出来了,但是它还不是很完美(没有进行属性填充和初始化),但是对于其他依赖它的对象而言已经足够了(可以根据对象引用定位到堆中对象),能够被认出来了,所以 Spring 在这个时候选择将该对象提前曝光出来让大家认识认识。 + +介绍到这里我们发现三级缓存 singletonFactories 和 二级缓存 earlySingletonObjects 中的值都有出处了,那一级缓存在哪里设置的呢?在类 DefaultSingletonBeanRegistry 中可以发现这个 `addSingleton()` 方法,源码如下: + +```java +protected void addSingleton(String beanName, Object singletonObject) { + synchronized (this.singletonObjects) { + this.singletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } +} +``` + +添加至一级缓存,同时从二级、三级缓存中删除。这个方法在我们创建 bean 的链路中有哪个地方引用呢?其实在前面博客已经提到过了,在 `doGetBean()` 处理不同 scope 时,如果是 singleton,则调用 `getSingleton()`,如下: + +```java +if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + // Explicitly remove instance from singleton cache: It might have been put there + // eagerly by the creation process, to allow for circular reference resolution. + // Also remove any beans that received a temporary reference to the bean. + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); +} +``` + + + +```java +public Object getSingleton(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(beanName, "Bean name must not be null"); + synchronized (this.singletonObjects) { + Object singletonObject = this.singletonObjects.get(beanName); + if (singletonObject == null) { + //.... + try { + singletonObject = singletonFactory.getObject(); + newSingleton = true; + } + //..... + if (newSingleton) { + addSingleton(beanName, singletonObject); + } + } + return singletonObject; + } +} +``` + +至此,Spring 关于 singleton bean 循环依赖已经分析完毕了。所以我们基本上可以确定 Spring 解决循环依赖的方案了:Spring 在创建 bean 的时候并不是等它完全完成,而是在创建过程中将创建中的 bean 的 ObjectFactory 提前曝光(即加入到 singletonFactories 缓存中),这样一旦下一个 bean 创建的时候需要依赖 bean ,则直接使用 ObjectFactory 的 `getObject()` 获取了,也就是 `getSingleton()`中的代码片段了。 + +最后来描述下就上面那个循环依赖 Spring 解决的过程:首先 A 完成初始化第一步并将自己提前曝光出来(通过 ObjectFactory 将自己提前曝光),在初始化的时候,发现自己依赖对象 B,此时就会去尝试 get(B),这个时候发现 B 还没有被创建出来,然后 B 就走创建流程,在 B 初始化的时候,同样发现自己依赖 C,C 也没有被创建出来,这个时候 C 又开始初始化进程,但是在初始化的过程中发现自己依赖 A,于是尝试 get(A),这个时候由于 A 已经添加至缓存中(一般都是添加至三级缓存 singletonFactories ),通过 ObjectFactory 提前曝光,所以可以通过 `ObjectFactory.getObject()` 拿到 A 对象,C 拿到 A 对象后顺利完成初始化,然后将自己添加到一级缓存中,回到 B ,B 也可以拿到 C 对象,完成初始化,A 可以顺利拿到 B 完成初始化。到这里整个链路就已经完成了初始化过程了。 \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\345\261\236\346\200\247\345\241\253\345\205\205.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\345\261\236\346\200\247\345\241\253\345\205\205.md" new file mode 100644 index 0000000..dc7b99b --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\345\261\236\346\200\247\345\241\253\345\205\205.md" @@ -0,0 +1,563 @@ +**正文** + +`doCreateBean()` 主要用于完成 bean 的创建和初始化工作,我们可以将其分为四个过程: + +- `createBeanInstance()` 实例化 bean +- `populateBean()` 属性填充 +- 循环依赖的处理 +- `initializeBean()` 初始化 bean + +第一个过程实例化 bean在前面一篇博客中已经分析完了,这篇博客开始分析 属性填充,也就是 `populateBean()` + +```java +protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper bw) { + PropertyValues pvs = mbd.getPropertyValues(); + + if (bw == null) { + if (!pvs.isEmpty()) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Cannot apply property values to null instance"); + } + else { + // Skip property population phase for null instance. + return; + } + } + + // Give any InstantiationAwareBeanPostProcessors the opportunity to modify the + // state of the bean before properties are set. This can be used, for example, + // to support styles of field injection. + boolean continueWithPropertyPopulation = true; + + if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; + //返回值为是否继续填充bean + if (!ibp.postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)) { + continueWithPropertyPopulation = false; + break; + } + } + } + } + //如果后处理器发出停止填充命令则终止后续的执行 + if (!continueWithPropertyPopulation) { + return; + } + + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME || + mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + MutablePropertyValues newPvs = new MutablePropertyValues(pvs); + + // Add property values based on autowire by name if applicable. + //根据名称自动注入 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_NAME) { + autowireByName(beanName, mbd, bw, newPvs); + } + + // Add property values based on autowire by type if applicable. + //根据类型自动注入 + if (mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_BY_TYPE) { + autowireByType(beanName, mbd, bw, newPvs); + } + + pvs = newPvs; + } + //后处理器已经初始化 + boolean hasInstAwareBpps = hasInstantiationAwareBeanPostProcessors(); + //需要依赖检查 + boolean needsDepCheck = (mbd.getDependencyCheck() != RootBeanDefinition.DEPENDENCY_CHECK_NONE); + + if (hasInstAwareBpps || needsDepCheck) { + PropertyDescriptor[] filteredPds = filterPropertyDescriptorsForDependencyCheck(bw, mbd.allowCaching); + if (hasInstAwareBpps) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof InstantiationAwareBeanPostProcessor) { + InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp; + //对所有需要依赖检查的属性进行后处理 + pvs = ibp.postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName); + if (pvs == null) { + return; + } + } + } + } + if (needsDepCheck) { + //依赖检查,对应depends-on属性,3.0已经弃用此属性 + checkDependencies(beanName, mbd, filteredPds, pvs); + } + } + //将属性应用到bean中 + //将所有ProtertyValues中的属性填充至BeanWrapper中。 + applyPropertyValues(beanName, mbd, bw, pvs); +} +``` + +我们来分析下populateBean的流程: + +(1)首先进行属性是否为空的判断 + +(2)通过调用InstantiationAwareBeanPostProcessor的postProcessAfterInstantiation(bw.getWrappedInstance(), beanName)方法来控制程序是否继续进行属性填充 + +(3)根据注入类型(byName/byType)提取依赖的bean,并统一存入PropertyValues中 + +(4)应用InstantiationAwareBeanPostProcessor的postProcessPropertyValues(pvs, filteredPds, bw.getWrappedInstance(), beanName)方法,对属性获取完毕填充前的再次处理,典型的应用是RequiredAnnotationBeanPostProcesser类中对属性的验证 + +(5)将所有的PropertyValues中的属性填充至BeanWrapper中 + +上面步骤中有几个地方是我们比较感兴趣的,它们分别是依赖注入(autowireByName/autowireByType)以及属性填充,接下来进一步分析这几个功能的实现细节 + + + +## 自动注入 + +Spring 会根据注入类型( byName / byType )的不同,调用不同的方法(`autowireByName()` / `autowireByType()`)来注入属性值。 + +### autowireByName() + +```java +protected void autowireByName( + String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) { + + // 获取 Bean 对象中非简单属性 + String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); + for (String propertyName : propertyNames) { + // 如果容器中包含指定名称的 bean,则将该 bean 注入到 bean中 + if (containsBean(propertyName)) { + // 递归初始化相关 bean + Object bean = getBean(propertyName); + // 为指定名称的属性赋予属性值 + pvs.add(propertyName, bean); + // 属性依赖注入 + registerDependentBean(propertyName, beanName); + if (logger.isDebugEnabled()) { + logger.debug("Added autowiring by name from bean name '" + beanName + + "' via property '" + propertyName + "' to bean named '" + propertyName + "'"); + } + } + else { + if (logger.isTraceEnabled()) { + logger.trace("Not autowiring property '" + propertyName + "' of bean '" + beanName + + "' by name: no matching bean found"); + } + } + } +} +``` + +该方法逻辑很简单,获取该 bean 的非简单属性,什么叫做非简单属性呢?就是类型为对象类型的属性,但是这里并不是将所有的对象类型都都会找到,比如 8 个原始类型,String 类型 ,Number类型、Date类型、URL类型、URI类型等都会被忽略,如下: + +```java +protected String[] unsatisfiedNonSimpleProperties(AbstractBeanDefinition mbd, BeanWrapper bw) { + Set result = new TreeSet<>(); + PropertyValues pvs = mbd.getPropertyValues(); + PropertyDescriptor[] pds = bw.getPropertyDescriptors(); + for (PropertyDescriptor pd : pds) { + if (pd.getWriteMethod() != null && !isExcludedFromDependencyCheck(pd) && !pvs.contains(pd.getName()) && + !BeanUtils.isSimpleProperty(pd.getPropertyType())) { + result.add(pd.getName()); + } + } + return StringUtils.toStringArray(result); +} +``` + +这里获取的就是需要依赖注入的属性。 + +autowireByName()函数的功能就是根据传入的参数中的pvs中找出已经加载的bean,并递归实例化,然后加入到pvs中 + + + +### autowireByType + +autowireByType与autowireByName对于我们理解与使用来说复杂程度相似,但是实现功能的复杂度却不一样,我们看下方法代码: + +```java +protected void autowireByType( + String beanName, AbstractBeanDefinition mbd, BeanWrapper bw, MutablePropertyValues pvs) { + + TypeConverter converter = getCustomTypeConverter(); + if (converter == null) { + converter = bw; + } + + Set autowiredBeanNames = new LinkedHashSet(4); + //寻找bw中需要依赖注入的属性 + String[] propertyNames = unsatisfiedNonSimpleProperties(mbd, bw); + for (String propertyName : propertyNames) { + try { + PropertyDescriptor pd = bw.getPropertyDescriptor(propertyName); + // Don't try autowiring by type for type Object: never makes sense, + // even if it technically is a unsatisfied, non-simple property. + if (!Object.class.equals(pd.getPropertyType())) { + //探测指定属性的set方法 + MethodParameter methodParam = BeanUtils.getWriteMethodParameter(pd); + // Do not allow eager init for type matching in case of a prioritized post-processor. + boolean eager = !PriorityOrdered.class.isAssignableFrom(bw.getWrappedClass()); + DependencyDescriptor desc = new AutowireByTypeDependencyDescriptor(methodParam, eager); + //解析指定beanName的属性所匹配的值,并把解析到的属性名称存储在autowiredBeanNames中, + Object autowiredArgument = resolveDependency(desc, beanName, autowiredBeanNames, converter); + if (autowiredArgument != null) { + pvs.add(propertyName, autowiredArgument); + } + for (String autowiredBeanName : autowiredBeanNames) { + //注册依赖 + registerDependentBean(autowiredBeanName, beanName); + if (logger.isDebugEnabled()) { + logger.debug("Autowiring by type from bean name '" + beanName + "' via property '" + + propertyName + "' to bean named '" + autowiredBeanName + "'"); + } + } + autowiredBeanNames.clear(); + } + } + catch (BeansException ex) { + throw new UnsatisfiedDependencyException(mbd.getResourceDescription(), beanName, propertyName, ex); + } + } +} +``` + +根据名称第一步与根据属性第一步都是寻找bw中需要依赖注入的属性,然后遍历这些属性并寻找类型匹配的bean,其中最复杂就是寻找类型匹配的bean。spring中提供了对集合的类型注入支持,如使用如下注解方式: + +```java +@Autowired +private List tests; +``` + +这种方式spring会把所有与Test匹配的类型找出来并注入到tests属性中,正是由于这一因素,所以在autowireByType函数,新建了局部遍历autowireBeanNames,用于存储所有依赖的bean,如果只是对非集合类的属性注入来说,此属性并无用处。 + +对于寻找类型匹配的逻辑实现是封装在了resolveDependency函数中,其实现如下: + +```java +public Object resolveDependency(DependencyDescriptor descriptor, String beanName, Set autowiredBeanNames, TypeConverter typeConverter) throws BeansException { + descriptor.initParameterNameDiscovery(getParameterNameDiscoverer()); + if (descriptor.getDependencyType().equals(ObjectFactory.class)) { + //ObjectFactory类注入的特殊处理 + return new DependencyObjectFactory(descriptor, beanName); + } + else if (descriptor.getDependencyType().equals(javaxInjectProviderClass)) { + //javaxInjectProviderClass类注入的特殊处理 + return new DependencyProviderFactory().createDependencyProvider(descriptor, beanName); + } + else { + //通用处理逻辑 + return doResolveDependency(descriptor, descriptor.getDependencyType(), beanName, autowiredBeanNames, typeConverter); + } +} + +protected Object doResolveDependency(DependencyDescriptor descriptor, Class type, String beanName, + Set autowiredBeanNames, TypeConverter typeConverter) throws BeansException { + /* + * 用于支持Spring中新增的注解@Value + */ + Object value = getAutowireCandidateResolver().getSuggestedValue(descriptor); + if (value != null) { + if (value instanceof String) { + String strVal = resolveEmbeddedValue((String) value); + BeanDefinition bd = (beanName != null && containsBean(beanName) ? getMergedBeanDefinition(beanName) : null); + value = evaluateBeanDefinitionString(strVal, bd); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + return (descriptor.getField() != null ? + converter.convertIfNecessary(value, type, descriptor.getField()) : + converter.convertIfNecessary(value, type, descriptor.getMethodParameter())); + } + //如果解析器没有成功解析,则需要考虑各种情况 + //属性是数组类型 + if (type.isArray()) { + Class componentType = type.getComponentType(); + //根据属性类型找到beanFactory中所有类型的匹配bean, + //返回值的构成为:key=匹配的beanName,value=beanName对应的实例化后的bean(通过getBean(beanName)返回) + Map matchingBeans = findAutowireCandidates(beanName, componentType, descriptor); + if (matchingBeans.isEmpty()) { + //如果autowire的require属性为true而找到的匹配项却为空则只能抛出异常 + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(componentType, "array of " + componentType.getName(), descriptor); + } + return null; + } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + //通过转换器将bean的值转换为对应的type类型 + return converter.convertIfNecessary(matchingBeans.values(), type); + } + //属性是Collection类型 + else if (Collection.class.isAssignableFrom(type) && type.isInterface()) { + Class elementType = descriptor.getCollectionType(); + if (elementType == null) { + if (descriptor.isRequired()) { + throw new FatalBeanException("No element type declared for collection [" + type.getName() + "]"); + } + return null; + } + Map matchingBeans = findAutowireCandidates(beanName, elementType, descriptor); + if (matchingBeans.isEmpty()) { + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(elementType, "collection of " + elementType.getName(), descriptor); + } + return null; + } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + TypeConverter converter = (typeConverter != null ? typeConverter : getTypeConverter()); + return converter.convertIfNecessary(matchingBeans.values(), type); + } + //属性是Map类型 + else if (Map.class.isAssignableFrom(type) && type.isInterface()) { + Class keyType = descriptor.getMapKeyType(); + if (keyType == null || !String.class.isAssignableFrom(keyType)) { + if (descriptor.isRequired()) { + throw new FatalBeanException("Key type [" + keyType + "] of map [" + type.getName() + + "] must be assignable to [java.lang.String]"); + } + return null; + } + Class valueType = descriptor.getMapValueType(); + if (valueType == null) { + if (descriptor.isRequired()) { + throw new FatalBeanException("No value type declared for map [" + type.getName() + "]"); + } + return null; + } + Map matchingBeans = findAutowireCandidates(beanName, valueType, descriptor); + if (matchingBeans.isEmpty()) { + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(valueType, "map with value type " + valueType.getName(), descriptor); + } + return null; + } + if (autowiredBeanNames != null) { + autowiredBeanNames.addAll(matchingBeans.keySet()); + } + return matchingBeans; + } + else { + Map matchingBeans = findAutowireCandidates(beanName, type, descriptor); + if (matchingBeans.isEmpty()) { + if (descriptor.isRequired()) { + raiseNoSuchBeanDefinitionException(type, "", descriptor); + } + return null; + } + if (matchingBeans.size() > 1) { + String primaryBeanName = determinePrimaryCandidate(matchingBeans, descriptor); + if (primaryBeanName == null) { + throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet()); + } + if (autowiredBeanNames != null) { + autowiredBeanNames.add(primaryBeanName); + } + return matchingBeans.get(primaryBeanName); + } + // We have exactly one match. + Map.Entry entry = matchingBeans.entrySet().iterator().next(); + if (autowiredBeanNames != null) { + autowiredBeanNames.add(entry.getKey()); + } + //已经确定只有一个匹配项 + return entry.getValue(); + } +} +``` + +主要就是通过Type从BeanFactory中找到对应的benaName,然后通过getBean获取实例 + +```java +protected Map findAutowireCandidates( + @Nullable String beanName, Class requiredType, DependencyDescriptor descriptor) { + //在BeanFactory找到所有Type类型的beanName + String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this, requiredType, true, descriptor.isEager()); + Map result = new LinkedHashMap<>(candidateNames.length); + + //遍历所有的beanName,通过getBean获取 + for (String candidate : candidateNames) { + if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) { + // + addCandidateEntry(result, candidate, descriptor, requiredType); + } + } + return result; +} + +private void addCandidateEntry(Map candidates, String candidateName, + DependencyDescriptor descriptor, Class requiredType) { + + Object beanInstance = descriptor.resolveCandidate(candidateName, requiredType, this); + if (!(beanInstance instanceof NullBean)) { + candidates.put(candidateName, beanInstance); + } +} + +public Object resolveCandidate(String beanName, Class requiredType, BeanFactory beanFactory) + throws BeansException { + //通过类型找到beanName,然后再找到其实例 + return beanFactory.getBean(beanName); +} +``` + +## applyPropertyValues + +程序运行到这里,已经完成了对所有注入属性的获取,但是获取的属性是以PropertyValues形式存在的,还并没有应用到已经实例化的bean中,这一工作是在applyPropertyValues中。继续跟踪到方法体中: + +```java +protected void applyPropertyValues(String beanName, BeanDefinition mbd, BeanWrapper bw, PropertyValues pvs) { + if (pvs == null || pvs.isEmpty()) { + return; + } + + MutablePropertyValues mpvs = null; + List original; + + if (System.getSecurityManager() != null) { + if (bw instanceof BeanWrapperImpl) { + ((BeanWrapperImpl) bw).setSecurityContext(getAccessControlContext()); + } + } + + if (pvs instanceof MutablePropertyValues) { + mpvs = (MutablePropertyValues) pvs; + //如果mpvs中的值已经被转换为对应的类型那么可以直接设置到beanwapper中 + if (mpvs.isConverted()) { + // Shortcut: use the pre-converted values as-is. + try { + bw.setPropertyValues(mpvs); + return; + } + catch (BeansException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Error setting property values", ex); + } + } + original = mpvs.getPropertyValueList(); + } + else { + //如果pvs并不是使用MutablePropertyValues封装的类型,那么直接使用原始的属性获取方法 + original = Arrays.asList(pvs.getPropertyValues()); + } + + TypeConverter converter = getCustomTypeConverter(); + if (converter == null) { + converter = bw; + } + //获取对应的解析器 + BeanDefinitionValueResolver valueResolver = new BeanDefinitionValueResolver(this, beanName, mbd, converter); + + // Create a deep copy, resolving any references for values. + List deepCopy = new ArrayList(original.size()); + boolean resolveNecessary = false; + //遍历属性,将属性转换为对应类的对应属性的类型 + for (PropertyValue pv : original) { + if (pv.isConverted()) { + deepCopy.add(pv); + } + else { + String propertyName = pv.getName(); + Object originalValue = pv.getValue(); + Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue); + Object convertedValue = resolvedValue; + boolean convertible = bw.isWritableProperty(propertyName) && + !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName); + if (convertible) { + convertedValue = convertForProperty(resolvedValue, propertyName, bw, converter); + } + // Possibly store converted value in merged bean definition, + // in order to avoid re-conversion for every created bean instance. + if (resolvedValue == originalValue) { + if (convertible) { + pv.setConvertedValue(convertedValue); + } + deepCopy.add(pv); + } + else if (convertible && originalValue instanceof TypedStringValue && + !((TypedStringValue) originalValue).isDynamic() && + !(convertedValue instanceof Collection || ObjectUtils.isArray(convertedValue))) { + pv.setConvertedValue(convertedValue); + deepCopy.add(pv); + } + else { + resolveNecessary = true; + deepCopy.add(new PropertyValue(pv, convertedValue)); + } + } + } + if (mpvs != null && !resolveNecessary) { + mpvs.setConverted(); + } + + // Set our (possibly massaged) deep copy. + try { + bw.setPropertyValues(new MutablePropertyValues(deepCopy)); + } + catch (BeansException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Error setting property values", ex); + } +} +``` + +我们来看看具体的属性赋值过程 + +```java +public class MyTestBean { + private String name ; + + public MyTestBean(String name) { + this.name = name; + } + + public MyTestBean() { + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} + + + + +``` + +如上 **bw.setPropertyValues** 最终都会走到如下方法 + +```java +@Override +public void setValue(final @Nullable Object value) throws Exception { + //获取writeMethod,也就是我们MyTestBean的setName方法 + final Method writeMethod = (this.pd instanceof GenericTypeAwarePropertyDescriptor ? + ((GenericTypeAwarePropertyDescriptor) this.pd).getWriteMethodForActualAccess() : + this.pd.getWriteMethod()); + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + ReflectionUtils.makeAccessible(writeMethod); + return null; + }); + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> + writeMethod.invoke(getWrappedInstance(), value), acc); + } + catch (PrivilegedActionException ex) { + throw ex.getException(); + } + } + else { + ReflectionUtils.makeAccessible(writeMethod); + //通过反射调用方法进行赋值 + writeMethod.invoke(getWrappedInstance(), value); + } +} +``` + +就是利用反射进行调用对象的set方法赋值 + +至此,`doCreateBean()` 第二个过程:属性填充 已经分析完成了,下篇分析第三个过程:循环依赖的处理,其实循环依赖并不仅仅只是在 `doCreateBean()` 中处理,其实在整个加载 bean 的过程中都有涉及,所以下篇内容并不仅仅只局限于 `doCreateBean()`。 \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" new file mode 100644 index 0000000..ce9103c --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" @@ -0,0 +1,73 @@ +## 概述 + +Spring是一个开放源代码的设计层面框架,他解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson创建。简单来说,Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。 + +## spring的整体架构 + +Spring框架是一个分层架构,它包含一系列的功能要素,并被分为大约20个模块,如下图所示: + +![](http://img.topjavaer.cn/img/202309161608576.png) + +从上图spring framework整体架构图可以看到,这些模块被总结为以下几个部分: + +### 1. Core Container + +Core Container(核心容器)包含有Core、Beans、Context和Expression Language模块 + +Core和Beans模块是框架的基础部分,提供IoC(转控制)和依赖注入特性。这里的基础概念是BeanFactory,它提供对Factory模式的经典实现来消除对程序性单例模式的需要,并真正地允许你从程序逻辑中分离出依赖关系和配置。 + +Core模块主要包含Spring框架基本的核心工具类 + +Beans模块是所有应用都要用到的,它包含访问配置文件、创建和管理bean以及进行Inversion of Control/Dependency Injection(Ioc/DI)操作相关的所有类 + +Context模块构建于Core和Beans模块基础之上,提供了一种类似于JNDI注册器的框架式的对象访问方法。Context模块继承了Beans的特性,为Spring核心提供了大量扩展,添加了对国际化(如资源绑定)、事件传播、资源加载和对Context的透明创建的支持。 + +ApplicationContext接口是Context模块的关键 + +Expression Language模块提供了一个强大的表达式语言用于在运行时查询和操纵对象,该语言支持设置/获取属性的值,属性的分配,方法的调用,访问数组上下文、容器和索引器、逻辑和算术运算符、命名变量以及从Spring的IoC容器中根据名称检索对象 + + + +### 2. Data Access/Integration + +JDBC模块提供了一个JDBC抽象层,它可以消除冗长的JDBC编码和解析数据库厂商特有的错误代码,这个模块包含了Spring对JDBC数据访问进行封装的所有类 + +ORM模块为流行的对象-关系映射API,如JPA、JDO、Hibernate、iBatis等,提供了一个交互层,利用ORM封装包,可以混合使用所有Spring提供的特性进行O/R映射,如前边提到的简单声明性事务管理 + +OXM模块提供了一个Object/XML映射实现的抽象层,Object/XML映射实现抽象层包括JAXB,Castor,XMLBeans,JiBX和XStream +JMS(java Message Service)模块主要包含了一些制造和消费消息的特性 + +Transaction模块支持编程和声明式事物管理,这些事务类必须实现特定的接口,并且对所有POJO都适用 + + + +### 3. Web + +Web上下文模块建立在应用程序上下文模块之上,为基于Web的应用程序提供了上下文,所以Spring框架支持与Jakarta Struts的集成。 + +Web模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。Web层包含了Web、Web-Servlet、Web-Struts和Web、Porlet模块 + +Web模块:提供了基础的面向Web的集成特性,例如,多文件上传、使用Servlet listeners初始化IoC容器以及一个面向Web的应用上下文,它还包含了Spring远程支持中Web的相关部分 + +Web-Servlet模块web.servlet.jar:该模块包含Spring的model-view-controller(MVC)实现,Spring的MVC框架使得模型范围内的代码和web forms之间能够清楚地分离开来,并与Spring框架的其他特性基础在一起 + +Web-Struts模块:该模块提供了对Struts的支持,使得类在Spring应用中能够与一个典型的Struts Web层集成在一起 + +Web-Porlet模块:提供了用于Portlet环境和Web-Servlet模块的MVC的实现 + + + +### 4. AOP + +AOP模块提供了一个符合AOP联盟标准的面向切面编程的实现,它让你可以定义例如方法拦截器和切点,从而将逻辑代码分开,降低它们之间的耦合性,利用source-level的元数据功能,还可以将各种行为信息合并到你的代码中 + +Spring AOP模块为基于Spring的应用程序中的对象提供了事务管理服务,通过使用Spring AOP,不用依赖EJB组件,就可以将声明性事务管理集成到应用程序中 + + + +### 5. Test + +Test模块支持使用Junit和TestNG对Spring组件进行测试 + + + diff --git "a/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC-\345\274\200\345\220\257 bean \347\232\204\345\212\240\350\275\275.md" "b/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC-\345\274\200\345\220\257 bean \347\232\204\345\212\240\350\275\275.md" new file mode 100644 index 0000000..ecb36af --- /dev/null +++ "b/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC-\345\274\200\345\220\257 bean \347\232\204\345\212\240\350\275\275.md" @@ -0,0 +1,650 @@ +## 概述 + +前面我们已经分析了spring对于xml配置文件的解析,将分析的信息组装成 BeanDefinition,并将其保存注册到相应的 BeanDefinitionRegistry 中。至此,Spring IOC 的初始化工作完成。接下来我们将对bean的加载进行探索。 + +## BeanFactory + +当我们显示或者隐式地调用 `getBean()` 时,则会触发加载 bean 阶段。如下: + +```java +public class AppTest { + @Test + public void MyTestBeanTest() { + BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); + MyTestBean myTestBean = (MyTestBean) bf.getBean("myTestBean"); + } +} +``` + +我们看到这个方法是在接口BeanFactory中定义的,我们看下BeanFactory体系结构,如下图所示: + +![](http://img.topjavaer.cn/img/202309200725956.png) + +从上图我们看到:   + +(1)BeanFactory作为一个主接口不继承任何接口,暂且称为一级接口。 + +(2)有3个子接口继承了它,进行功能上的增强。这3个子接口称为二级接口。 + +(3)ConfigurableBeanFactory可以被称为三级接口,对二级接口HierarchicalBeanFactory进行了再次增强,它还继承了另一个外来的接口SingletonBeanRegistry + +(4)ConfigurableListableBeanFactory是一个更强大的接口,继承了上述的所有接口,无所不包,称为四级接口。(这4级接口是BeanFactory的基本接口体系。 + +(5)AbstractBeanFactory作为一个抽象类,实现了三级接口ConfigurableBeanFactory大部分功能。 + +(6)AbstractAutowireCapableBeanFactory同样是抽象类,继承自AbstractBeanFactory,并额外实现了二级接口AutowireCapableBeanFactory + +(7)DefaultListableBeanFactory继承自AbstractAutowireCapableBeanFactory,实现了最强大的四级接口ConfigurableListableBeanFactory,并实现了一个外来接口BeanDefinitionRegistry,它并非抽象类。 + +(8)最后是最强大的XmlBeanFactory,继承自DefaultListableBeanFactory,重写了一些功能,使自己更强大。 + + + +#### 定义 + +BeanFactory,以Factory结尾,表示它是一个工厂类(接口), **它负责生产和管理bean的一个工厂**。在Spring中,BeanFactory是IOC容器的核心接口,它的职责包括:实例化、定位、配置应用程序中的对象及建立这些对象间的依赖。BeanFactory只是个接口,并不是IOC容器的具体实现,但是Spring容器给出了很多种实现,如 DefaultListableBeanFactory、XmlBeanFactory、ApplicationContext等,其中**XmlBeanFactory就是常用的一个,该实现将以XML方式描述组成应用的对象及对象间的依赖关系**。XmlBeanFactory类将持有此XML配置元数据,并用它来构建一个完全可配置的系统或应用。 + +BeanFactory是Spring IOC容器的鼻祖,是IOC容器的基础接口,所有的容器都是从它这里继承实现而来。可见其地位。BeanFactory提供了最基本的IOC容器的功能,即所有的容器至少需要实现的标准。 + +XmlBeanFactory,只是提供了最基本的IOC容器的功能。而且XMLBeanFactory,继承自DefaultListableBeanFactory。DefaultListableBeanFactory实际包含了基本IOC容器所具有的所有重要功能,是一个完整的IOC容器。 + +ApplicationContext包含BeanFactory的所有功能,通常建议比BeanFactory优先。 + +BeanFactory体系结构是典型的工厂方法模式,即什么样的工厂生产什么样的产品。BeanFactory是最基本的抽象工厂,而其他的IOC容器只不过是具体的工厂,对应着各自的Bean定义方法。但同时,其他容器也针对具体场景不同,进行了扩充,提供具体的服务。 如下: + +```java +Resource resource = new FileSystemResource("beans.xml"); +BeanFactory factory = new XmlBeanFactory(resource); +ClassPathResource resource = new ClassPathResource("beans.xml"); +BeanFactory factory = new XmlBeanFactory(resource); +ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"applicationContext.xml"}); +BeanFactory factory = (BeanFactory) context; +``` + +基本就是这些了,接着使用getBean(String beanName)方法就可以取得bean的实例;BeanFactory提供的方法及其简单,仅提供了六种方法供客户调用: + +- boolean containsBean(String beanName) 判断工厂中是否包含给定名称的bean定义,若有则返回true +- Object getBean(String) 返回给定名称注册的bean实例。根据bean的配置情况,如果是singleton模式将返回一个共享实例,否则将返回一个新建的实例,如果没有找到指定bean,该方法可能会抛出异常 +- Object getBean(String, Class) 返回以给定名称注册的bean实例,并转换为给定class类型 +- Class getType(String name) 返回给定名称的bean的Class,如果没有找到指定的bean实例,则排除NoSuchBeanDefinitionException异常 +- boolean isSingleton(String) 判断给定名称的bean定义是否为单例模式 +- String[] getAliases(String name) 返回给定bean名称的所有别名 + +```java +package org.springframework.beans.factory; +import org.springframework.beans.BeansException; +public interface BeanFactory { + String FACTORY_BEAN_PREFIX = "&"; + Object getBean(String name) throws BeansException; + T getBean(String name, Class requiredType) throws BeansException; + T getBean(Class requiredType) throws BeansException; + Object getBean(String name, Object... args) throws BeansException; + boolean containsBean(String name); + boolean isSingleton(String name) throws NoSuchBeanDefinitionException; + boolean isPrototype(String name) throws NoSuchBeanDefinitionException; + boolean isTypeMatch(String name, Class targetType) throws NoSuchBeanDefinitionException; + Class getType(String name) throws NoSuchBeanDefinitionException; + String[] getAliases(String name); +} +``` + + + +## FactoryBean + +一般情况下,Spring通过反射机制利`用`的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在``中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。它们隐藏了实例化一些复杂Bean的细节,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持泛型,即接口声明改为`FactoryBean`的形式。 + +以Bean结尾,表示它是一个Bean,不同于普通Bean的是:**它是实现了FactoryBean接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取**。 + +```java +package org.springframework.beans.factory; +public interface FactoryBean { + T getObject() throws Exception; + Class getObjectType(); + boolean isSingleton(); +} +``` + +在该接口中还定义了以下3个方法: + +- **T getObject()**:返回由FactoryBean创建的Bean实例,如果isSingleton()返回true,则该实例会放到Spring容器中单实例缓存池中; +- **boolean isSingleton()**:返回由FactoryBean创建的Bean实例的作用域是singleton还是prototype; +- **Class getObjectType()**:返回FactoryBean创建的Bean类型。 + +当配置文件中``的class属性配置的实现类是FactoryBean时,通过getBean()方法返回的不是FactoryBean本身,而是FactoryBean#getObject()方法所返回的对象,相当于FactoryBean#getObject()代理了getBean()方法。 +例:如果使用传统方式配置下面Car的``时,Car的每个属性分别对应一个``元素标签。 + +```java +public class Car { + private int maxSpeed ; + private String brand ; + private double price ; + //get//set 方法 +} +``` + + + +如果用FactoryBean的方式实现就灵活点,下例通过逗号分割符的方式一次性的为Car的所有属性指定配置值: + +```java +import org.springframework.beans.factory.FactoryBean; +public class CarFactoryBean implements FactoryBean { + private String carInfo ; + public Car getObject() throws Exception { + Car car = new Car(); + String[] infos = carInfo.split(","); + car.setBrand(infos[0]); + car.setMaxSpeed(Integer.valueOf(infos[1])); + car.setPrice(Double.valueOf(infos[2])); + return car; + } + public Class getObjectType(){ + return Car.class ; + } + public boolean isSingleton(){ + return false ; + } + public String getCarInfo(){ + return this.carInfo; + } + + //接受逗号分割符设置属性信息 + public void setCarInfo (String carInfo){ + this.carInfo = carInfo; + } +} +``` + +有了这个CarFactoryBean后,就可以在配置文件中使用下面这种自定义的配置方式配置CarBean了: + +```xml + +``` + +当调用getBean("car")时,Spring通过反射机制发现CarFactoryBean实现了FactoryBean的接口,这时Spring容器就调用接口方法CarFactoryBean#getObject()方法返回。如果希望获取CarFactoryBean的实例,则需要在使用getBean(beanName)方法时在beanName前显示的加上"&"前缀:如getBean("&car"); + +## 获取bean + +接下来我们回到加载bean的阶段,当我们显示或者隐式地调用 `getBean()` 时,则会触发加载 bean 阶段。如下: + +```java +public Object getBean(String name) throws BeansException { + return doGetBean(name, null, null, false); +} +``` + +内部调用 `doGetBean()` 方法,这个方法的代码比较长,各位耐心看下: + +```java +@SuppressWarnings("unchecked") +protected T doGetBean(final String name, @Nullable final Class requiredType, + @Nullable final Object[] args, boolean typeCheckOnly) throws BeansException { + //获取 beanName,这里是一个转换动作,将 name 转换为 beanName + final String beanName = transformedBeanName(name); + Object bean; + /* + *检查缓存中的实例工程是否存在对应的实例 + *为何要优先使用这段代码呢? + *因为在创建单例bean的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖 + *spring创建bean的原则是在不等bean创建完就会将创建bean的objectFactory提前曝光,即将其加入到缓存中,一旦下个bean创建时依赖上个bean则直接使用objectFactory + *直接从缓存中或singletonFactories中获取objectFactory + *就算没有循环依赖,只是单纯的依赖注入,如B依赖A,如果A已经初始化完成,B进行初始化时,需要递归调用getBean获取A,这是A已经在缓存里了,直接可以从这里取到 + */ + // Eagerly check singleton cache for manually registered singletons. + Object sharedInstance = getSingleton(beanName); + if (sharedInstance != null && args == null) { + if (logger.isDebugEnabled()) { + if (isSingletonCurrentlyInCreation(beanName)) { + logger.debug("Returning eagerly cached instance of singleton bean '" + beanName + + "' that is not fully initialized yet - a consequence of a circular reference"); + } + else { + logger.debug("Returning cached instance of singleton bean '" + beanName + "'"); + } + } + //返回对应的实例,有些时候并不是直接返回实例,而是返回某些方法返回的实例 + //这里涉及到我们上面讲的FactoryBean,如果此Bean是FactoryBean的实现类,如果name前缀为"&",则直接返回此实现类的bean,如果没有前缀"&",则需要调用此实现类的getObject方法,返回getObject里面真是的返回对象 + bean = getObjectForBeanInstance(sharedInstance, name, beanName, null); + } + else { + //只有在单例的情况下才会解决循环依赖 + if (isPrototypeCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } + //尝试从parentBeanFactory中查找bean + BeanFactory parentBeanFactory = getParentBeanFactory(); + if (parentBeanFactory != null && !containsBeanDefinition(beanName)) { + // Not found -> check parent. + String nameToLookup = originalBeanName(name); + if (parentBeanFactory instanceof AbstractBeanFactory) { + return ((AbstractBeanFactory) parentBeanFactory).doGetBean( + nameToLookup, requiredType, args, typeCheckOnly); + } + else if (args != null) { + // Delegation to parent with explicit args. + return (T) parentBeanFactory.getBean(nameToLookup, args); + } + else { + // No args -> delegate to standard getBean method. + return parentBeanFactory.getBean(nameToLookup, requiredType); + } + } + //如果不是仅仅做类型检查,则这里需要创建bean,并做记录 + if (!typeCheckOnly) { + markBeanAsCreated(beanName); + } + try { + //将存储XML配置文件的GenericBeanDefinition转换为RootBeanDefinition,同时如果存在父bean的话则合并父bean的相关属性 + final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName); + checkMergedBeanDefinition(mbd, beanName, args); + + //如果存在依赖则需要递归实例化依赖的bean + String[] dependsOn = mbd.getDependsOn(); + if (dependsOn != null) { + for (String dep : dependsOn) { + if (isDependent(beanName, dep)) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Circular depends-on relationship between '" + beanName + "' and '" + dep + "'"); + } + registerDependentBean(dep, beanName); + try { + getBean(dep); + } + catch (NoSuchBeanDefinitionException ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "'" + beanName + "' depends on missing bean '" + dep + "'", ex); + } + } + } + + // 单例模式 + // 实例化依赖的bean后对bean本身进行实例化 + if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + // Explicitly remove instance from singleton cache: It might have been put there + // eagerly by the creation process, to allow for circular reference resolution. + // Also remove any beans that received a temporary reference to the bean. + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); + } + // 原型模式 + else if (mbd.isPrototype()) { + // It's a prototype -> create a new instance. + Object prototypeInstance = null; + try { + beforePrototypeCreation(beanName); + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); + } + // 从指定的 scope 下创建 bean + else { + String scopeName = mbd.getScope(); + final Scope scope = this.scopes.get(scopeName); + if (scope == null) { + throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); + } + try { + Object scopedInstance = scope.get(beanName, () -> { + beforePrototypeCreation(beanName); + try { + return createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + }); + bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); + } + catch (IllegalStateException ex) { + throw new BeanCreationException(beanName, + "Scope '" + scopeName + "' is not active for the current thread; consider " + + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", + ex); + } + } + } + catch (BeansException ex) { + cleanupAfterBeanCreationFailure(beanName); + throw ex; + } + } + + // Check if required type matches the type of the actual bean instance. + if (requiredType != null && !requiredType.isInstance(bean)) { + try { + T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType); + if (convertedBean == null) { + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } + return convertedBean; + } + catch (TypeMismatchException ex) { + if (logger.isDebugEnabled()) { + logger.debug("Failed to convert bean '" + name + "' to required type '" + + ClassUtils.getQualifiedName(requiredType) + "'", ex); + } + throw new BeanNotOfRequiredTypeException(name, requiredType, bean.getClass()); + } + } + return (T) bean; +} +``` + +代码是相当长,处理逻辑也是相当复杂,下面将其进行拆分讲解。 + +### 获取 beanName + +```java +final String beanName = transformedBeanName(name); +``` + +这里传递的是 name,不一定就是 beanName,可能是 aliasName,也有可能是 FactoryBean(带“&”前缀),所以这里需要调用 `transformedBeanName()` 方法对 name 进行一番转换,主要如下: + +```java +protected String transformedBeanName(String name) { + return canonicalName(BeanFactoryUtils.transformedBeanName(name)); +} + +// 去除 FactoryBean 的修饰符 +public static String transformedBeanName(String name) { + Assert.notNull(name, "'name' must not be null"); + String beanName = name; + while (beanName.startsWith(BeanFactory.FACTORY_BEAN_PREFIX)) { + beanName = beanName.substring(BeanFactory.FACTORY_BEAN_PREFIX.length()); + } + return beanName; +} + +// 转换 aliasName +public String canonicalName(String name) { + String canonicalName = name; + // Handle aliasing... + String resolvedName; + do { + resolvedName = this.aliasMap.get(canonicalName); + if (resolvedName != null) { + canonicalName = resolvedName; + } + } + while (resolvedName != null); + return canonicalName; +} +``` + +主要处理过程包括两步: + +1. 去除 FactoryBean 的修饰符。如果 name 以 “&” 为前缀,那么会去掉该 “&”,例如,`name = "&studentService"`,则会是 `name = "studentService"`。 +2. 取指定的 alias 所表示的最终 beanName。主要是一个循环获取 beanName 的过程,例如别名 A 指向名称为 B 的 bean 则返回 B,若 别名 A 指向别名 B,别名 B 指向名称为 C 的 bean,则返回 C。 + + + +### 缓存中获取单例bean + +单例在Spring的同一个容器内只会被创建一次,后续再获取bean直接从单例缓存中获取,当然这里也只是尝试加载,首先尝试从缓存中加载,然后再次尝试从singletonFactorry加载因为在创建单例bean的时候会存在依赖注入的情况,而在创建依赖的时候为了避免循环依赖,Spring创建bean的原则不等bean创建完成就会创建bean的ObjectFactory提早曝光加入到缓存中,一旦下一个bean创建时需要依赖上个bean,则直接使用ObjectFactory;就算没有循环依赖,只是单纯的依赖注入,如B依赖A,如果A已经初始化完成,B进行初始化时,需要递归调用getBean获取A,这是A已经在缓存里了,直接可以从这里取到。接下来我们看下获取单例bean的方法getSingleton(beanName),进入方法体: + +```java +@Override +@Nullable +public Object getSingleton(String beanName) { + //参数true是允许早期依赖 + return getSingleton(beanName, true); +} +@Nullable +protected Object getSingleton(String beanName, boolean allowEarlyReference) { + //检查缓存中是否存在实例,这里就是上面说的单纯的依赖注入,如B依赖A,如果A已经初始化完成,B进行初始化时,需要递归调用getBean获取A,这是A已经在缓存里了,直接可以从这里取到 + Object singletonObject = this.singletonObjects.get(beanName); + //如果缓存为空且单例bean正在创建中,则锁定全局变量,为什么要判断bean在创建中呢?这里就是可以判断是否循环依赖了。 + //A依赖B,B也依赖A,A实例化的时候,发现依赖B,则递归去实例化B,B发现依赖A,则递归实例化A,此时会走到原点A的实例化,第一次A的实例化还没完成,只不过把实例化的对象加入到缓存中,但是状态还是正在创建中,由此回到原点发现A正在创建中,由此可以判断是循环依赖了 + if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { + synchronized (this.singletonObjects) { + //如果此bean正在加载,则不处理 + singletonObject = this.earlySingletonObjects.get(beanName); + if (singletonObject == null && allowEarlyReference) { + //当某些方法需要提前初始化的时候会直接调用addSingletonFactory把对应的ObjectFactory初始化策略存储在singletonFactory中 + ObjectFactory singletonFactory = this.singletonFactories.get(beanName); + if (singletonFactory != null) { + //使用预先设定的getObject方法 + singletonObject = singletonFactory.getObject(); + 记录在缓存中,注意earlySingletonObjects和singletonFactories是互斥的 + this.earlySingletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + } + } + } + } + return singletonObject; +} +``` + +接下来我们根据源码再来梳理下这个方法,这样更易于理解,这个方法先尝试从singletonObjects里面获取实例,如果如果获取不到再从earlySingletonObjects里面获取,如果还获取不到,再尝试从singletonFactories里面获取beanName对应的ObjectFactory,然后再调用这个ObjectFactory的getObject方法创建bean,并放到earlySingletonObjects里面去,并且从singletonFactoryes里面remove调这个ObjectFactory,而对于后续所有的内存操作都只为了循环依赖检测时候使用,即allowEarlyReference为true的时候才会使用。 +这里涉及到很多个存储bean的不同map,简单解释下: + +1.singletonObjects:用于保存BeanName和创建bean实例之间的关系,beanName–>bean Instance + +2.singletonFactories:用于保存BeanName和创建bean的工厂之间的关系,banName–>ObjectFactory + +3.earlySingletonObjects:也是保存BeanName和创建bean实例之间的关系,与singletonObjects的不同之处在于,当一个单例bean被放到这里面后,那么当bean还在创建过程中,就可以通过getBean方法获取到了,其目的是用来检测循环引用。 + +4.registeredSingletons:用来保存当前所有已注册的bean. + + + +### 从bean的实例中获取对象 + +获取到bean以后就要获取实例对象了,这里用到的是getObjectForBeanInstance方法。getObjectForBeanInstance是个频繁使用的方法,无论是从缓存中获得bean还是根据不同的scope策略加载bean.总之,我们得到bean的实例后,要做的第一步就是调用这个方法来检测一下正确性,其实就是检测获得Bean是不是FactoryBean类型的bean,如果是,那么需要调用该bean对应的FactoryBean实例中的getObject()作为返回值。接下来我们看下此方法的源码: + +```java +protected Object getObjectForBeanInstance( + Object beanInstance, String name, String beanName, @Nullable RootBeanDefinition mbd) { + //如果指定的name是工厂相关的(以&开头的) + if (BeanFactoryUtils.isFactoryDereference(name)) { + //如果是NullBean则直接返回此bean + if (beanInstance instanceof NullBean) { + return beanInstance; + } + //如果不是FactoryBean类型,则验证不通过抛出异常 + if (!(beanInstance instanceof FactoryBean)) { + throw new BeanIsNotAFactoryException(transformedBeanName(name), beanInstance.getClass()); + } + } + // Now we have the bean instance, which may be a normal bean or a FactoryBean. + // If it's a FactoryBean, we use it to create a bean instance, unless the + // caller actually wants a reference to the factory. + //如果获取的beanInstance不是FactoryBean类型,则说明是普通的Bean,可直接返回 + //如果获取的beanInstance是FactoryBean类型,但是是以(以&开头的),也直接返回,此时返回的是FactoryBean的实例 + if (!(beanInstance instanceof FactoryBean) || BeanFactoryUtils.isFactoryDereference(name)) { + return beanInstance; + } + Object object = null; + if (mbd == null) { + object = getCachedObjectForFactoryBean(beanName); + } + if (object == null) { + // Return bean instance from factory. + FactoryBean factory = (FactoryBean) beanInstance; + // Caches object obtained from FactoryBean if it is a singleton. + if (mbd == null && containsBeanDefinition(beanName)) { + mbd = getMergedLocalBeanDefinition(beanName); + } + boolean synthetic = (mbd != null && mbd.isSynthetic()); + //到了这里说明获取的beanInstance是FactoryBean类型,但没有以"&"开头,此时就要返回factory内部getObject里面的对象了 + object = getObjectFromFactoryBean(factory, beanName, !synthetic); + } + return object; +} +``` + +接着我们来看看真正的核心功能getObjectFromFactoryBean(factory, beanName, !synthetic)方法中实现的,继续跟进代码: + +```java +protected Object getObjectFromFactoryBean(FactoryBean factory, String beanName, boolean shouldPostProcess) { + // 为单例模式且缓存中存在 + if (factory.isSingleton() && containsSingleton(beanName)) { + + synchronized (getSingletonMutex()) { + // 从缓存中获取指定的 factoryBean + Object object = this.factoryBeanObjectCache.get(beanName); + + if (object == null) { + // 为空,则从 FactoryBean 中获取对象 + object = doGetObjectFromFactoryBean(factory, beanName); + + // 从缓存中获取 + Object alreadyThere = this.factoryBeanObjectCache.get(beanName); + if (alreadyThere != null) { + object = alreadyThere; + } + else { + // 需要后续处理 + if (shouldPostProcess) { + // 若该 bean 处于创建中,则返回非处理对象,而不是存储它 + if (isSingletonCurrentlyInCreation(beanName)) { + return object; + } + // 前置处理 + beforeSingletonCreation(beanName); + try { + // 对从 FactoryBean 获取的对象进行后处理 + // 生成的对象将暴露给bean引用 + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, + "Post-processing of FactoryBean's singleton object failed", ex); + } + finally { + // 后置处理 + afterSingletonCreation(beanName); + } + } + // 缓存 + if (containsSingleton(beanName)) { + this.factoryBeanObjectCache.put(beanName, object); + } + } + } + return object; + } + } + else { + // 非单例 + Object object = doGetObjectFromFactoryBean(factory, beanName); + if (shouldPostProcess) { + try { + object = postProcessObjectFromFactoryBean(object, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "Post-processing of FactoryBean's object failed", ex); + } + } + return object; + } +} +``` + +该方法应该就是创建 bean 实例对象中的核心方法之一了。这里我们关注三个方法:`beforeSingletonCreation()` 、 `afterSingletonCreation()` 、 `postProcessObjectFromFactoryBean()`。可能有小伙伴觉得前面两个方法不是很重要,LZ 可以肯定告诉你,这两方法是非常重要的操作,因为他们记录着 bean 的加载状态,是检测当前 bean 是否处于创建中的关键之处,对解决 bean 循环依赖起着关键作用。before 方法用于标志当前 bean 处于创建中,after 则是移除。其实在这篇博客刚刚开始就已经提到了 `isSingletonCurrentlyInCreation()` 是用于检测当前 bean 是否处于创建之中,如下: + +```java +public boolean isSingletonCurrentlyInCreation(String beanName) { + return this.singletonsCurrentlyInCreation.contains(beanName); +} +``` + +是根据 singletonsCurrentlyInCreation 集合中是否包含了 beanName,集合的元素则一定是在 `beforeSingletonCreation()` 中添加的,如下: + +```java +protected void beforeSingletonCreation(String beanName) { + if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) { + throw new BeanCurrentlyInCreationException(beanName); + } +} +``` + +`afterSingletonCreation()` 为移除,则一定就是对 singletonsCurrentlyInCreation 集合 remove 了,如下: + +```java +protected void afterSingletonCreation(String beanName) { + if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) { + throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation"); + } +} +``` + +我们再来看看真正的核心方法 doGetObjectFromFactoryBean + +```java +private Object doGetObjectFromFactoryBean(final FactoryBean factory, final String beanName) + throws BeanCreationException { + + Object object; + try { + if (System.getSecurityManager() != null) { + AccessControlContext acc = getAccessControlContext(); + try { + object = AccessController.doPrivileged((PrivilegedExceptionAction) factory::getObject, acc); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + object = factory.getObject(); + } + } + catch (FactoryBeanNotInitializedException ex) { + throw new BeanCurrentlyInCreationException(beanName, ex.toString()); + } + catch (Throwable ex) { + throw new BeanCreationException(beanName, "FactoryBean threw exception on object creation", ex); + } + + // Do not accept a null value for a FactoryBean that's not fully + // initialized yet: Many FactoryBeans just return null then. + if (object == null) { + if (isSingletonCurrentlyInCreation(beanName)) { + throw new BeanCurrentlyInCreationException( + beanName, "FactoryBean which is currently in creation returned null from getObject"); + } + object = new NullBean(); + } + return object; +} +``` + +以前我们曾经介绍过FactoryBean的调用方法,如果bean声明为FactoryBean类型,则当提取bean时候提取的不是FactoryBean,而是FactoryBean中对应的getObject方法返回的bean,而doGetObjectFromFactroyBean真是实现这个功能。 + +而调用完doGetObjectFromFactoryBean方法后,并没有直接返回,getObjectFromFactoryBean方法中还调用了object = postProcessObjectFromFactoryBean(object, beanName);方法,在子类AbstractAutowireCapableBeanFactory,有这个方法的实现: + +```java +@Override +protected Object postProcessObjectFromFactoryBean(Object object, String beanName) { + return applyBeanPostProcessorsAfterInitialization(object, beanName); +} +@Override +public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) + throws BeansException { + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + Object current = beanProcessor.postProcessAfterInitialization(result, beanName); + if (current == null) { + return result; + } + result = current; + } + return result; +} +``` + +对于后处理器的使用,我们目前还没接触,后续会有大量篇幅介绍,这里我们只需要了解在Spring获取bean的规则中有这样一条:尽可能保证所有bean初始化后都会调用注册的BeanPostProcessor的postProcessAfterInitialization方法进行处理,在实际开发过程中大可以针对此特性设计自己的业务处理。 \ No newline at end of file diff --git "a/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean\345\210\233\345\273\272.md" "b/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean\345\210\233\345\273\272.md" new file mode 100644 index 0000000..259e050 --- /dev/null +++ "b/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean\345\210\233\345\273\272.md" @@ -0,0 +1,702 @@ +**正文** + +在 Spring 中存在着不同的 scope,默认是 singleton ,还有 prototype、request 等等其他的 scope,他们的初始化步骤是怎样的呢?这个答案在这篇博客中给出。 + +## singleton + +Spring 的 scope 默认为 singleton,第一部分分析了从缓存中获取单例模式的 bean,但是如果缓存中不存在呢?则需要从头开始加载 bean,这个过程由 `getSingleton()` 实现。其初始化的代码如下: + +```java +if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); +} +``` + +这里我们看到了 java8的新特性lambda表达式 () -> , getSingleton方法的第二个参数为 `ObjectFactory singletonFactory,() ->`相当于创建了一个ObjectFactory类型的匿名内部类,去实现ObjectFactory接口中的getObject()方法,其中{}中的代码相当于写在匿名内部类中getObject()的代码片段,等着getSingleton方法里面通过ObjectFactory singletonFactory去显示调用,如singletonFactory.getObject()。上述代码可以反推成如下代码: + +```java +sharedInstance = getSingleton(beanName, new ObjectFactory() { + @Override + public Object getObject() { + try { + return createBean(beanName, mbd, args); + } catch (BeansException ex) { + destroySingleton(beanName); + throw ex; + } + } +}); +``` + +下面我们进入到 **getSingleton**方法中 + +```java +public Object getSingleton(String beanName, ObjectFactory singletonFactory) { + Assert.notNull(beanName, "Bean name must not be null"); + + // 全局加锁 + synchronized (this.singletonObjects) { + // 从缓存中检查一遍 + // 因为 singleton 模式其实就是复用已经创建的 bean 所以这步骤必须检查 + Object singletonObject = this.singletonObjects.get(beanName); + // 为空,开始加载过程 + if (singletonObject == null) { + // 省略 部分代码 + + // 加载前置处理 + beforeSingletonCreation(beanName); + boolean newSingleton = false; + // 省略代码 + try { + // 初始化 bean + // 这个过程就是我上面讲的调用匿名内部类的方法,其实是调用 createBean() 方法 + singletonObject = singletonFactory.getObject(); + newSingleton = true; + } + // 省略 catch 部分 + } + finally { + // 后置处理 + afterSingletonCreation(beanName); + } + // 加入缓存中 + if (newSingleton) { + addSingleton(beanName, singletonObject); + } + } + // 直接返回 + return singletonObject; + } +} +``` + +上述代码中其实,使用了回调方法,使得程序可以在单例创建的前后做一些准备及处理操作,而真正获取单例bean的方法其实并不是在此方法中实现的,其实现逻辑是在ObjectFactory类型的实例singletonFactory中实现的(即上图贴上的第一段代码)。而这些准备及处理操作包括如下内容。 + +(1)检查缓存是否已经加载过 + +(2)如果没有加载,则记录beanName的正在加载状态 + +(3)加载单例前记录加载状态。 可能你会觉得beforeSingletonCreation方法是个空实现,里面没有任何逻辑,但其实这个函数中做了一个很重要的操作:记录加载状态,也就是通过this.singletonsCurrentlyInCreation.add(beanName)将当前正要创建的bean记录在缓存中,这样便可以对循环依赖进行检测。 我们上一篇文章已经讲过,可以去看看。 + +(4)通过调用参数传入的ObjectFactory的个体Object方法实例化bean + +(5)加载单例后的处理方法调用。 同步骤3的记录加载状态相似,当bean加载结束后需要移除缓存中对该bean的正在加载状态的记录。 + +(6)将结果记录至缓存并删除加载bean过程中所记录的各种辅助状态。 + +(7)返回处理结果 + +我们看另外一个方法 `addSingleton()`。 + +```java +protected void addSingleton(String beanName, Object singletonObject) { + synchronized (this.singletonObjects) { + this.singletonObjects.put(beanName, singletonObject); + this.singletonFactories.remove(beanName); + this.earlySingletonObjects.remove(beanName); + this.registeredSingletons.add(beanName); + } +} +``` + +一个 put、一个 add、两个 remove。singletonObjects 单例 bean 的缓存,singletonFactories 单例 bean Factory 的缓存,earlySingletonObjects “早期”创建的单例 bean 的缓存,registeredSingletons 已经注册的单例缓存。 + +加载了单例 bean 后,调用 `getObjectForBeanInstance()` 从 bean 实例中获取对象。该方法我们在上一篇中已经讲过。 + +## 原型模式 + +```java +else if (mbd.isPrototype()) { + Object prototypeInstance = null; + try { + beforePrototypeCreation(beanName); + prototypeInstance = createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd); +} +``` + +原型模式的初始化过程很简单:直接创建一个新的实例就可以了。过程如下: + +1. 调用 `beforeSingletonCreation()` 记录加载原型模式 bean 之前的加载状态,即前置处理。 +2. 调用 `createBean()` 创建一个 bean 实例对象。 +3. 调用 `afterSingletonCreation()` 进行加载原型模式 bean 后的后置处理。 +4. 调用 `getObjectForBeanInstance()` 从 bean 实例中获取对象。 + + + +## 其他作用域 + +```java +String scopeName = mbd.getScope(); +final Scope scope = this.scopes.get(scopeName); +if (scope == null) { + throw new IllegalStateException("No Scope registered for scope name '" + scopeName + "'"); +} +try { + Object scopedInstance = scope.get(beanName, () -> { + beforePrototypeCreation(beanName); + try { + return createBean(beanName, mbd, args); + } + finally { + afterPrototypeCreation(beanName); + } + }); + bean = getObjectForBeanInstance(scopedInstance, name, beanName, mbd); +} +catch (IllegalStateException ex) { + throw new BeanCreationException(beanName, + "Scope '" + scopeName + "' is not active for the current thread; consider " + + "defining a scoped proxy for this bean if you intend to refer to it from a singleton", + ex); +} +``` + +核心流程和原型模式一样,只不过获取 bean 实例是由 `scope.get()` 实现,如下: + +```java +public Object get(String name, ObjectFactory objectFactory) { + // 获取 scope 缓存 + Map scope = this.threadScope.get(); + Object scopedObject = scope.get(name); + if (scopedObject == null) { + scopedObject = objectFactory.getObject(); + // 加入缓存 + scope.put(name, scopedObject); + } + return scopedObject; +} +``` + +对于上面三个模块,其中最重要的方法,是 `createBean(),也就是核心创建bean的过程,下面我们来具体看看。` + +## 准备创建bean + +```java +if (mbd.isSingleton()) { + sharedInstance = getSingleton(beanName, () -> { + try { + return createBean(beanName, mbd, args); + } + catch (BeansException ex) { + destroySingleton(beanName); + throw ex; + } + }); + bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd); +} +``` + +如上所示,createBean是真正创建bean的地方,此方法是定义在AbstractAutowireCapableBeanFactory中,我们看下其源码: + +```java +protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) + throws BeanCreationException { + + if (logger.isDebugEnabled()) { + logger.debug("Creating instance of bean '" + beanName + "'"); + } + RootBeanDefinition mbdToUse = mbd; + + // 确保此时的 bean 已经被解析了 + // 如果获取的class 属性不为null,则克隆该 BeanDefinition + // 主要是因为该动态解析的 class 无法保存到到共享的 BeanDefinition + Class resolvedClass = resolveBeanClass(mbd, beanName); + if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) { + mbdToUse = new RootBeanDefinition(mbd); + mbdToUse.setBeanClass(resolvedClass); + } + + try { + // 验证和准备覆盖方法 + mbdToUse.prepareMethodOverrides(); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanDefinitionStoreException(mbdToUse.getResourceDescription(), + beanName, "Validation of method overrides failed", ex); + } + + try { + // 给 BeanPostProcessors 一个机会用来返回一个代理类而不是真正的类实例 + // AOP 的功能就是基于这个地方 + Object bean = resolveBeforeInstantiation(beanName, mbdToUse); + if (bean != null) { + return bean; + } + } + catch (Throwable ex) { + throw new BeanCreationException(mbdToUse.getResourceDescription(), beanName, + "BeanPostProcessor before instantiation of bean failed", ex); + } + + try { + // 执行真正创建 bean 的过程 + Object beanInstance = doCreateBean(beanName, mbdToUse, args); + if (logger.isDebugEnabled()) { + logger.debug("Finished creating instance of bean '" + beanName + "'"); + } + return beanInstance; + } + catch (BeanCreationException | ImplicitlyAppearedSingletonException ex) { + throw ex; + } + catch (Throwable ex) { + throw new BeanCreationException( + mbdToUse.getResourceDescription(), beanName, "Unexpected exception during bean creation", ex); + } +} +``` + +### 实例化的前置处理 + +`resolveBeforeInstantiation()` 的作用是给 BeanPostProcessors 后置处理器返回一个代理对象的机会,其实在调用该方法之前 Spring 一直都没有创建 bean ,那么这里返回一个 bean 的代理类有什么作用呢?作用体现在后面的 `if` 判断: + +```java +if (bean != null) { + return bean; +} +``` + +如果代理对象不为空,则直接返回代理对象,这一步骤有非常重要的作用,Spring 后续实现 AOP 就是基于这个地方判断的。 + +```java +protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { + Object bean = null; + if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) { + if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + Class targetType = determineTargetType(beanName, mbd); + if (targetType != null) { + bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName); + if (bean != null) { + bean = applyBeanPostProcessorsAfterInitialization(bean, beanName); + } + } + } + mbd.beforeInstantiationResolved = (bean != null); + } + return bean; +} +``` + +这个方法核心就在于 `applyBeanPostProcessorsBeforeInstantiation()` 和 `applyBeanPostProcessorsAfterInitialization()` 两个方法,before 为实例化前的后处理器应用,after 为实例化后的后处理器应用,由于本文的主题是创建 bean,关于 Bean 的增强处理后续 LZ 会单独出博文来做详细说明。 + +### 创建 bean + +如果没有代理对象,就只能走常规的路线进行 bean 的创建了,该过程有 `doCreateBean()` 实现,如下: + +```java +protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args) + throws BeanCreationException { + + // BeanWrapper是对Bean的包装,其接口中所定义的功能很简单包括设置获取被包装的对象,获取被包装bean的属性描述器 + BeanWrapper instanceWrapper = null; + // 单例模型,则从未完成的 FactoryBean 缓存中删除 + if (mbd.isSingleton()) {anceWrapper = this.factoryBeanInstanceCache.remove(beanName); + } + + // 使用合适的实例化策略来创建新的实例:工厂方法、构造函数自动注入、简单初始化 + if (instanceWrapper == null) { + instanceWrapper = createBeanInstance(beanName, mbd, args); + } + + // 包装的实例对象 + final Object bean = instanceWrapper.getWrappedInstance(); + // 包装的实例对象的类型 + Class beanType = instanceWrapper.getWrappedClass(); + if (beanType != NullBean.class) { + mbd.resolvedTargetType = beanType; + } + + // 检测是否有后置处理 + // 如果有后置处理,则允许后置处理修改 BeanDefinition + synchronized (mbd.postProcessingLock) { + if (!mbd.postProcessed) { + try { + // applyMergedBeanDefinitionPostProcessors + // 后置处理修改 BeanDefinition + applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName); + } + catch (Throwable ex) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Post-processing of merged bean definition failed", ex); + } + mbd.postProcessed = true; + } + } + + // 解决单例模式的循环依赖 + // 单例模式 & 允许循环依赖&当前单例 bean 是否正在被创建 + boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && + isSingletonCurrentlyInCreation(beanName)); + if (earlySingletonExposure) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly caching bean '" + beanName + + "' to allow for resolving potential circular references"); + } + // 提前将创建的 bean 实例加入到ObjectFactory 中 + // 这里是为了后期避免循环依赖 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); + } + + /* + * 开始初始化 bean 实例对象 + */ + Object exposedObject = bean; + try { + // 对 bean 进行填充,将各个属性值注入,其中,可能存在依赖于其他 bean 的属性 + // 则会递归初始依赖 bean + populateBean(beanName, mbd, instanceWrapper); + // 调用初始化方法,比如 init-method + exposedObject = initializeBean(beanName, exposedObject, mbd); + } + catch (Throwable ex) { + if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) { + throw (BeanCreationException) ex; + } + else { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Initialization of bean failed", ex); + } + } + + /** + * 循环依赖处理 + */ + if (earlySingletonExposure) { + // 获取 earlySingletonReference + Object earlySingletonReference = getSingleton(beanName, false); + // 只有在存在循环依赖的情况下,earlySingletonReference 才不会为空 + if (earlySingletonReference != null) { + // 如果 exposedObject 没有在初始化方法中被改变,也就是没有被增强 + if (exposedObject == bean) { + exposedObject = earlySingletonReference; + } + // 处理依赖 + else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { + String[] dependentBeans = getDependentBeans(beanName); + Set actualDependentBeans = new LinkedHashSet<>(dependentBeans.length); + for (String dependentBean : dependentBeans) { + if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) { + actualDependentBeans.add(dependentBean); + } + } + if (!actualDependentBeans.isEmpty()) { + throw new BeanCurrentlyInCreationException(beanName, + "Bean with name '" + beanName + "' has been injected into other beans [" + + StringUtils.collectionToCommaDelimitedString(actualDependentBeans) + + "] in its raw version as part of a circular reference, but has eventually been " + + "wrapped. This means that said other beans do not use the final version of the " + + "bean. This is often the result of over-eager type matching - consider using " + + "'getBeanNamesOfType' with the 'allowEagerInit' flag turned off, for example."); + } + } + } + } + try { + // 注册 bean + registerDisposableBeanIfNecessary(beanName, bean, mbd); + } + catch (BeanDefinitionValidationException ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Invalid destruction signature", ex); + } + + return exposedObject; +} +``` + +大概流程如下: + +- `createBeanInstance()` 实例化 bean +- `populateBean()` 属性填充 +- 循环依赖的处理 +- `initializeBean()` 初始化 bean + +#### createBeanInstance + +我们首先从createBeanInstance方法开始。方法代码如下: + +```java +protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) { + // 解析 bean,将 bean 类名解析为 class 引用 + Class beanClass = resolveBeanClass(mbd, beanName); + + if (beanClass != null && !Modifier.isPublic(beanClass.getModifiers()) && !mbd.isNonPublicAccessAllowed()) { + throw new BeanCreationException(mbd.getResourceDescription(), beanName, + "Bean class isn't public, and non-public access not allowed: " + beanClass.getName()); + } + + // 如果存在 Supplier 回调,则使用给定的回调方法初始化策略 + Supplier instanceSupplier = mbd.getInstanceSupplier(); + if (instanceSupplier != null) { + return obtainFromSupplier(instanceSupplier, beanName); + } + + // 如果工厂方法不为空,则使用工厂方法初始化策略,这里推荐看Factory-Method实例化Bean + if (mbd.getFactoryMethodName() != null) { + return instantiateUsingFactoryMethod(beanName, mbd, args); + } + + boolean resolved = false; + boolean autowireNecessary = false; + if (args == null) { + // constructorArgumentLock 构造函数的常用锁 + synchronized (mbd.constructorArgumentLock) { + // 如果已缓存的解析的构造函数或者工厂方法不为空,则可以利用构造函数解析 + // 因为需要根据参数确认到底使用哪个构造函数,该过程比较消耗性能,所有采用缓存机制 + if (mbd.resolvedConstructorOrFactoryMethod != null) { + resolved = true; + autowireNecessary = mbd.constructorArgumentsResolved; + } + } + } + // 已经解析好了,直接注入即可 + if (resolved) { + // 自动注入,调用构造函数自动注入 + if (autowireNecessary) { + return autowireConstructor(beanName, mbd, null, null); + } + else { + // 使用默认构造函数构造 + return instantiateBean(beanName, mbd); + } + } + + // 确定解析的构造函数 + // 主要是检查已经注册的 SmartInstantiationAwareBeanPostProcessor + Constructor[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName); + if (ctors != null || + mbd.getResolvedAutowireMode() == RootBeanDefinition.AUTOWIRE_CONSTRUCTOR || + mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) { + // 构造函数自动注入 + return autowireConstructor(beanName, mbd, ctors, args); + } + + //使用默认构造函数注入 + return instantiateBean(beanName, mbd); +} +``` + +实例化 bean 是一个复杂的过程,其主要的逻辑为: + +- 如果存在 Supplier 回调,则调用 `obtainFromSupplier()` 进行初始化 +- 如果存在工厂方法,则使用工厂方法进行初始化 +- 首先判断缓存,如果缓存中存在,即已经解析过了,则直接使用已经解析了的,根据 constructorArgumentsResolved 参数来判断是使用构造函数自动注入还是默认构造函数 +- 如果缓存中没有,则需要先确定到底使用哪个构造函数来完成解析工作,因为一个类有多个构造函数,每个构造函数都有不同的构造参数,所以需要根据参数来锁定构造函数并完成初始化,如果存在参数则使用相应的带有参数的构造函数,否则使用默认构造函数。 + +**instantiateBean** + +不带参数的构造函数的实例化过程使用的方法是instantiateBean(beanName, mbd),我们看下源码: + +```java +protected BeanWrapper instantiateBean(final String beanName, final RootBeanDefinition mbd) { + try { + Object beanInstance; + final BeanFactory parent = this; + if (System.getSecurityManager() != null) { + beanInstance = AccessController.doPrivileged((PrivilegedAction) () -> + getInstantiationStrategy().instantiate(mbd, beanName, parent), + getAccessControlContext()); + } + else { + beanInstance = getInstantiationStrategy().instantiate(mbd, beanName, parent); + } + BeanWrapper bw = new BeanWrapperImpl(beanInstance); + initBeanWrapper(bw); + return bw; + } + catch (Throwable ex) { + throw new BeanCreationException( + mbd.getResourceDescription(), beanName, "Instantiation of bean failed", ex); + } +} +``` + +**实例化策略** + +实例化过程中,反复提到了实例化策略,这是做什么的呢?其实,经过前面的分析,我们已经得到了足以实例化的相关信息,完全可以使用最简单的反射方法来构造实例对象,但Spring却没有这么做。 + +接下来我们看下Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner)方法,具体的实现是在SimpleInstantiationStrategy中,具体代码如下: + +```java +public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + // 没有覆盖 + // 直接使用反射实例化即可 + if (!bd.hasMethodOverrides()) { + // 重新检测获取下构造函数 + // 该构造函数是经过前面 N 多复杂过程确认的构造函数 + Constructor constructorToUse; + synchronized (bd.constructorArgumentLock) { + // 获取已经解析的构造函数 + constructorToUse = (Constructor) bd.resolvedConstructorOrFactoryMethod; + // 如果为 null,从 class 中解析获取,并设置 + if (constructorToUse == null) { + final Class clazz = bd.getBeanClass(); + if (clazz.isInterface()) { + throw new BeanInstantiationException(clazz, "Specified class is an interface"); + } + try { + if (System.getSecurityManager() != null) { + constructorToUse = AccessController.doPrivileged( + (PrivilegedExceptionAction>) clazz::getDeclaredConstructor); + } + else { + //利用反射获取构造器 + constructorToUse = clazz.getDeclaredConstructor(); + } + bd.resolvedConstructorOrFactoryMethod = constructorToUse; + } + catch (Throwable ex) { + throw new BeanInstantiationException(clazz, "No default constructor found", ex); + } + } + } + + // 通过BeanUtils直接使用构造器对象实例化bean + return BeanUtils.instantiateClass(constructorToUse); + } + else { + // 生成CGLIB创建的子类对象 + return instantiateWithMethodInjection(bd, beanName, owner); + } +} +``` + +如果该 bean 没有配置 lookup-method、replaced-method 标签或者 @Lookup 注解,则直接通过反射的方式实例化 bean 即可,方便快捷,但是如果存在需要覆盖的方法或者动态替换的方法则需要使用 CGLIB 进行动态代理,因为可以在创建代理的同时将动态方法织入类中。 + +调用工具类 BeanUtils 的 `instantiateClass()` 方法完成反射工作: + +```java +public static T instantiateClass(Constructor ctor, Object... args) throws BeanInstantiationException { + Assert.notNull(ctor, "Constructor must not be null"); + try { + ReflectionUtils.makeAccessible(ctor); + return (KotlinDetector.isKotlinType(ctor.getDeclaringClass()) ? + KotlinDelegate.instantiateClass(ctor, args) : ctor.newInstance(args)); + } + // 省略一些 catch +} +``` + +**CGLIB** + +```java +protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + throw new UnsupportedOperationException("Method Injection not supported in SimpleInstantiationStrategy"); +} +``` + +方法默认是没有实现的,具体过程由其子类 CglibSubclassingInstantiationStrategy 实现: + +```java +protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner) { + return instantiateWithMethodInjection(bd, beanName, owner, null); +} + +protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, + @Nullable Constructor ctor, @Nullable Object... args) { + + // 通过CGLIB生成一个子类对象 + return new CglibSubclassCreator(bd, owner).instantiate(ctor, args); +} +``` + +创建一个 CglibSubclassCreator 对象,调用其 `instantiate()` 方法生成其子类对象: + +```java +public Object instantiate(@Nullable Constructor ctor, @Nullable Object... args) { + // 通过 Cglib 创建一个代理类 + Class subclass = createEnhancedSubclass(this.beanDefinition); + Object instance; + // 没有构造器,通过 BeanUtils 使用默认构造器创建一个bean实例 + if (ctor == null) { + instance = BeanUtils.instantiateClass(subclass); + } + else { + try { + // 获取代理类对应的构造器对象,并实例化 bean + Constructor enhancedSubclassConstructor = subclass.getConstructor(ctor.getParameterTypes()); + instance = enhancedSubclassConstructor.newInstance(args); + } + catch (Exception ex) { + throw new BeanInstantiationException(this.beanDefinition.getBeanClass(), + "Failed to invoke constructor for CGLIB enhanced subclass [" + subclass.getName() + "]", ex); + } + } + + // 为了避免memory leaks异常,直接在bean实例上设置回调对象 + Factory factory = (Factory) instance; + factory.setCallbacks(new Callback[] {NoOp.INSTANCE, + new CglibSubclassingInstantiationStrategy.LookupOverrideMethodInterceptor(this.beanDefinition, this.owner), + new CglibSubclassingInstantiationStrategy.ReplaceOverrideMethodInterceptor(this.beanDefinition, this.owner)}); + return instance; +} +``` + +当然这里还没有具体分析 CGLIB 生成子类的详细过程,具体的过程等后续分析 AOP 的时候再详细地介绍。 + +### 记录创建bean的ObjectFactory + +在刚刚创建完Bean的实例后,也就是刚刚执行完构造器实例化后,doCreateBean方法中有下面一段代码: + +```java +boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences && + isSingletonCurrentlyInCreation(beanName)); +if (earlySingletonExposure) { + if (logger.isDebugEnabled()) { + logger.debug("Eagerly caching bean '" + beanName + + "' to allow for resolving potential circular references"); + } + //为避免后期循环依赖,可以在bean初始化完成前将创建实例的ObjectFactory加入工厂 + //依赖处理:在Spring中会有循环依赖的情况,例如,当A中含有B的属性,而B中又含有A的属性时就会 + //构成一个循环依赖,此时如果A和B都是单例,那么在Spring中的处理方式就是当创建B的时候,涉及 + //自动注入A的步骤时,并不是直接去再次创建A,而是通过放入缓存中的ObjectFactory来创建实例, + //这样就解决了循环依赖的问题。 + addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean)); +} +``` + +isSingletonCurrentlyInCreation(beanName):该bean是否在创建中。在Spring中,会有个专门的属性默认为DefaultSingletonBeanRegistry的singletonsCurrentlyInCreation来记录bean的加载状态,在bean开始创建前会将beanName记录在属性中,在bean创建结束后会将beanName移除。那么我们跟随代码一路走下来可以对这个属性的记录并没有多少印象,这个状态是在哪里记录的呢?不同scope的记录位置不一样,我们以singleton为例,在singleton下记录属性的函数是在DefaultSingletonBeanRegistry类的public Object getSingleton(String beanName,ObjectFactory singletonFactory)函数的beforeSingletonCreation(beanName)和afterSingletonCreation(beanName)中,在这两段函数中分别this.singletonsCurrentlyInCreation.add(beanName)与this.singletonsCurrentlyInCreation.remove(beanName)来进行状态的记录与移除。 + +变量earlySingletonExposure是否是单例,是否允许循环依赖,是否对应的bean正在创建的条件的综合。当这3个条件都满足时会执行addSingletonFactory操作,那么加入SingletonFactory的作用是什么?又是在什么时候调用的? + +我们还是以最简单AB循环为例,类A中含有属性B,而类B中又会含有属性A,那么初始化beanA的过程如下: + +![](http://img.topjavaer.cn/img/202309200747808.png) + +上图展示了创建BeanA的流程,在创建A的时候首先会记录类A所对应额beanName,并将beanA的创建工厂加入缓存中,而在对A的属性填充也就是调用pupulateBean方法的时候又会再一次的对B进行递归创建。同样的,因为在B中同样存在A属性,因此在实例化B的populateBean方法中又会再次地初始化B,也就是图形的最后,调用getBean(A).关键是在这里,我们之前分析过,在这个函数中并不是直接去实例化A,而是先去检测缓存中是否有已经创建好的对应的bean,或者是否已经创建的ObjectFactory,而此时对于A的ObjectFactory我们早已经创建,所以便不会再去向后执行,而是直接调用ObjectFactory去创建A.这里最关键的是ObjectFactory的实现。 + +其中getEarlyBeanReference的代码如下: + +```java +protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) { + Object exposedObject = bean; + if (bean != null && !mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) { + for (BeanPostProcessor bp : getBeanPostProcessors()) { + if (bp instanceof SmartInstantiationAwareBeanPostProcessor) { + SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp; + exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName); + if (exposedObject == null) { + return exposedObject; + } + } + } + } + return exposedObject; +} +``` + +在getEarlyBeanReference函数中除了后处理的调用外没有别的处理工作,根据分析,基本可以理清Spring处理循环依赖的解决办法,在B中创建依赖A时通过ObjectFactory提供的实例化方法来获取原始A,使B中持有的A仅仅是刚刚初始化并没有填充任何属性的A,而这初始化A的步骤还是刚刚创建A时进行的,但是因为A与B中的A所表示的属性地址是一样的所以在A中创建好的属性填充自然可以通过B中的A获取,这样就解决了循环依赖的问题。 \ No newline at end of file diff --git "a/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276\350\247\243\346\236\220.md" "b/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276\350\247\243\346\236\220.md" new file mode 100644 index 0000000..1f7c14d --- /dev/null +++ "b/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276\350\247\243\346\236\220.md" @@ -0,0 +1,602 @@ +## 概述 + +之前我们已经介绍了spring中默认标签的解析,解析来我们将分析自定义标签的解析,我们先回顾下自定义标签解析所使用的方法,如下图所示: + +![](http://img.topjavaer.cn/img/202309180850242.png) + +我们看到自定义标签的解析是通过BeanDefinitionParserDelegate.parseCustomElement(ele)进行的,解析来我们进行详细分析。 + + + +## 自定义标签的使用 + +扩展 Spring 自定义标签配置一般需要以下几个步骤: + +1. 创建一个需要扩展的组件 +2. 定义一个 XSD 文件,用于描述组件内容 +3. 创建一个实现 AbstractSingleBeanDefinitionParser 接口的类,用来解析 XSD 文件中的定义和组件定义 +4. 创建一个 Handler,继承 NamespaceHandlerSupport ,用于将组件注册到 Spring 容器 +5. 编写 Spring.handlers 和 Spring.schemas 文件 + +下面就按照上面的步骤来实现一个自定义标签组件。 + +### 创建组件 + +该组件就是一个普通的 JavaBean,没有任何特别之处。这里我创建了两个组件,为什么是两个,后面有用到 + +**User.java** + +```java +package dabin.spring01; + +public class User { + + private String id; + + private String userName; + + private String email;public void setId(String id) { + this.id = id; + }public void setUserName(String userName) { + this.userName = userName; + }public void setEmail(String email) { + this.email = email; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("{"); + sb.append("\"id\":\"") + .append(id).append('\"'); + sb.append(",\"userName\":\"") + .append(userName).append('\"'); + sb.append(",\"email\":\"") + .append(email).append('\"'); + sb.append('}'); + return sb.toString(); + } +} +``` + +**Phone.java** + +```java +package dabin.spring01; + +public class Phone { + + private String color; + + private int size; + + private String remark; + + + public void setColor(String color) { + this.color = color; + } + + public void setSize(int size) { + this.size = size; + } + + public void setRemark(String remark) { + this.remark = remark; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("{"); + sb.append("\"color\":\"") + .append(color).append('\"'); + sb.append(",\"size\":") + .append(size); + sb.append(",\"remark\":\"") + .append(remark).append('\"'); + sb.append('}'); + return sb.toString(); + } +} +``` + +### 定义 XSD 文件 + + + +```xml + + + + + + + + + + + + + + + + + + + + +``` + +在上述XSD文件中描述了一个新的targetNamespace,并在这个空间里定义了一个name为**user**和**phone**的element 。user里面有三个attribute。主要是为了验证Spring配置文件中的自定义格式。再进一步解释,就是,Spring位置文件中使用的user自定义标签中,属性只能是上面的三种,有其他的属性的话,就会报错。 + + + +### Parser 类 + +定义一个 Parser 类,该类继承 **AbstractSingleBeanDefinitionParser** ,并实现 **`getBeanClass()`** 和 **`doParse()`** 两个方法。主要是用于解析 XSD 文件中的定义和组件定义。这里定义了两个Parser类,一个是解析User类,一个用来解析Phone类。 + +**UserBeanDefinitionParser.java** + +```java +package dabin.spring01; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + @Override + protected Class getBeanClass(Element ele){ + return User.class; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String id = element.getAttribute("id"); + String userName=element.getAttribute("userName"); + String email=element.getAttribute("email"); + if(StringUtils.hasText(id)){ + builder.addPropertyValue("id",id); + } + if(StringUtils.hasText(userName)){ + builder.addPropertyValue("userName", userName); + } + if(StringUtils.hasText(email)){ + builder.addPropertyValue("email", email); + } + + } +} +``` + +**PhoneBeanDefinitionParser.java** + +```java +package dabin.spring01; + +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser; +import org.springframework.util.StringUtils; +import org.w3c.dom.Element; + +public class PhoneBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + @Override + protected Class getBeanClass(Element ele){ + return Phone.class; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String color = element.getAttribute("color"); + int size=Integer.parseInt(element.getAttribute("size")); + String remark=element.getAttribute("remark"); + if(StringUtils.hasText(color)){ + builder.addPropertyValue("color",color); + } + if(StringUtils.hasText(String.valueOf(size))){ + builder.addPropertyValue("size", size); + } + if(StringUtils.hasText(remark)){ + builder.addPropertyValue("remark", remark); + } + + } +} +``` + + + +### Handler 类 + +定义 Handler 类,继承 NamespaceHandlerSupport ,主要目的是将上面定义的解析器**Parser类**注册到 Spring 容器中。 + +```java +package dabin.spring01; + +import org.springframework.beans.factory.xml.NamespaceHandlerSupport; + +public class MyNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("user",new UserBeanDefinitionParser()); + + registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser()); + } + +} +``` + +我们看看 registerBeanDefinitionParser 方法做了什么 + +```java +private final Map parsers = new HashMap<>(); + +protected final void registerBeanDefinitionParser(String elementName, BeanDefinitionParser parser) { + this.parsers.put(elementName, parser); +} +``` + +就是将解析器 UserBeanDefinitionParser和 PhoneBeanDefinitionParser 的实例放到全局的Map中,key为user和phone。 + +### Spring.handlers和Spring.schemas + +编写Spring.handlers和Spring.schemas文件,默认位置放在工程的META-INF文件夹下 + +**Spring.handlers** + +``` +http\://www.dabin.com/schema/user=dabin.spring01.MyNamespaceHandler +``` + +**Spring.schemas** + +``` +http\://www.dabin.com/schema/user.xsd=org/user.xsd +``` + +而 Spring 加载自定义的大致流程是遇到自定义标签然后 就去 Spring.handlers 和 Spring.schemas 中去找对应的 handler 和 XSD ,默认位置是 META-INF 下,进而有找到对应的handler以及解析元素的 Parser ,从而完成了整个自定义元素的解析,也就是说 Spring 将向定义标签解析的工作委托给了 用户去实现。 + +### 创建测试配置文件 + +经过上面几个步骤,就可以使用自定义的标签了。在 xml 配置文件中使用如下: + +```xml + + + + + + + + + + +``` + +**xmlns:myTag**表示myTag的命名空间是 **http://www.dabin.com/schema/user ,**在文章开头的判断处 if (delegate.isDefaultNamespace(ele)) 肯定会返回false,将进入到自定义标签的解析 + +### 测试 + +``` +import dabin.spring01.MyTestBean; +import dabin.spring01.Phone; +import dabin.spring01.User; +import org.junit.Test; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.xml.XmlBeanFactory; +import org.springframework.core.io.ClassPathResource; + +public class AppTest { + @Test + public void MyTestBeanTest() { + BeanFactory bf = new XmlBeanFactory( new ClassPathResource("spring-config.xml")); + //MyTestBean myTestBean01 = (MyTestBean) bf.getBean("myTestBean"); + User user = (User) bf.getBean("user"); + Phone iphone = (Phone) bf.getBean("iphone"); + + System.out.println(user); + System.out.println(iphone); + } + +} +``` + +输出结果: + +``` +("id":"user","userName":"dabin","email":"dabin@163. com”} +{"color":"black","size":128,"remark":"iphone XR"} +``` + + + +## 自定义标签的解析 + + 了解了自定义标签的使用后,接下来我们分析下自定义标签的解析,自定义标签解析用的是方法:parseCustomElement(Element ele, @Nullable BeanDefinition containingBd),进入方法体: + +```java +public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { + // 获取 标签对应的命名空间 + String namespaceUri = getNamespaceURI(ele); + if (namespaceUri == null) { + return null; + } + + // 根据 命名空间找到相应的 NamespaceHandler + NamespaceHandler handler = this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri); + if (handler == null) { + error("Unable to locate Spring NamespaceHandler for XML schema namespace [" + namespaceUri + "]", ele); + return null; + } + + // 调用自定义的 Handler 处理 + return handler.parse(ele, new ParserContext(this.readerContext, this, containingBd)); +} +``` + +相信了解了自定义标签的使用方法后,或多或少会对向定义标签的实现过程有一个自己的想法。其实思路非常的简单,无非是根据对应的bean 获取对应的命名空间 ,根据命名空间解析对应的处理器,然后根据用户自定义的处理器进行解析。 + + + +### 获取标签的命名空间 + + 标签的解析是从命名空间的提起开始的,元论是**区分 Spring中默认标签和自定义标** 还是 **区分自定义标签中不同标签的处理器**都是以标签所提供的命名空间为基础的,而至于如何提取对应元素的命名空间其实并不需要我们亲内去实现,在 org.w3c.dom.Node 中已经提供了方法供我们直接调用: + +``` +String namespaceUri = getNamespaceURI(ele); +@Nullable +public String getNamespaceURI(Node node) { + return node.getNamespaceURI(); +} +``` + +这里我们可以通过DEBUG看出**myTag:user自定义标签对应的** namespaceUri 是 **http://www.dabin.com/schema/user** + + + +### 读取自定义标签处理器 + +根据 namespaceUri 获取 Handler,这个映射关系我们在 Spring.handlers 中已经定义了,所以只需要找到该类,然后初始化返回,最后调用该 Handler 对象的 `parse()` 方法处理,该方法我们也提供了实现。所以上面的核心就在于怎么找到该 Handler 类。调用方法为:`this.readerContext.getNamespaceHandlerResolver().resolve(namespaceUri)` + +```java +public NamespaceHandler resolve(String namespaceUri) { + // 获取所有已经配置的 Handler 映射 + Map handlerMappings = getHandlerMappings(); + + // 根据 namespaceUri 获取 handler的信息:这里一般都是类路径 + Object handlerOrClassName = handlerMappings.get(namespaceUri); + if (handlerOrClassName == null) { + return null; + } + else if (handlerOrClassName instanceof NamespaceHandler) { + // 如果已经做过解析,直接返回 + return (NamespaceHandler) handlerOrClassName; + } + else { + String className = (String) handlerOrClassName; + try { + + Class handlerClass = ClassUtils.forName(className, this.classLoader); + if (!NamespaceHandler.class.isAssignableFrom(handlerClass)) { + throw new FatalBeanException("Class [" + className + "] for namespace [" + namespaceUri + + "] does not implement the [" + NamespaceHandler.class.getName() + "] interface"); + } + + // 初始化类 + NamespaceHandler namespaceHandler = (NamespaceHandler) BeanUtils.instantiateClass(handlerClass); + + // 调用 自定义NamespaceHandler 的init() 方法 + namespaceHandler.init(); + + // 记录在缓存 + handlerMappings.put(namespaceUri, namespaceHandler); + return namespaceHandler; + } + catch (ClassNotFoundException ex) { + throw new FatalBeanException("Could not find NamespaceHandler class [" + className + + "] for namespace [" + namespaceUri + "]", ex); + } + catch (LinkageError err) { + throw new FatalBeanException("Unresolvable class definition for NamespaceHandler class [" + + className + "] for namespace [" + namespaceUri + "]", err); + } + } +} +``` + +首先调用 `getHandlerMappings()` 获取所有配置文件中的映射关系 handlerMappings ,就是我们在 Spring.handlers 文件中配置 命名空间与命名空间处理器的映射关系,该关系为 <命名空间,类路径>,然后根据命名空间 namespaceUri 从映射关系中获取相应的信息,如果为空或者已经初始化了就直接返回,否则根据反射对其进行初始化,同时调用其 `init()`方法,最后将该 Handler 对象缓存。我们再次回忆下示例中对于命名空间处理器的内容: + +```java +public class MyNamespaceHandler extends NamespaceHandlerSupport { + + @Override + public void init() { + registerBeanDefinitionParser("user",new UserBeanDefinitionParser()); + + registerBeanDefinitionParser("phone",new PhoneBeanDefinitionParser()); + } + +} +``` + +当得到自定义命名空间处理后会马上执行 namespaceHandler.init() 来进行自定义 BeanDefinitionParser的注册,在这里,你可以注册多个标签解析器,如当前示例中 parsers` 。 + +### 标签解析 + +得到了解析器和分析的元素后,Spring就可以将解析工作委托给自定义解析器去解析了,对于标签的解析使用的是:NamespaceHandler.parse(ele, new ParserContext(this.readerContext, this, containingBd))方法,进入到方法体内: + +```java +public BeanDefinition parse(Element element, ParserContext parserContext) { + BeanDefinitionParser parser = findParserForElement(element, parserContext); + return (parser != null ? parser.parse(element, parserContext) : null); +} +``` + +调用 `findParserForElement()` 方法获取 BeanDefinitionParser 实例,其实就是获取在 `init()` 方法里面注册的实例对象。如下: + +```java +private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { + //获取元素名称,也就是 beanClass = getBeanClass(element); + if (beanClass != null) { + builder.getRawBeanDefinition().setBeanClass(beanClass); + } + else { + // beanClass 为 null,意味着子类并没有重写 getBeanClass() 方法,则尝试去判断是否重写了 getBeanClassName() + String beanClassName = getBeanClassName(element); + if (beanClassName != null) { + builder.getRawBeanDefinition().setBeanClassName(beanClassName); + } + } + builder.getRawBeanDefinition().setSource(parserContext.extractSource(element)); + BeanDefinition containingBd = parserContext.getContainingBeanDefinition(); + if (containingBd != null) { + // Inner bean definition must receive same scope as containing bean. + builder.setScope(containingBd.getScope()); + } + if (parserContext.isDefaultLazyInit()) { + // Default-lazy-init applies to custom bean definitions as well. + builder.setLazyInit(true); + } + + // 调用子类的 doParse() 进行解析 + doParse(element, parserContext, builder); + return builder.getBeanDefinition(); +} + +public static BeanDefinitionBuilder genericBeanDefinition() { + return new BeanDefinitionBuilder(new GenericBeanDefinition()); +} + +protected Class getBeanClass(Element element) { + return null; +} + +protected void doParse(Element element, BeanDefinitionBuilder builder) { +} +``` + +在该方法中我们主要关注两个方法:**`getBeanClass()` 、`doParse()`**。对于 `getBeanClass()` 方法,AbstractSingleBeanDefinitionParser 类并没有提供具体实现,而是直接返回 null,意味着它希望子类能够重写该方法,当然如果没有重写该方法,这会去调用 `getBeanClassName()` ,判断子类是否已经重写了该方法。对于 `doParse()` 则是直接空实现。所以对于 `parseInternal()` 而言它总是期待它的子类能够实现 `getBeanClass()`、`doParse()`,其中 `doParse()` 尤为重要,如果你不提供实现,怎么来解析自定义标签呢?最后将自定义的解析器:UserDefinitionParser 再次回观。 + +```java +public class UserBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { + @Override + protected Class getBeanClass(Element ele){ + return User.class; + } + + @Override + protected void doParse(Element element, BeanDefinitionBuilder builder) { + String id = element.getAttribute("id"); + String userName=element.getAttribute("userName"); + String email=element.getAttribute("email"); + if(StringUtils.hasText(id)){ + builder.addPropertyValue("id",id); + } + if(StringUtils.hasText(userName)){ + builder.addPropertyValue("userName", userName); + } + if(StringUtils.hasText(email)){ + builder.addPropertyValue("email", email); + } + + } +} +``` + +我们看看 builder.addPropertyValue ("id",id) ,实际上是将自定义标签中的属性解析,存入 BeanDefinitionBuilder 中的 beanDefinition实例中 + +``` +private final AbstractBeanDefinition beanDefinition; + +public BeanDefinitionBuilder addPropertyValue(String name, @Nullable Object value) { + this.beanDefinition.getPropertyValues().add(name, value); + return this; +} +``` + +最后 将 AbstractBeanDefinition 转换为 BeanDefinitionHolder 并注册 registerBeanDefinition(holder, parserContext.getRegistry());这就和默认标签的注册是一样了。 + +至此,自定义标签的解析过程已经分析完成了。其实整个过程还是较为简单:首先会加载 handlers 文件,将其中内容进行一个解析,形成 这样的一个映射,然后根据获取的 namespaceUri 就可以得到相应的类路径,对其进行初始化等到相应的 Handler 对象,调用 `parse()` 方法,在该方法中根据标签的 localName 得到相应的 BeanDefinitionParser 实例对象,调用 `parse()` ,该方法定义在 AbstractBeanDefinitionParser 抽象类中,核心逻辑封装在其 `parseInternal()` 中,该方法返回一个 AbstractBeanDefinition 实例对象,其主要是在 AbstractSingleBeanDefinitionParser 中实现,对于自定义的 Parser 类,其需要实现 `getBeanClass()` 或者 `getBeanClassName()` 和 `doParse()`。最后将 AbstractBeanDefinition 转换为 BeanDefinitionHolder 并注册。 \ No newline at end of file diff --git a/docs/zsxq/introduce.md b/docs/zsxq/introduce.md index bae53f0..edc181d 100644 --- a/docs/zsxq/introduce.md +++ b/docs/zsxq/introduce.md @@ -181,9 +181,9 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, ## 怎么进入星球? -如果你下定决心要加入的话,可以直接扫下面这个二维码。星球定价**178**元,减去**50**元的优惠券,等于说只需要**128**元(**拒绝割韭菜**)的价格就可以加入,服务期一年,**每天只要三毛钱**(0.35元),相比培训班几万块的学费,非常值了,星球提供的服务**远超**门票价格了。 +如果你下定决心要加入的话,可以直接扫下面这个二维码。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元(**拒绝割韭菜**)的价格就可以加入,服务期一年,**每天只要三毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**,所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款**~ diff --git "a/docs/zsxq/question/VO\357\274\214 BO\357\274\214 PO\357\274\214 DO\357\274\214 DTO.md" "b/docs/zsxq/question/VO\357\274\214 BO\357\274\214 PO\357\274\214 DO\357\274\214 DTO.md" new file mode 100644 index 0000000..ab02966 --- /dev/null +++ "b/docs/zsxq/question/VO\357\274\214 BO\357\274\214 PO\357\274\214 DO\357\274\214 DTO.md" @@ -0,0 +1,31 @@ +最近[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)有小伙伴提出了关于性格测试的疑问: + +**球友提问**: + +大彬大佬,请教一个概念和具体应用上的问题, VO, BO, PO, DO, DTO 这些概念和具体应用是怎样的? + +--- + +**大彬的回答**: + +你好,这个问题要结合实际业务来讲更好理解。 + +比如现在有一个用户登录的业务。 有一张表user存用户数据,这个表里面有 id ,name ,password。首先说说PO,PO比较好理解,就是数据库中的记录,一个PO的数据结构对应着表的结构,表中的一条记录就是一个PO对象。 + +再来看看什么是DO。比如现在需要实现登录功能,那就只需要检查用户输入的 name 和 password 和数据库是否一致就可以了。 这种情况下,就用一个 User 对象来表示这个领域模型就可以了。我一般用 Domain Object ,也就是 DO 表示,对应下来就是 UserDO ( 统一用 Do 也可以) + + + +再假设现在有一个展示用户信息的业务,假想给一个客服(不期望把用户密码给他看到的那种情况) + +一般来说有两种做法: + +第一种,你可以在序列化为 json 字符串的时候,隐藏 password 的序列化来实现; + +第二种,你可以新建一个 VO ( View Object )对象,就叫 UserVO ,然后这里面就只有 id 和 name 两个属性即可; + + + +再假设,现在的场景不是直接展示到页面上,而是被一个业务系统调用,这个系统需要依赖登录的服务。 然后需要提供 sdk 给这个业务系统集成,那么这个时候就可以声明一个 DTO 对象,里面也只有 id 和 name 属性即可。 + +当然实际业务肯定不可能说只有 id ,name ,password 这么简单,这里只是举个例子方便理解。 \ No newline at end of file diff --git a/docs/zsxq/question/tech/service-expansion.md b/docs/zsxq/question/tech/service-expansion.md new file mode 100644 index 0000000..b4a5b1d --- /dev/null +++ b/docs/zsxq/question/tech/service-expansion.md @@ -0,0 +1,7 @@ +## 怎么做到增加机器能线性增加性能的? + +线性扩容有两种情况,一种是“无状态”服务,比如常见的 web 后端;另一种是“有状态服务”,比如 MySQL 数据库。 + +对于无状态服务,只要解决了服务发现功能,在服务启动之后能够把请求量引到新服务上,就可以做到线性扩容。常见的服务发现有 DNS 调度(通过域名解析到不同机器)、负载均衡调度(通过反向代理服务转发到不同机器)或者动态服务发现(比如 dubbo ),等等。 + +对于有状态服务,除了要解决服务发现问题之外,还要解决状态(数据)迁移问题,迁移又分两步:先是数据拆分,常见的用哈希把数据打散。然后是迁移,常见的办法有快照和日志两类迁移方式。也有一些数据库直接实现了开发无感知的状态迁移功能,比如 hbase。 \ No newline at end of file diff --git "a/docs/zsxq/question/\344\270\211\345\271\264\346\265\213\345\274\200\350\275\254\345\220\216\347\253\257.md" "b/docs/zsxq/question/\344\270\211\345\271\264\346\265\213\345\274\200\350\275\254\345\220\216\347\253\257.md" new file mode 100644 index 0000000..b8e25d6 --- /dev/null +++ "b/docs/zsxq/question/\344\270\211\345\271\264\346\265\213\345\274\200\350\275\254\345\220\216\347\253\257.md" @@ -0,0 +1,61 @@ +大彬,您好! + +首先介绍下基本情况:211本科,信息工程专业。 + +本人毕业快三年,第一年做了一些功能测试之类的活,后来,岗位调动去做低代码平台的开发,拖拉拽的那种,主要是写SQL,所以自学了MySQL,所以数据库这块看看面试题应该差不了太多。 + +从22年开始转Java后端,边工作边学习,看的都是B站黑马程序员的课程,目前已经学完了Javase,Javaweb,Spring,SSM,Springboot,Git,Maven等,也算比较系统的学了一遍。 + +Javase课程基本是跟着用手敲了一遍,后面的视频就只认真看了一遍来赶进度。 + +大学里课程选修了linux操作系统,算法与数据结构,计算机网络,不过也快忘得差不多了,除了记得一些linux常用命令,因为驻场有用到。 + +目前主要是在学redis和mq等中间件,八股文也在准备,但是算法没有刷过题。 + +公司客户主要是一些金融机构,因为积累了差不多两年的业务经验,所以本人也初步打算继续往金融科技这块发展(不一定,哪钱多去哪)。 + +后面打算跳槽甲方,但是目前除了相关技术学了一轮,还没有做过后端开发的项目。 + +请问: + +1、现在这个状态找测开怎么样,有什么需要重点准备的吗。虽然本人更想做开发。 + +2、现在提离职,估计要过一阵放我走。现在边工作边找,但是工作完没有太多时间学习,并且没有java项目开发的经历,应该只能投测开,长远看,本人更想做后端开发,还得准备刷下算法,背背八股。 + +因为我现在到手也就1w出头,所以如果三月底离职了还没找到理想的开发或测开工作,我是想直接找个比如黑马的瑞吉外卖项目,把已学的融汇贯通,认真做个把月冲击后端咋样? + +3、继续骑驴找马,直到找到才提离职。但是这样准备项目的时间不太够,做完一个项目估计起码四五月份,还不说背其它的八股和算法。 + +目前继续做这份工作我也没有太多成长,温水煮青蛙。 + +4、如何面试包装下涨薪最快呢,想尽可能的提升下月base,目标是2w+,但是又好像有不超过30%的限制?问上家月薪的时候可以虚报吗,或者怎么说? + +5、金融行业不少公司比较看重学历,请问咱做技术的,是否有必要花一年时间读个海外硕投资自己呢? + + + +------ + +大彬的回答: + +1、 遵从你的内心。如果确实想做开发,先按照开发的方向去努力吧,尽量投开发。后面又去做测开,可能你会有不甘心。当然3年经验转方向,可能会 + +2、已经提离职的话,工作上的内容 就推就推,马上就要走人了,身上就别挂那么多活了,特别是那种紧急上线,什么三月份之前上线,就是趁你走之前 赶紧开发完的那种, 都推一推,多给自己争取点自学的时间。 + +自己做一个项目是可以的,好好冲后端。 + +3、建议是尽量一边工作一边准备,如果时间确实挤不开,可以提离职,也是背水一战了。 + +4、入职的时候 上家公司的月薪 是要 银行流水截图发个hr的,不过有的人 是把 银行流水给p图了,改的薪资。 + +这么干只能说 有风险(可能被hr发现,终止offer),但确实不少人这么干的,然后也顺利入职了。 + +要不要这么搞 还需要你自己权衡一下。 + +你是 测试 转开发,double还是有难度。如果你面试表现足够好的话,要个double 也是有希望的,这个薪资 基本要去 北上深 这样的城市了。 + +5、自己攒够钱去 读个 香港 一年制 是可以的,提升一下自己,重新校招,学校认可度在大陆整体也还行,入学门槛相对足够低了。 + +不过花费不少,可能要50w起步, 这不是小数目,估计可以在老家买套不错的房子了。 + +不过如果你已经跳槽到一家不错的企业的话,我倾向于好好赚钱就好,毕竟 海外一年制学费不低,还要耽误 一年多的赚钱时间,后面能不能把这个钱赚回来,不太确定。 \ No newline at end of file diff --git "a/docs/zsxq/question/\350\257\273\345\215\232\350\277\230\346\230\257\346\211\276\345\267\245\344\275\234.md" "b/docs/zsxq/question/\350\257\273\345\215\232\350\277\230\346\230\257\346\211\276\345\267\245\344\275\234.md" new file mode 100644 index 0000000..f9375bb --- /dev/null +++ "b/docs/zsxq/question/\350\257\273\345\215\232\350\277\230\346\230\257\346\211\276\345\267\245\344\275\234.md" @@ -0,0 +1,22 @@ +前段时间[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)有小伙伴提出了关于读博还是找工作的疑问,跟大家分享一下: + +**学弟提问**: + +大彬老师好,我是参加今年24届秋招的**研二**学生,本科是川大数学专业,硕士在xxx研究所读研,计算机应用技术专业。在所里主要是研究无人机的路径规划和强化学习。 + +所里因为不给出去实习,所以我也**没有实习项目**,所里给的福利也就是最后可以留所在央企研究所工作,但是我不太想留,所以想出去找工作。自己有2篇无人机相关的论文,但是以后也不想去做无人机相关。只想做纯计算机相关的工作。后面就想转java,可以说除了高中的竞赛基础,完全没有别的基础,java就跟着机构的视频在学。但是感觉学的太慢了,从4月才开始算0基础学,秋招感觉是完全赶不上了,学了2个月去看看面经发现基本都答不上。所以很迷茫到底是继续java开发往后学下去,反正最后有个保底。还是不去找工作了,凭着2篇论文去找个博士读,博士毕业再出去找企业。希望能给个建议,谢谢! + +--- + +**大彬的回答**: + +你好,你现在研二了,过了两年的研究生生活,如果你适应这种探索性研究生活、**对科研有热情、有想法**想要去**探索**一些问题,而且没有太大生活压力,家里不需要你的经济支持,那建议你去申请读博。读完博士你的选择会更多更多。 + +如果你是处于**懵懂**状态,感觉硕士毕业找不到好工作就去读博,也没有科研热情,就这样盲目进行的话,这样只是**浪费时间**走一些弯路,之后也可能会后悔,建议你还是直接去找工作。因为读博也是有很大的挑战性的,需要克服焦虑和长时间的孤独,可能比你想象中的要困难一些。 + +另外如果想要找Java开发方向岗位的话,目前你的进度确实稍微慢了一些,秋招马上就要开始了,建议你抓重点去学,参考[星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)置顶帖的最新Java学习路线,加快学习节奏。 + + + + + diff --git "a/docs/zsxq/share/Java\345\274\200\345\217\221\347\232\20416\344\270\252\345\260\217\345\273\272\350\256\256.md" "b/docs/zsxq/share/Java\345\274\200\345\217\221\347\232\20416\344\270\252\345\260\217\345\273\272\350\256\256.md" new file mode 100644 index 0000000..66d63ea --- /dev/null +++ "b/docs/zsxq/share/Java\345\274\200\345\217\221\347\232\20416\344\270\252\345\260\217\345\273\272\350\256\256.md" @@ -0,0 +1,332 @@ +## 前言 + +开发过程中其实有很多小细节要去注意,只有不断去抠细节,写出精益求精的代码,从量变中收获质变。 + +技术的进步并非一蹴而就,而是通过无数次的量变,才能引发质的飞跃。我们始终坚信,只有对每一个细节保持敏锐的触觉,才能绽放出完美的技术之花。 + +从一行行代码中,我们品味到了追求卓越的滋味。每一个小小的优化,每一个微妙的改进,都是我们追求技艺的印记。我们知道,只有更多的关注细节,才能真正理解技术的本质,洞察其中的玄机。正是在对细节的把握中,我们得以成就更好的技术人生。 + +耐心看完,你一定会有所收获。 + +### 1. **代码风格一致性**: + +代码风格一致性可以提高代码的可读性和可维护性。例如,使用Java编程中普遍遵循的命名约定(驼峰命名法),使代码更易于理解。 + +```ini +ini复制代码// 不好的代码风格 +int g = 10; +String S = "Hello"; + +// 好的代码风格 +int count = 10; +String greeting = "Hello"; +``` + +### 2. **使用合适的数据结构和集合**: + +选择适当的数据结构和集合类可以改进代码的性能和可读性。例如,使用HashSet来存储唯一的元素。 + +```csharp +csharp复制代码// 不好的例子 - 使用ArrayList存储唯一元素 +List list = new ArrayList<>(); +list.add(1); +list.add(2); +list.add(1); // 重复元素 + +// 好的例子 - 使用HashSet存储唯一元素 +Set set = new HashSet<>(); +set.add(1); +set.add(2); +set.add(1); // 自动忽略重复元素 +``` + +### 3. **避免使用魔法数值**: + +使用常量或枚举来代替魔法数值可以提高代码的可维护性和易读性。 + +```ini +ini复制代码// 不好的例子 - 魔法数值硬编码 +if (status == 1) { + // 执行某些操作 +} + +// 好的例子 - 使用常量代替魔法数值 +final int STATUS_ACTIVE = 1; +if (status == STATUS_ACTIVE) { + // 执行某些操作 +} +``` + +### 4. **异常处理**: + +正确处理异常有助于代码的健壮性和容错性,避免不必要的try-catch块可以提高代码性能。 + +```php +php复制代码// 不好的例子 - 捕获所有异常,没有具体处理 +try { + // 一些可能抛出异常的操作 +} catch (Exception e) { + // 空的异常处理块 +} + +// 好的例子 - 捕获并处理特定异常,或向上抛出 +try { + // 一些可能抛出异常的操作 +} catch (FileNotFoundException e) { + // 处理文件未找到异常 +} catch (IOException e) { + // 处理其他IO异常 +} +``` + +### 5. **及时关闭资源**: + +使用完资源后,及时关闭它们可以避免资源泄漏,特别是对于文件流、数据库连接等资源。 + +更好的处理方式参见第16条,搭配`try-with-resources`食用最佳 + +```ini +ini复制代码// 不好的例子 - 未及时关闭数据库连接 +Connection conn = null; +Statement stmt = null; +try { + conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); + stmt = conn.createStatement(); + // 执行数据库查询操作 +} catch (SQLException e) { + e.printStackTrace(); +} finally { + // 数据库连接未关闭 +} + +// 好的例子 - 使用try-with-resources确保资源及时关闭,避免了数据库连接资源泄漏的问题 +try (Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD); + Statement stmt = conn.createStatement()) { + // 执行数据库查询操作 +} catch (SQLException e) { + e.printStackTrace(); +} +``` + +### 6. **避免过度使用全局变量**: + +过度使用全局变量容易引发意外的副作用和不可预测的结果,建议尽量避免使用全局变量。 + +```csharp +csharp复制代码// 不好的例子 - 过度使用全局变量 +public class MyClass { + private int count; + + // 省略其他代码 +} + +// 好的例子 - 使用局部变量或实例变量 +public class MyClass { + public void someMethod() { + int count = 0; + // 省略其他代码 + } +} +``` + +### 7. **避免不必要的对象创建**: + +避免在循环或频繁调用的方法中创建不必要的对象,可以使用对象池、StringBuilder等技术。 + +```arduino +arduino复制代码// 不好的例子 - 频繁调用方法创建不必要的对象 +public String formatData(int year, int month, int day) { + String formattedDate = String.format("%d-%02d-%02d", year, month, day); // 每次调用方法都会创建新的String对象 + return formattedDate; +} + +// 好的例子 - 避免频繁调用方法创建不必要的对象 +private static final String DATE_FORMAT = "%d-%02d-%02d"; +public String formatData(int year, int month, int day) { + return String.format(DATE_FORMAT, year, month, day); // 重复使用同一个String对象 +} +``` + +### 8. **避免使用不必要的装箱和拆箱**: + +避免频繁地在基本类型和其对应的包装类型之间进行转换,可以提高代码的性能和效率。 + +```dart +dart复制代码// 不好的例子 +Integer num = 10; // 不好的例子,自动装箱 +int result = num + 5; // 不好的例子,自动拆箱 + +// 好的例子 - 避免装箱和拆箱 +int num = 10; // 好的例子,使用基本类型 +int result = num + 5; // 好的例子,避免装箱和拆箱 +``` + +### 9. **使用foreach循环遍历集合**: + +使用foreach循环可以简化集合的遍历,并提高代码的可读性。 + +```ini +ini复制代码// 不好的例子 - 可读性不强,并且增加了方法调用的开销 +List names = Arrays.asList("Alice", "Bob", "Charlie"); +for (int i = 0; i < names.size(); i++) { + System.out.println(names.get(i)); // 不好的例子 +} + +// 好的例子 - 更加简洁,可读性更好,性能上也更优 +List names = Arrays.asList("Alice", "Bob", "Charlie"); +for (String name : names) { + System.out.println(name); // 好的例子 +} +``` + +### 10. **使用StringBuilder或StringBuffer拼接大量字符串**: + +在循环中拼接大量字符串时,使用StringBuilder或StringBuffer可以避免产生大量临时对象,提高性能。 + +```ini +ini复制代码// 不好的例子 - 每次循环都产生新的字符串对象 +String result = ""; +for (int i = 0; i < 1000; i++) { + result += "Number " + i + ", "; +} + +// 好的例子 - StringBuilder不会产生大量临时对象 +StringBuilder result = new StringBuilder(); +for (int i = 0; i < 1000; i++) { + result.append("Number ").append(i).append(", "); +} +``` + +### 11. **使用equals方法比较对象的内容**: + +老生常谈的问题,在比较对象的内容时,使用equals方法而不是==操作符,确保正确比较对象的内容。 + +```ini +ini复制代码// 不好的例子 +String name1 = "Alice"; +String name2 = new String("Alice"); +if (name1 == name2) { + // 不好的例子,使用==比较对象的引用,而非内容 +} + +// 好的例子 +String name1 = "Alice"; +String name2 = new String("Alice"); +if (name1.equals(name2)) { + // 好的例子,使用equals比较对象的内容 +} +``` + +### 12. **避免使用多个连续的空格或制表符**: + +多个连续的空格或制表符会使代码看起来杂乱不堪,建议使用合适的缩进和空格,保持代码的清晰可读。 + +```ini +ini复制代码// 不好的例子 +int a = 10; // 不好的例子,多个连续的空格和制表符 +String name = "John"; // 不好的例子,多个连续的空格和制表符 + +// 好的例子 +int a = 10; // 好的例子,适当的缩进和空格 +String name = "John"; // 好的例子,适当的缩进和空格 +``` + +### 13. **使用日志框架记录日志**: + +在代码中使用日志框架(如Log4j、SLF4J)来记录日志,而不是直接使用System.out.println(),可以更灵活地管理日志输出和级别。 + +```go +go复制代码// 不好的例子: +System.out.println("Error occurred"); // 不好的例子,直接输出日志到控制台 + +// 好的例子: +logger.error("Error occurred"); // 好的例子,使用日志框架记录日志 +``` + +### 14. **避免在循环中创建对象**: + +在循环中频繁地创建对象会导致大量的内存分配和垃圾回收,影响性能。尽量在循环外部创建对象,或使用对象池来复用对象,从而减少对象的创建和销毁开销。 + +```ini +ini复制代码// 不好的例子 - 在循环过程中频繁地创建和销毁对象,增加了垃圾回收的负担 +public List getNextWeekDates() { + List dates = new ArrayList<>(); + for (int i = 0; i < 7; i++) { + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.DAY_OF_MONTH, i + 1); + Date date = calendar.getTime(); // 在循环中频繁创建Calendar和Date对象 + dates.add(date); + } + return dates; +} + + +// 好的例子 - 在循环外部创建对象,减少内存分配和垃圾回收的开销 +public List getNextWeekDates() { + List dates = new ArrayList<>(); + Calendar calendar = Calendar.getInstance(); + Date date = new Date(); // 在循环外部创建Date对象 + for (int i = 0; i < 7; i++) { + calendar.add(Calendar.DAY_OF_MONTH, 1); + date.setTime(calendar.getTimeInMillis()); // 复用Date对象 + dates.add(date); + } + return dates; +} +``` + +### 15. **使用枚举替代常量**: + +这条其实和第3条一个道理,使用枚举可以更清晰地表示一组相关的常量,并且能够提供更多的类型安全性和功能性。 + +```arduino +arduino复制代码// 不好的例子 - 使用常量表示颜色 +public static final int RED = 1; +public static final int GREEN = 2; +public static final int BLUE = 3; + +// 好的例子 - 使用枚举表示颜色 +public enum Color { + RED, GREEN, BLUE +} +``` + +### 16. **使用try-with-resources语句**: + +在处理需要关闭的资源(如文件、数据库连接等)时,使用try-with-resources语句可以自动关闭资源,避免资源泄漏。 + +```java +java复制代码// 不好的例子 - 没有使用try-with-resources +FileReader reader = null; +try { + reader = new FileReader("file.txt"); + // 执行一些操作 +} catch (IOException e) { + // 处理异常 +} finally { + if (reader != null) { + try { + reader.close(); + } catch (IOException e) { + // 处理关闭异常 + } + } +} + +// 好的例子 - 使用try-with-resources自动关闭资源 +try (FileReader reader = new FileReader("file.txt")) { + // 执行一些操作 +} catch (IOException e) { + // 处理异常 +} +``` + + + + + +这16个小建议,希望对你有所帮助。 + + + +参考链接:https://juejin.cn/post/7261835383201726523 diff --git "a/docs/zsxq/share/\345\210\206\344\272\25310\344\270\252\351\253\230\347\272\247SQL\345\206\231\346\263\225.md" "b/docs/zsxq/share/\345\210\206\344\272\25310\344\270\252\351\253\230\347\272\247SQL\345\206\231\346\263\225.md" new file mode 100644 index 0000000..1812f1d --- /dev/null +++ "b/docs/zsxq/share/\345\210\206\344\272\25310\344\270\252\351\253\230\347\272\247SQL\345\206\231\346\263\225.md" @@ -0,0 +1,219 @@ +本文主要介绍不同业务所对应的 SQL 写法进行归纳总结而来。在这里分享给大家。 + +- 本文所讲述 sql 语法都是基于 MySQL 8.0 + +# 一、ORDER BY FIELD() 自定义排序逻辑 + +MySql 中的排序 ORDER BY 除了可以用 ASC 和 DESC,还可以通过 **ORDER BY FIELD(str,str1,...)** 自定义字符串/数字来实现排序。这里用 order_diy 表举例,结构以及表数据展示: + +![](http://img.topjavaer.cn/img/202309150751904.png) + + ORDER BY FIELD(str,str1,...) 自定义排序sql如下: + +```sql +SELECT * from order_diy ORDER BY FIELD(title,'九阴真经', +'降龙十八掌','九阴白骨爪','双手互博','桃花岛主', +'全真内功心法','蛤蟆功','销魂掌','灵白山少主'); +``` + +查询结果如下: + +![](http://img.topjavaer.cn/img/202309150752762.png) + + + + 如上,我们设置自定义排序字段为 title 字段,然后将我们自定义的排序结果跟在 title 后面。 + +# 二、CASE 表达式 + +**case when then else end**表达式功能非常强大可以帮助我们解决 `if elseif else` 这种问题,这里继续用 order_diy 表举例,假如我们想在 order_diy 表加一列 level 列,根据money 判断大于60就是高级,大于30就是中级,其余显示低级,sql 如下: + +```sql +SELECT *, +case when money > 60 then '高级' +when money > 30 then '中级' +else '低级' END level +from order_diy; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150752538.png) + +> 需要注意的就是 **case when then** 语句不匹配如果没有写 **else end** 会返回 null,影响数据展示。 + +# 三、EXISTS 用法 + +我猜大家在日常开发中,应该都对关键词 exists 用的比较少,估计使用 in 查询偏多。这里给大家介绍一下 exists 用法,引用官网文档: + +![](http://img.topjavaer.cn/img/202309150752021.png) + +可知 exists 后面是跟着一个子查询语句,它的作用是**根据主查询的数据,每一行都放到子查询中做条件验证,根据验证结果(TRUE 或者 FALSE),TRUE的话该行数据就会保留**,下面用 emp 表和 dept 表进行举例,表结构以及数据展示: + +![](http://img.topjavaer.cn/img/202309150753317.png) + +计入我们现在想找到 emp 表中 dept_name 与 dept表 中 dept_name 对应不上员工数据,sql 如下: + +```sql +SELECT * from emp e where exists ( +SELECT * from dept p where e.dept_id = p.dept_id +and e.dept_name != p.dept_name +) +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150753427.png) + +我们通过 exists 语法将外层 emp 表全部数据 放到子查询中与一一与 dept 表全部数据进行比较,只要有一行记录返回true。画个图展示主查询所有记录与子查询交互如下: + +![](http://img.topjavaer.cn/img/202309150753553.png) + +- 第一条记录与子查询比较时,全部返回 false,所以第一行不展示。 +- 第二行记录与子查询比较时,发现 `销售部门` 与 dept 表第二行 `销售部` 对应不上,返回 true,所以主查询该行记录会返回。 +- 第二行以后记录执行结果同第一条。 + +# 四、GROUP_CONCAT(expr) 组连接函数 + +**GROUP_CONCAT(expr)** 组连接函数可以返回分组后指定字段的字符串连接形式,并且可以指定排序逻辑,以及连接字符串,默认为英文逗号连接。这里继续用 order_diy 表举例:sql 如下: + +```sql +SELECT name, GROUP_CONCAT(title ORDER BY id desc SEPARATOR '-') +from order_diy GROUP BY name ORDER BY NULL; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150753797.png) + +如上我们通过 **GROUP_CONCAT(title ORDER BY id desc SEPARATOR '-')** 语句,指定分组连接 title 字段并按照 id 排序,设置连接字符串为 `-`。 + +# 五、自连接查询 + +自连接查询是 sql 语法里常用的一种写法,掌握了自连接的用法我们可以在 sql 层面轻松解决很多问题。这里用 tree 表举例,结构以及表数据展示: + +![](http://img.topjavaer.cn/img/202309150753344.png) + +tree 表中通过 pid 字段与 id 字段进行父子关联,假如现在有一个需求,我们想按照父子层级将 tree 表数据转换成 `一级职位 二级职位 三级职位` 三个列名进行展示,sql 如下: + +```sql +SELECT t1.job_name '一级职位', t2.job_name '二级职位', t3.job_name '三级职位' +from tree t1 join tree t2 on t1.id = t2.pid left join tree t3 on t2.id = t3.pid +where t1.pid = 0; +``` + +结果如下: + +![](http://img.topjavaer.cn/img/202309150753141.png) + +我们通过 **tree t1 join tree t2 on t1.id = t2.pid** 自连接展示 `一级职位 二级职位`,再用 **left join tree t3 on t2.id = t3.pid** 自连接展示 `二级职位 三级职位`,最后通过**where 条件 t1.pid = 0**过滤掉非一级职位的展示,完成这个需求。 + +# 六、更新 emp 表和 dept 表关联数据 + +这里继续使用上文提到的 emp 表和 dept 表,数据如下: + +![](http://img.topjavaer.cn/img/202309150754679.png) + +可以看到上述 emp 表中 jack 的部门名称与 dept 表实际不符合,现在我们想将 jack 的部门名称更新成 dept 表的正确数据,sql 如下: + +```sql +update emp, dept set emp.dept_name = dept.dept_name +where emp.dept_id = dept.dept_id; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754517.png) + +我们可以直接关联 emp 表和 dept 表并设置关联条件,然后更新 emp 表的 dept_name 为 dept 表的 dept_name。 + +# 七、ORDER BY 空值 NULL 排序 + +ORDER BY 字句中可以跟我们要排序的字段名称,但是当字段中存在 null 值时,会对我们的排序结果造成影响。我们可以通过 **ORDER BY IF(ISNULL(title), 1, 0)** 语法将 null 值转换成0或1,来达到将 null 值放到前面还是后面进行排序的效果。这里继续用 order_diy 表举例,sql 如下: + +```sql +SELECT * FROM order_diy ORDER BY IF(ISNULL(title), 0, 1), money; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754872.png) + +# 八、with rollup 分组统计数据的基础上再进行统计汇总 + +MySql 中可以使用 with rollup 在分组统计数据的基础上再进行统计汇总,即用来得到 group by 的汇总信息。这里继续用order_diy 表举例,sql 如下: + +```sql +SELECT name, SUM(money) as money +FROM order_diy GROUP BY name WITH ROLLUP; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754016.png) + +可以看到通过 **GROUP BY name WITH ROLLUP** 语句,查询结果最后一列显示了分组统计的汇总结果。但是 name 字段最后显示为 null,我们可以通过 `coalesce(val1, val2, ...)` 函数,这个函数会返回参数列表中的第一个非空参数。 + +```sql +SELECT coalesce(name, '总金额') name, SUM(money) as money +FROM order_diy GROUP BY name WITH ROLLUP; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754845.png) + +# 九、with as 提取临时表别名 + +with as 语法需要 MySql 8.0以上版本,它有一个别名叫做 CTE,官方对它的说明如下 + +> 公用表表达式 (CTE) 是一个命名的临时结果集,它存在于单个语句的范围内,稍后可以在该语句中引用,可以多次引用。 + +它的作用主要是提取子查询,方便后续共用,更多情况下会用在数据分析的场景上。 + +如果一整句查询中**多个子查询都需要使用同一个子查询**的结果,那么就可以用 with as,将共用的子查询提取出来,加个别名。后面查询语句可以直接用,对于大量复杂的SQL语句起到了很好的优化作用。这里继续用 order_diy 表举例,这里使用 with as 给出 sql 如下: + +```sql +-- 使用 with as +with t1 as (SELECT * from order_diy where money > 30), +t2 as (SELECT * from order_diy where money > 60) +SELECT * from t1 +where t1.id not in (SELECT id from t2) and t1.name = '周伯通'; +``` + +查询结果: + +![](http://img.topjavaer.cn/img/202309150754613.png) + +这个 sql 查询了 order_diy 表中 money 大于30且小于等于60之间并且 name 是周伯通的记录。可以看到使用 CTE 语法后,sql写起来会简洁很多。 + +# 10、存在就更新,不存在就插入 + +MySql 中通过**on duplicate key update**语法来实现存在就更新,不存在就插入的逻辑。插入或者更新时,它会根据表中主键索引或者唯一索引进行判断,如果主键索引或者唯一索引有冲突,就会执行**on duplicate key update**后面的赋值语句。 这里通过 news 表举例,表结构和说数据展示,其中 news_code 字段有唯一索引: + +![](http://img.topjavaer.cn/img/202309150754511.png) + +添加sql: + +```sql +-- 第一次执行添加语句 +INSERT INTO `news` (`news_title`, `news_auth`, `news_code`) +VALUES ('新闻3', '小花', 'wx-0003') +on duplicate key update news_title = '新闻3'; +-- 第二次执行修改语句 +INSERT INTO `news` (`news_title`, `news_auth`, `news_code`) +VALUES ('新闻4', '小花', 'wx-0003') +on duplicate key update news_title = '新闻4'; +``` + +结果如下: + +![](http://img.topjavaer.cn/img/202309150755137.png) + +# 总结 + +到这里,本文所分享的10个高级sql写法就全部介绍完了,希望对大家日常开发 sql 编写有所帮助。 + + + +参考链接:https://juejin.cn/post/7209625823580766264 \ No newline at end of file diff --git "a/docs/zsxq/share/\347\224\261\342\200\234 YYYY-MM-dd \342\200\235\345\274\225\345\217\221\347\232\204bug.md" "b/docs/zsxq/share/\347\224\261\342\200\234 YYYY-MM-dd \342\200\235\345\274\225\345\217\221\347\232\204bug.md" new file mode 100644 index 0000000..40a8c05 --- /dev/null +++ "b/docs/zsxq/share/\347\224\261\342\200\234 YYYY-MM-dd \342\200\235\345\274\225\345\217\221\347\232\204bug.md" @@ -0,0 +1,49 @@ +# 由“ YYYY-MM-dd ”引发的bug + +跟大家分享网上看到的一篇文章。 + +## 前言 + +在使用一些 App 的时候,竟然被我发现了一个应该是由于前端粗心而导致的 bug,在 2019.12.30 出发,结果 App 上显示的是 2020.12.30(吓得我以为我的订单下错了,此处是不是该把程序员拉去祭天了)。 + +鉴于可能会有程序员因此而被拉去祭天,而我以前学 Java 的时候就有留意过这个问题,所以我还是把这个问题拿出来说一下,希望能尽量避免这方面的粗心大意(毕竟这种问题也很难测出来)。 + +## 正文 + +```java +public class DateTest { + public static void main(String[] args) { + Calendar calendar = Calendar.getInstance(); + calendar.set(2019, Calendar.AUGUST, 31); + Date strDate = calendar.getTime(); + DateFormat formatUpperCase = new SimpleDateFormat("yyyy-MM-dd"); + System.out.println("2019-08-31 to yyyy-MM-dd: " + formatUpperCase.format(strDate)); + formatUpperCase = new SimpleDateFormat("YYYY-MM-dd"); + System.out.println("2019-08-31 to YYYY/MM/dd: " + formatUpperCase.format(strDate)); + } +} +``` + +我们来看下运行结果: + +``` +2019-08-31 to yyyy-MM-dd: 2019-08-31 +2019-08-31 to YYYY/MM/dd: 2019-08-31 +``` + +如果我们日期改成 12.31: + +``` +2019-12-31 to yyyy-MM-dd: 2019-12-31 +2019-12-31 to YYYY-MM-dd: 2020-12-31 +``` + +问题就出现了是吧,虽然是一个小小的细节,但是用户看了也会一脸懵,但是我们作为开发者,不能懵啊,赶紧文档查起来: + +![](http://img.topjavaer.cn/img/202309081043248.png) + +y:year-of-era;正正经经的年,即元旦过后; + +Y:week-based-year;只要本周跨年,那么这周就算入下一年;就比如说今年(2019-2020) 12.31 这一周是跨年的一周,而 12.31 是周二,那使用 YYYY 的话会显示 2020,使用 yyyy 则会从 1.1 才开始算是 2020。 + +这虽然是个很小的知识点,但是也有很多人栽到坑里,在此记录一下~ \ No newline at end of file From 0835f12b4838288504fc14cfe285573a1a2c7309 Mon Sep 17 00:00:00 2001 From: dabin <1713476357@qq.com> Date: Sun, 1 Oct 2023 11:53:30 +0800 Subject: [PATCH 3/8] =?UTF-8?q?spring=E6=BA=90=E7=A0=81=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06\347\232\204\347\224\237\346\210\220.md" | 634 +++++++++ ...13\347\211\251\344\273\213\347\273\215.md" | 389 ++++++ ...32\344\271\211\346\240\207\347\255\276.md" | 313 +++++ ...25\347\232\204\346\211\247\350\241\214.md" | 154 +++ ...231\250refresh\350\277\207\347\250\213.md" | 1164 +++++++++++++++++ ...04\345\210\235\345\247\213\345\214\226.md" | 321 +++++ ...26\345\242\236\345\274\272\345\231\250.md" | 803 ++++++++++++ ...40\345\267\245\344\275\234\357\274\237.md" | 39 + 8 files changed, 3817 insertions(+) create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 AOP\344\273\243\347\220\206\347\232\204\347\224\237\346\210\220.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224@Transactional\346\263\250\350\247\243\347\232\204\345\243\260\346\230\216\345\274\217\344\272\213\347\211\251\344\273\213\347\273\215.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\232\204\344\275\277\347\224\250\345\217\212AOP\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\233\256\346\240\207\346\226\271\346\263\225\345\222\214\345\242\236\345\274\272\346\226\271\346\263\225\347\232\204\346\211\247\350\241\214.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224ApplicationContext\345\256\271\345\231\250refresh\350\277\207\347\250\213.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean \347\232\204\345\210\235\345\247\213\345\214\226.md" create mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\345\210\233\345\273\272AOP\344\273\243\347\220\206\344\271\213\350\216\267\345\217\226\345\242\236\345\274\272\345\231\250.md" create mode 100644 "docs/zsxq/question/\346\200\216\346\240\267\345\207\206\345\244\207\346\211\215\350\203\275\346\211\276\345\210\260\344\270\200\344\273\275\345\256\236\344\271\240\345\267\245\344\275\234\357\274\237.md" diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 AOP\344\273\243\347\220\206\347\232\204\347\224\237\346\210\220.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 AOP\344\273\243\347\220\206\347\232\204\347\224\237\346\210\220.md" new file mode 100644 index 0000000..1d23eeb --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 AOP\344\273\243\347\220\206\347\232\204\347\224\237\346\210\220.md" @@ -0,0 +1,634 @@ +**正文** + +在获取了所有对应bean的增强后,便可以进行代理的创建了。回到AbstractAutoProxyCreator的wrapIfNecessary方法中,如下所示: + +```java + protected static final Object[] DO_NOT_PROXY = null; + + protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // Create proxy if we have advice. + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } + + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } +``` + +我们上一篇文章分析完了第16行,获取到了所有对应bean的增强器,并获取到了此目标bean所有匹配的 Advisor,接下来我们要从第17行开始分析,如果 specificInterceptors 不为空,则要为当前bean创建代理类,接下来我们来看创建代理类的方法 **createProxy:** + +```java +protected Object createProxy(Class beanClass, @Nullable String beanName, + @Nullable Object[] specificInterceptors, TargetSource targetSource) { + + if (this.beanFactory instanceof ConfigurableListableBeanFactory) { + AutoProxyUtils.exposeTargetClass((ConfigurableListableBeanFactory) this.beanFactory, beanName, beanClass); + } + + ProxyFactory proxyFactory = new ProxyFactory(); + // 获取当前类中相关属性 + proxyFactory.copyFrom(this); + + if (!proxyFactory.isProxyTargetClass()) { + // 决定对于给定的bean是否应该使用targetClass而不是他的接口代理, + // 检査 proxyTargetClass 设置以及 preserveTargetClass 属性 + if (shouldProxyTargetClass(beanClass, beanName)) { + proxyFactory.setProxyTargetClass(true); + } + else { + evaluateProxyInterfaces(beanClass, proxyFactory); + } + } + + Advisor[] advisors = buildAdvisors(beanName, specificInterceptors); + // 加入增强器 + proxyFactory.addAdvisors(advisors); + // 设置要代理的目标类 + proxyFactory.setTargetSource(targetSource); + // 定制代理 + customizeProxyFactory(proxyFactory); + // 用来控制代理工厂被配置之后,是否还允许修改通知。 + // 缺省值是false (即在代理被配置之后,不允许修改代理的配置)。 + proxyFactory.setFrozen(this.freezeProxy); + if (advisorsPreFiltered()) { + proxyFactory.setPreFiltered(true); + } + + //真正创建代理的方法 + return proxyFactory.getProxy(getProxyClassLoader()); +} + +@Override +public void setTargetSource(@Nullable TargetSource targetSource) { + this.targetSource = (targetSource != null ? targetSource : EMPTY_TARGET_SOURCE); +} + +public void addAdvisors(Collection advisors) { + if (isFrozen()) { + throw new AopConfigException("Cannot add advisor: Configuration is frozen."); + } + if (!CollectionUtils.isEmpty(advisors)) { + for (Advisor advisor : advisors) { + if (advisor instanceof IntroductionAdvisor) { + validateIntroductionAdvisor((IntroductionAdvisor) advisor); + } + Assert.notNull(advisor, "Advisor must not be null"); + this.advisors.add(advisor); + } + updateAdvisorArray(); + adviceChanged(); + } +} +``` + +从上面代码我们看到对于代理类的创建及处理spring是委托给了ProxyFactory处理的 + +## 创建代理 + +```java +public Object getProxy(@Nullable ClassLoader classLoader) { + return createAopProxy().getProxy(classLoader); +} +``` + +在上面的getProxy方法中createAopProxy方法,其实现是在DefaultAopProxyFactory中,这个方法的主要功能是,根据optimize、ProxyTargetClass等参数来决定生成Jdk动态代理,还是生成Cglib代理。我们进入到方法内: + +```java +protected final synchronized AopProxy createAopProxy() { + if (!this.active) { + activate(); + } + // 创建代理 + return getAopProxyFactory().createAopProxy(this); +} + +public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException { + if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { + Class targetClass = config.getTargetClass(); + if (targetClass == null) { + throw new AopConfigException("TargetSource cannot determine target class: " + + "Either an interface or a target is required for proxy creation."); + } + //手动设置创建Cglib代理类后,如果目标bean是一个接口,也要创建jdk代理类 + if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) { + return new JdkDynamicAopProxy(config); + } + //创建Cglib代理 + return new ObjenesisCglibAopProxy(config); + } + else { + //默认创建jdk代理 + return new JdkDynamicAopProxy(config); + } +} +``` + + + +我们知道对于Spring的代理是通过JDKProxy的实现和CglibProxy实现。Spring是如何选取的呢? + +从if的判断条件中可以看到3个方面影响这Spring的判断。 + +- optimize:用来控制通过CGLIB创建的代理是否使用激进的优化策略,除非完全了解AOP代理如何处理优化,否则不推荐用户使用这个设置。目前这个属性仅用于CGLIB 代理,对于JDK动态代理(缺省代理)无效。 +- proxyTargetClass:这个属性为true时,目标类本身被代理而不是目标类的接口。如果这个属性值被设为true,CGLIB代理将被创建,设置方式:**。** +- hasNoUserSuppliedProxylnterfaces:是否存在代理接口 + +下面是对JDK与Cglib方式的总结。 + +- **如果目标对象实现了接口,默认情况下会采用JDK的动态代理实现AOP。** +- **如果目标对象实现了接口,可以强制使用CGLIB实现AOP。** +- **如果目标对象没有实现了接口,必须采用CGLIB库,Spring会自动在JDK动态代理 和CGLIB之间转换。** + +如何强制使用CGLIB实现AOP? + +(1)添加 CGLIB 库,Spring_HOME/cglib/*.jar。 + +(2)在 Spring 配置文件中加人。 + +JDK动态代理和CGLIB字节码生成的区别? + +- JDK动态代理只能对实现了接口的类生成代理,而不能针对类。 +- CGLIB是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法,因为是继承,所以该类或方法最好不要声明成final。 + +## 获取代理 + +Spring的AOP实现其实也是用了**Proxy和InvocationHandler**这两个东西的。 + +我们再次来回顾一下使用JDK代理的方式,在整个创建过程中,对于InvocationHandler的创建是最为核心的,在自定义的InvocationHandler中需要重写3个函数。 + +- 构造函数,将代理的对象传入。 +- invoke方法,此方法中实现了 AOP增强的所有逻辑。 +- getProxy方法,此方法千篇一律,但是必不可少。 + +那么,我们看看Spring中的JDK代理实现是不是也是这么做的呢?我们来看看简化后的 JdkDynamicAopProxy 。 + +```java + final class JdkDynamicAopProxy implements AopProxy, InvocationHandler, Serializable { + + private final AdvisedSupport advised; + + public JdkDynamicAopProxy(AdvisedSupport config) throws AopConfigException { + Assert.notNull(config, "AdvisedSupport must not be null"); + if (config.getAdvisors().length == 0 && config.getTargetSource() == AdvisedSupport.EMPTY_TARGET_SOURCE) { + throw new AopConfigException("No advisors and no TargetSource specified"); + } + this.advised = config; + } + + + @Override + public Object getProxy() { + return getProxy(ClassUtils.getDefaultClassLoader()); + } + + @Override + public Object getProxy(@Nullable ClassLoader classLoader) { + if (logger.isTraceEnabled()) { + logger.trace("Creating JDK dynamic proxy: " + this.advised.getTargetSource()); + } + Class[] proxiedInterfaces = AopProxyUtils.completeProxiedInterfaces(this.advised, true); + findDefinedEqualsAndHashCodeMethods(proxiedInterfaces); + return Proxy.newProxyInstance(classLoader, proxiedInterfaces, this); + } + + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + Object oldProxy = null; + boolean setProxyContext = false; + + TargetSource targetSource = this.advised.targetSource; + Object target = null; + + try { + if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + // The target does not implement the equals(Object) method itself. + return equals(args[0]); + } + else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { + // The target does not implement the hashCode() method itself. + return hashCode(); + } + else if (method.getDeclaringClass() == DecoratingProxy.class) { + // There is only getDecoratedClass() declared -> dispatch to proxy config. + return AopProxyUtils.ultimateTargetClass(this.advised); + } + else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && + method.getDeclaringClass().isAssignableFrom(Advised.class)) { + // Service invocations on ProxyConfig with the proxy config... + return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); + } + + Object retVal; + + if (this.advised.exposeProxy) { + // Make invocation available if necessary. + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + + // Get as late as possible to minimize the time we "own" the target, + // in case it comes from a pool. + target = targetSource.getTarget(); + Class targetClass = (target != null ? target.getClass() : null); + + // Get the interception chain for this method. + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + + // Check whether we have any advice. If we don't, we can fallback on direct + // reflective invocation of the target, and avoid creating a MethodInvocation. + if (chain.isEmpty()) { + // We can skip creating a MethodInvocation: just invoke the target directly + // Note that the final invoker must be an InvokerInterceptor so we know it does + // nothing but a reflective operation on the target, and no hot swapping or fancy proxying. + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); + } + else { + // We need to create a method invocation... + MethodInvocation invocation = + new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); + // Proceed to the joinpoint through the interceptor chain. + retVal = invocation.proceed(); + } + + // Massage return value if necessary. + Class returnType = method.getReturnType(); + if (retVal != null && retVal == target && + returnType != Object.class && returnType.isInstance(proxy) && + !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { + // Special case: it returned "this" and the return type of the method + // is type-compatible. Note that we can't help if the target sets + // a reference to itself in another returned object. + retVal = proxy; + } + else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { + throw new AopInvocationException( + "Null return value from advice does not match primitive return type for: " + method); + } + return retVal; + } + finally { + if (target != null && !targetSource.isStatic()) { + // Must have come from TargetSource. + targetSource.releaseTarget(target); + } + if (setProxyContext) { + // Restore old proxy. + AopContext.setCurrentProxy(oldProxy); + } + } + } + + } +``` + + + +我们看到JdkDynamicAopProxy 也是和我们自定义的**InvocationHandler**一样,实现了InvocationHandler接口,并且提供了一个getProxy方法创建代理类,重写invoke方法。 + +我们重点看看代理类的调用。了解Jdk动态代理的话都会知道,在实现Jdk动态代理功能,要实现InvocationHandler接口的invoke方法(这个方法是一个回调方法)。 被代理类中的方法被调用时,实际上是调用的invoke方法,我们看一看这个方法的实现。 + +```java + @Override + @Nullable + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + MethodInvocation invocation; + Object oldProxy = null; + boolean setProxyContext = false; + + TargetSource targetSource = this.advised.targetSource; + Object target = null; + + try { + if (!this.equalsDefined && AopUtils.isEqualsMethod(method)) { + return equals(args[0]); + } + else if (!this.hashCodeDefined && AopUtils.isHashCodeMethod(method)) { + return hashCode(); + } + else if (method.getDeclaringClass() == DecoratingProxy.class) { + return AopProxyUtils.ultimateTargetClass(this.advised); + } + else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && + method.getDeclaringClass().isAssignableFrom(Advised.class)) { + return AopUtils.invokeJoinpointUsingReflection(this.advised, method, args); + } + + Object retVal; + if (this.advised.exposeProxy) { + // Make invocation available if necessary. + oldProxy = AopContext.setCurrentProxy(proxy); + setProxyContext = true; + } + + target = targetSource.getTarget(); + Class targetClass = (target != null ? target.getClass() : null); + + // 获取当前方法的拦截器链 + List chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass); + + if (chain.isEmpty()) { + // 如果没有发现任何拦截器那么直接调用切点方法 + Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); + } + else { + // We need to create a method invocation... + // 将拦截器封装在ReflectiveMethodInvocation, + // 以便于使用其proceed进行链接表用拦截器 + invocation = new ReflectiveMethodInvocation(proxy, target, method, args, targetClass, chain); + // Proceed to the joinpoint through the interceptor chain. + // 执行拦截器链 + retVal = invocation.proceed(); + } + + Class returnType = method.getReturnType(); + // 返回结果 + if (retVal != null && retVal == target && + returnType != Object.class && returnType.isInstance(proxy) && + !RawTargetAccess.class.isAssignableFrom(method.getDeclaringClass())) { + retVal = proxy; + } + else if (retVal == null && returnType != Void.TYPE && returnType.isPrimitive()) { + throw new AopInvocationException( + "Null return value from advice does not match primitive return type for: " + method); + } + return retVal; + } + finally { + if (target != null && !targetSource.isStatic()) { + // Must have come from TargetSource. + targetSource.releaseTarget(target); + } + if (setProxyContext) { + // Restore old proxy. + AopContext.setCurrentProxy(oldProxy); + } + } + } +``` + +我们先来看看第37行,获取目标bean中目标method中的增强器,并将增强器封装成拦截器链 + +```java + @Override + public List getInterceptorsAndDynamicInterceptionAdvice( + Advised config, Method method, @Nullable Class targetClass) { + + // This is somewhat tricky... We have to process introductions first, + // but we need to preserve order in the ultimate list. + AdvisorAdapterRegistry registry = GlobalAdvisorAdapterRegistry.getInstance(); + Advisor[] advisors = config.getAdvisors(); + List interceptorList = new ArrayList<>(advisors.length); + Class actualClass = (targetClass != null ? targetClass : method.getDeclaringClass()); + Boolean hasIntroductions = null; + + //获取bean中的所有增强器 + for (Advisor advisor : advisors) { + if (advisor instanceof PointcutAdvisor) { + // Add it conditionally. + PointcutAdvisor pointcutAdvisor = (PointcutAdvisor) advisor; + if (config.isPreFiltered() || pointcutAdvisor.getPointcut().getClassFilter().matches(actualClass)) { + MethodMatcher mm = pointcutAdvisor.getPointcut().getMethodMatcher(); + boolean match; + if (mm instanceof IntroductionAwareMethodMatcher) { + if (hasIntroductions == null) { + hasIntroductions = hasMatchingIntroductions(advisors, actualClass); + } + match = ((IntroductionAwareMethodMatcher) mm).matches(method, actualClass, hasIntroductions); + } + else { + //根据增强器中的Pointcut判断增强器是否能匹配当前类中的method + //我们要知道目标Bean中并不是所有的方法都需要增强,也有一些普通方法 + match = mm.matches(method, actualClass); + } + if (match) { + //如果能匹配,就将advisor封装成MethodInterceptor加入到interceptorList中 + MethodInterceptor[] interceptors = registry.getInterceptors(advisor); + if (mm.isRuntime()) { + // Creating a new object instance in the getInterceptors() method + // isn't a problem as we normally cache created chains. + for (MethodInterceptor interceptor : interceptors) { + interceptorList.add(new InterceptorAndDynamicMethodMatcher(interceptor, mm)); + } + } + else { + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + } + } + else if (advisor instanceof IntroductionAdvisor) { + IntroductionAdvisor ia = (IntroductionAdvisor) advisor; + if (config.isPreFiltered() || ia.getClassFilter().matches(actualClass)) { + Interceptor[] interceptors = registry.getInterceptors(advisor); + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + else { + Interceptor[] interceptors = registry.getInterceptors(advisor); + interceptorList.addAll(Arrays.asList(interceptors)); + } + } + + return interceptorList; + } +``` + + + +我们知道目标Bean中并不是所有的方法都需要增强,所以我们要遍历所有的 Advisor ,根据**Pointcut判断增强器是否能匹配当前类中的method,取出能匹配的增强器,封装成** **MethodInterceptor,加入到拦截器链中**,我们来看看第34行 + +```java +@Override +public MethodInterceptor[] getInterceptors(Advisor advisor) throws UnknownAdviceTypeException { + List interceptors = new ArrayList<>(3); + Advice advice = advisor.getAdvice(); + if (advice instanceof MethodInterceptor) { + interceptors.add((MethodInterceptor) advice); + } + //这里遍历三个适配器,将对应的advisor转化成Interceptor + //这三个适配器分别是MethodBeforeAdviceAdapter,AfterReturningAdviceAdapter,ThrowsAdviceAdapter + for (AdvisorAdapter adapter : this.adapters) { + if (adapter.supportsAdvice(advice)) { + interceptors.add(adapter.getInterceptor(advisor)); + } + } + if (interceptors.isEmpty()) { + throw new UnknownAdviceTypeException(advisor.getAdvice()); + } + return interceptors.toArray(new MethodInterceptor[0]); +} + +private final List adapters = new ArrayList<>(3); + +/** + * Create a new DefaultAdvisorAdapterRegistry, registering well-known adapters. + */ +public DefaultAdvisorAdapterRegistry() { + registerAdvisorAdapter(new MethodBeforeAdviceAdapter()); + registerAdvisorAdapter(new AfterReturningAdviceAdapter()); + registerAdvisorAdapter(new ThrowsAdviceAdapter()); +} + +@Override +public void registerAdvisorAdapter(AdvisorAdapter adapter) { + this.adapters.add(adapter); +} +``` + +由于Spring中涉及过多的拦截器,增强器,增强方法等方式来对逻辑进行增强,在上一篇文章中我们知道创建的几个增强器,AspectJAroundAdvice、AspectJAfterAdvice、AspectJAfterThrowingAdvice这几个增强器都实现了 MethodInterceptor 接口,AspectJMethodBeforeAdvice 和AspectJAfterReturningAdvice 并没有实现 MethodInterceptor 接口,因此AspectJMethodBeforeAdvice 和AspectJAfterReturningAdvice不能满足MethodInterceptor 接口中的invoke方法,所以这里使用适配器模式将AspectJMethodBeforeAdvice 和AspectJAfterReturningAdvice转化成能满足需求的MethodInterceptor实现类。 + +遍历adapters,通过adapter.supportsAdvice(advice)找到advice对应的适配器,adapter.getInterceptor(advisor)将advisor转化成对应的interceptor + +接下来我们看看MethodBeforeAdviceAdapter和AfterReturningAdviceAdapter这两个适配器,这两个适配器是将MethodBeforeAdvice和AfterReturningAdvice适配成对应的Interceptor + +**MethodBeforeAdviceAdapter** + +```java +class MethodBeforeAdviceAdapter implements AdvisorAdapter, Serializable { + @Override + public boolean supportsAdvice(Advice advice) { + //判断是否是MethodBeforeAdvice类型的advice + return (advice instanceof MethodBeforeAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + MethodBeforeAdvice advice = (MethodBeforeAdvice) advisor.getAdvice(); + //将advice封装成MethodBeforeAdviceInterceptor + return new MethodBeforeAdviceInterceptor(advice); + } +} + +//MethodBeforeAdviceInterceptor实现了MethodInterceptor接口,实现了invoke方法,并将advice作为属性 +public class MethodBeforeAdviceInterceptor implements MethodInterceptor, BeforeAdvice, Serializable { + + private final MethodBeforeAdvice advice; + + public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + return mi.proceed(); + } + +} +``` + +**AfterReturningAdviceAdapter** + +```java +class AfterReturningAdviceAdapter implements AdvisorAdapter, Serializable { + @Override + public boolean supportsAdvice(Advice advice) { + return (advice instanceof AfterReturningAdvice); + } + + @Override + public MethodInterceptor getInterceptor(Advisor advisor) { + AfterReturningAdvice advice = (AfterReturningAdvice) advisor.getAdvice(); + return new AfterReturningAdviceInterceptor(advice); + } +} + +public class AfterReturningAdviceInterceptor implements MethodInterceptor, AfterAdvice, Serializable { + + private final AfterReturningAdvice advice; + + public AfterReturningAdviceInterceptor(AfterReturningAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + Object retVal = mi.proceed(); + this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis()); + return retVal; + } + +} +``` + +至此我们获取到了一个拦截器链,链中包括AspectJAroundAdvice、AspectJAfterAdvice、AspectJAfterThrowingAdvice、MethodBeforeAdviceInterceptor、AfterReturningAdviceInterceptor + +接下来 ReflectiveMethodInvocation 类进行了链的封装,而在ReflectiveMethodInvocation类的proceed方法中实现了拦截器的逐一调用,那么我们继续来探究,在proceed方法中是怎么实现前置增强在目标方法前调用后置增强在目标方法后调用的逻辑呢? + +我们先来看看ReflectiveMethodInvocation的构造器,只是简单的进行属性赋值,不过我们要注意有一个特殊的变量currentInterceptorIndex,这个变量代表执行Interceptor的下标,从-1开始,Interceptor执行一个,先++this.currentInterceptorIndex + +```java +protected ReflectiveMethodInvocation( + Object proxy, @Nullable Object target, Method method, @Nullable Object[] arguments, + @Nullable Class targetClass, List interceptorsAndDynamicMethodMatchers) { + + this.proxy = proxy; + this.target = target; + this.targetClass = targetClass; + this.method = BridgeMethodResolver.findBridgedMethod(method); + this.arguments = AopProxyUtils.adaptArgumentsIfNecessary(method, arguments); + this.interceptorsAndDynamicMethodMatchers = interceptorsAndDynamicMethodMatchers; +} + +private int currentInterceptorIndex = -1; +``` + +下面是ReflectiveMethodInvocation类Proceed方法: + +```java +public Object proceed() throws Throwable { + // 首先,判断是不是所有的interceptor(也可以想像成advisor)都被执行完了。 + // 判断的方法是看currentInterceptorIndex这个变量的值,增加到Interceptor总个数这个数值没有, + // 如果到了,就执行被代理方法(invokeJoinpoint());如果没到,就继续执行Interceptor。 + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { + return invokeJoinpoint(); + } + + // 如果Interceptor没有被全部执行完,就取出要执行的Interceptor,并执行。 + // currentInterceptorIndex先自增 + Object interceptorOrInterceptionAdvice =this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + // 如果Interceptor是PointCut类型 + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { + InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; + // 如果当前方法符合Interceptor的PointCut限制,就执行Interceptor + if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { +    // 这里将this当变量传进去,这是非常重要的一点 + return dm.interceptor.invoke(this); + } + // 如果不符合,就跳过当前Interceptor,执行下一个Interceptor + else { + return proceed(); + } + } + // 如果Interceptor不是PointCut类型,就直接执行Interceptor里面的增强。 + else { + return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); + } +} +``` + + + +下一篇文章讲解目标方法和增强方法是如何执行的。 \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224@Transactional\346\263\250\350\247\243\347\232\204\345\243\260\346\230\216\345\274\217\344\272\213\347\211\251\344\273\213\347\273\215.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224@Transactional\346\263\250\350\247\243\347\232\204\345\243\260\346\230\216\345\274\217\344\272\213\347\211\251\344\273\213\347\273\215.md" new file mode 100644 index 0000000..748cbc0 --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224@Transactional\346\263\250\350\247\243\347\232\204\345\243\260\346\230\216\345\274\217\344\272\213\347\211\251\344\273\213\347\273\215.md" @@ -0,0 +1,389 @@ +**正文** + +面的几个章节已经分析了spring基于`@AspectJ`的源码,那么接下来我们分析一下Aop的另一个重要功能,事物管理。 + +## 事务的介绍 + +### 1.数据库事物特性 + +- 原子性 + 多个数据库操作是不可分割的,只有所有的操作都执行成功,事物才能被提交;只要有一个操作执行失败,那么所有的操作都要回滚,数据库状态必须回复到操作之前的状态 +- 一致性 + 事物操作成功后,数据库的状态和业务规则必须一致。例如:从A账户转账100元到B账户,无论数据库操作成功失败,A和B两个账户的存款总额是不变的。 +- 隔离性 + 当并发操作时,不同的数据库事物之间不会相互干扰(当然这个事物隔离级别也是有关系的) +- 持久性 + 事物提交成功之后,事物中的所有数据都必须持久化到数据库中。即使事物提交之后数据库立刻崩溃,也需要保证数据能能够被恢复。 + +### 2.事物隔离级别 + +当数据库并发操作时,可能会引起脏读、不可重复读、幻读、第一类丢失更新、第二类更新丢失等现象。 + +- 脏读 + 事物A读取事物B尚未提交的更改数据,并做了修改;此时如果事物B回滚,那么事物A读取到的数据是无效的,此时就发生了脏读。 +- 不可重复读 + 一个事务执行相同的查询两次或两次以上,每次都得到不同的数据。如:A事物下查询账户余额,此时恰巧B事物给账户里转账100元,A事物再次查询账户余额,那么A事物的两次查询结果是不一致的。 +- 幻读 + A事物读取B事物提交的新增数据,此时A事物将出现幻读现象。幻读与不可重复读容易混淆,如何区分呢?幻读是读取到了其他事物提交的新数据,不可重复读是读取到了已经提交事物的更改数据(修改或删除) + +对于以上问题,可以有多个解决方案,设置数据库事物隔离级别就是其中的一种,数据库事物隔离级别分为四个等级,通过一个表格描述其作用。 + +| 隔离级别 | 脏读 | 不可重复读 | 幻象读 | +| ---------------- | ------ | ---------- | ------ | +| READ UNCOMMITTED | 允许 | 允许 | 允许 | +| READ COMMITTED | 脏读 | 允许 | 允许 | +| REPEATABLE READ | 不允许 | 不允许 | 允许 | +| SERIALIZABLE | 不允许 | 不允许 | 不允许 | + +### 3.Spring事物支持核心接口 + +![](http://img.topjavaer.cn/img/202310011145573.png) + +- TransactionDefinition-->定义与spring兼容的事务属性的接口 + +```java +public interface TransactionDefinition { + // 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中。 + int PROPAGATION_REQUIRED = 0; + // 支持当前事物,如果当前没有事物,则以非事物方式执行。 + int PROPAGATION_SUPPORTS = 1; + // 使用当前事物,如果当前没有事物,则抛出异常。 + int PROPAGATION_MANDATORY = 2; + // 新建事物,如果当前已经存在事物,则挂起当前事物。 + int PROPAGATION_REQUIRES_NEW = 3; + // 以非事物方式执行,如果当前存在事物,则挂起当前事物。 + int PROPAGATION_NOT_SUPPORTED = 4; + // 以非事物方式执行,如果当前存在事物,则抛出异常。 + int PROPAGATION_NEVER = 5; + // 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同 + int PROPAGATION_NESTED = 6; + // 使用后端数据库默认的隔离级别。 + int ISOLATION_DEFAULT = -1; + // READ_UNCOMMITTED 隔离级别 + int ISOLATION_READ_UNCOMMITTED = Connection.TRANSACTION_READ_UNCOMMITTED; + // READ_COMMITTED 隔离级别 + int ISOLATION_READ_COMMITTED = Connection.TRANSACTION_READ_COMMITTED; + // REPEATABLE_READ 隔离级别 + int ISOLATION_REPEATABLE_READ = Connection.TRANSACTION_REPEATABLE_READ; + // SERIALIZABLE 隔离级别 + int ISOLATION_SERIALIZABLE = Connection.TRANSACTION_SERIALIZABLE; + // 默认超时时间 + int TIMEOUT_DEFAULT = -1; + // 获取事物传播特性 + int getPropagationBehavior(); + // 获取事物隔离级别 + int getIsolationLevel(); + // 获取事物超时时间 + int getTimeout(); + // 判断事物是否可读 + boolean isReadOnly(); + // 获取事物名称 + @Nullable + String getName(); +} +``` + +1. Spring事物传播特性表: + +| 传播特性名称 | 说明 | +| ------------------------- | ------------------------------------------------------------ | +| PROPAGATION_REQUIRED | 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中 | +| PROPAGATION_SUPPORTS | 支持当前事物,如果当前没有事物,则以非事物方式执行 | +| PROPAGATION_MANDATORY | 使用当前事物,如果当前没有事物,则抛出异常 | +| PROPAGATION_REQUIRES_NEW | 新建事物,如果当前已经存在事物,则挂起当前事物 | +| PROPAGATION_NOT_SUPPORTED | 以非事物方式执行,如果当前存在事物,则挂起当前事物 | +| PROPAGATION_NEVER | 以非事物方式执行,如果当前存在事物,则抛出异常 | +| PROPAGATION_NESTED | 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同 | + +1. Spring事物隔离级别表: + +| 事务隔离级别 | 脏读 | 不可重复读 | 幻读 | +| ---------------------------- | ---- | ---------- | ---- | +| 读未提交(read-uncommitted) | 是 | 是 | 是 | +| 不可重复读(read-committed) | 否 | 是 | 是 | +| 可重复读(repeatable-read) | 否 | 否 | 是 | +| 串行化(serializable) | 否 | 否 | 否 | + +MySQL默认的事务隔离级别为 **可重复读repeatable-read** + +  3.PlatformTransactionManager-->Spring事务基础结构中的中心接口 + +```java +public interface PlatformTransactionManager { + // 根据指定的传播行为,返回当前活动的事务或创建新事务。 + TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException; + // 就给定事务的状态提交给定事务。 + void commit(TransactionStatus status) throws TransactionException; + // 执行给定事务的回滚。 + void rollback(TransactionStatus status) throws TransactionException; +} +``` + +Spring将事物管理委托给底层的持久化框架来完成,因此,Spring为不同的持久化框架提供了不同的PlatformTransactionManager接口实现。列举几个Spring自带的事物管理器: + +| 事物管理器 | 说明 | +| ------------------------------------------------------------ | ------------------------------------------------------------ | +| org.springframework.jdbc.datasource.DataSourceTransactionManager | 提供对单个javax.sql.DataSource事务管理,用于Spring JDBC抽象框架、iBATIS或MyBatis框架的事务管理 | +| org.springframework.orm.jpa.JpaTransactionManager | 提供对单个javax.persistence.EntityManagerFactory事务支持,用于集成JPA实现框架时的事务管理 | +| org.springframework.transaction.jta.JtaTransactionManager | 提供对分布式事务管理的支持,并将事务管理委托给Java EE应用服务器事务管理器 | + + + +- TransactionStatus-->事物状态描述 + +1. TransactionStatus接口 + +```java +public interface TransactionStatus extends SavepointManager, Flushable { + // 返回当前事务是否为新事务(否则将参与到现有事务中,或者可能一开始就不在实际事务中运行) + boolean isNewTransaction(); + // 返回该事务是否在内部携带保存点,也就是说,已经创建为基于保存点的嵌套事务。 + boolean hasSavepoint(); + // 设置事务仅回滚。 + void setRollbackOnly(); + // 返回事务是否已标记为仅回滚 + boolean isRollbackOnly(); + // 将会话刷新到数据存储区 + @Override + void flush(); + // 返回事物是否已经完成,无论提交或者回滚。 + boolean isCompleted(); +} +``` + +1. SavepointManager接口 + +```java +public interface SavepointManager { + // 创建一个新的保存点。 + Object createSavepoint() throws TransactionException; + // 回滚到给定的保存点。 + // 注意:调用此方法回滚到给定的保存点之后,不会自动释放保存点, + // 可以通过调用releaseSavepoint方法释放保存点。 + void rollbackToSavepoint(Object savepoint) throws TransactionException; + // 显式释放给定的保存点。(大多数事务管理器将在事务完成时自动释放保存点) + void releaseSavepoint(Object savepoint) throws TransactionException; +} +``` + + + +## Spring编程式事物 + +- 表 + +```sql +CREATE TABLE `account` ( + `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '自增主键', + `balance` int(11) DEFAULT NULL COMMENT '账户余额', + PRIMARY KEY (`id`) +) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8 COMMENT='--账户表' +``` + +- 实现 + +```java + import org.apache.commons.dbcp.BasicDataSource; + import org.springframework.dao.DataAccessException; + import org.springframework.jdbc.core.JdbcTemplate; + import org.springframework.jdbc.datasource.DataSourceTransactionManager; + import org.springframework.transaction.TransactionDefinition; + import org.springframework.transaction.TransactionStatus; + import org.springframework.transaction.support.DefaultTransactionDefinition; + + import javax.sql.DataSource; + + public class MyTransaction { + + private JdbcTemplate jdbcTemplate; + private DataSourceTransactionManager txManager; + private DefaultTransactionDefinition txDefinition; + private String insert_sql = "insert into account (balance) values ('100')"; + + public void save() { + + // 1、初始化jdbcTemplate + DataSource dataSource = getDataSource(); + jdbcTemplate = new JdbcTemplate(dataSource); + + // 2、创建物管理器 + txManager = new DataSourceTransactionManager(); + txManager.setDataSource(dataSource); + + // 3、定义事物属性 + txDefinition = new DefaultTransactionDefinition(); + txDefinition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED); + + // 3、开启事物 + TransactionStatus txStatus = txManager.getTransaction(txDefinition); + + // 4、执行业务逻辑 + try { + jdbcTemplate.execute(insert_sql); + //int i = 1/0; + jdbcTemplate.execute(insert_sql); + txManager.commit(txStatus); + } catch (DataAccessException e) { + txManager.rollback(txStatus); + e.printStackTrace(); + } + + } + + public DataSource getDataSource() { + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("com.mysql.jdbc.Driver"); + dataSource.setUrl("jdbc:mysql://localhost:3306/my_test?useSSL=false&useUnicode=true&characterEncoding=UTF-8"); + dataSource.setUsername("root"); + dataSource.setPassword("dabin1991@"); + return dataSource; + } + + } +``` + + + +- 测试类及结果 + +```java +public class MyTest { + @Test + public void test1() { + MyTransaction myTransaction = new MyTransaction(); + myTransaction.save(); + } +} +``` + +运行测试类,在抛出异常之后手动回滚事物,所以数据库表中不会增加记录。 + + + +## 基于@Transactional注解的声明式事物 + +其底层建立在 AOP 的基础之上,对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。通过声明式事物,无需在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过等价的基于标注的方式),便可以将事务规则应用到业务逻辑中。 + +- 接口 + +```java +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +@Transactional(propagation = Propagation.REQUIRED) +public interface AccountServiceImp { + void save() throws RuntimeException; +} +``` + +- 实现 + +```java +import org.springframework.jdbc.core.JdbcTemplate; + +public class AccountServiceImpl implements AccountServiceImp { + + private JdbcTemplate jdbcTemplate; + + private static String insert_sql = "insert into account(balance) values (100)"; + + + @Override + public void save() throws RuntimeException { + System.out.println("==开始执行sql"); + jdbcTemplate.update(insert_sql); + System.out.println("==结束执行sql"); + + System.out.println("==准备抛出异常"); + throw new RuntimeException("==手动抛出一个异常"); + } + + public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } +} +``` + +- 配置文件 + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +- 测试 + +```java +import org.junit.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.ClassPathXmlApplicationContext; + +public class MyTest { + + @Test + public void test1() { + // 基于tx标签的声明式事物 + ApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml"); + AccountServiceImp studentService = ctx.getBean("accountService", AccountServiceImp.class); + studentService.save(); + } +} +``` + +- 测试 + +```java +==开始执行sql +==结束执行sql +==准备抛出异常 + +java.lang.RuntimeException: ==手动抛出一个异常 + + at com.lyc.cn.v2.day09.AccountServiceImpl.save(AccountServiceImpl.java:24) + at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) + at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) + at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) + at java.lang.reflect.Method.invoke(Method.java:498) +``` + +测试方法中手动抛出了一个异常,Spring会自动回滚事物,查看数据库可以看到并没有新增记录。 + +注意:默认情况下Spring中的事务处理只对RuntimeException方法进行回滚,所以,如果此处将RuntimeException替换成普通的Exception不会产生回滚效果。 + + + +下一篇我们分析基于@Transactional注解的声明式事物的的源码实现。 \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\232\204\344\275\277\347\224\250\345\217\212AOP\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\232\204\344\275\277\347\224\250\345\217\212AOP\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276.md" new file mode 100644 index 0000000..209b7be --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\232\204\344\275\277\347\224\250\345\217\212AOP\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276.md" @@ -0,0 +1,313 @@ +**正文** + +我们知道在面向对象OOP编程存在一些弊端,当需要为多个不具有继承关系的对象引入同一个公共行为时,例如日志,安全检测等,我们只有在每个对象里引入公共行为,这样程序中就产生了大量的重复代码,所以有了面向对象编程的补充,面向切面编程(AOP),AOP所关注的方向是横向的,不同于OOP的纵向。接下来我们就详细分析下spring中的AOP。首先我们从动态AOP的使用开始。 + +## AOP的使用 + +在开始前,先引入Aspect。 + +```xml + + + org.aspectj + aspectjweaver + ${aspectj.version} + +``` + +### 创建用于拦截的bean + +```java +public class TestBean { + private String message = "test bean"; + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public void test(){ + System.out.println(this.message); + } +} +``` + + + +### 创建Advisor + +Spring中摒弃了最原始的繁杂配置方式而采用@AspectJ注解对POJO进行标注,使AOP的工作大大简化,例如,在AspectJTest类中,我们要做的就是在所有类的test方法执行前在控制台beforeTest。而在所有类的test方法执行后打印afterTest,同时又使用环绕的方式在所有类的方法执行前后在此分别打印before1和after1,以下是AspectJTest的代码: + +```java +@Aspect +public class AspectJTest { + @Pointcut("execution(* *.test(..))") + public void test(){ + } + + @Before("test()") + public void beforeTest(){ + System.out.println("beforeTest"); + } + + @Around("test()") + public Object aroundTest(ProceedingJoinPoint p){ + System.out.println("around.....before"); + Object o = null; + try{ + o = p.proceed(); + }catch(Throwable e){ + e.printStackTrace(); + } + System.out.println("around.....after"); + return o; + } + + @After("test()") + public void afterTest() + { + System.out.println("afterTest"); + } + } +``` + + + +### 创建配置文件 + +要在Spring中开启AOP功能,,还需要在配置文件中作如下声明: + +```xml + + + + + + + + + +``` + + + +### 测试 + +```java +public class Test { + public static void main(String[] args) { + ApplicationContext bf = new ClassPathXmlApplicationContext("aspectTest.xml"); + TestBean bean = (TestBean)bf.getBean("test"); + bean.test(); + } +} +``` + + + +执行后输出如下: + +![](http://img.topjavaer.cn/img/202309241751697.png) + + + +Spring实现了对所有类的test方法进行增强,使辅助功能可以独立于核心业务之外,方便与程序的扩展和解耦。 + +那么,Spring是如何实现AOP的呢?首先我们知道,SPring是否支持注解的AOP是由一个配置文件控制的,也就是``,当在配置文件中声明了这句配置的时候,Spring就会支持注解的AOP,那么我们的分析就从这句注解开始。 + + + +## AOP自定义标签 + +之前讲过Spring中的自定义注解,如果声明了自定义的注解,那么就一定会在程序中的某个地方注册了对应的解析器。我们搜索 **aspectj-autoproxy** 这个代码,尝试找到注册的地方,全局搜索后我们发现了在org.springframework.aop.config包下的AopNamespaceHandler中对应着这样一段函数: + +```java +@Override +public void init() { + // In 2.0 XSD as well as in 2.1 XSD. + registerBeanDefinitionParser("config", new ConfigBeanDefinitionParser()); + registerBeanDefinitionParser("aspectj-autoproxy", new AspectJAutoProxyBeanDefinitionParser()); + registerBeanDefinitionDecorator("scoped-proxy", new ScopedProxyBeanDefinitionDecorator()); + + // Only in 2.0 XSD: moved to context namespace as of 2.1 + registerBeanDefinitionParser("spring-configured", new SpringConfiguredBeanDefinitionParser()); +} +``` + +这里我们就不再对spring中的自定义注解方式进行讨论了。从这段代码中我们可以得知,在解析配置文件的时候,一旦遇到了aspectj-autoproxy注解的时候会使用解析器AspectJAutoProxyBeanDefinitionParser进行解析,接下来我们就详细分析下其内部实现。 + +### 注册AnnotationAwareAspectJAutoProxyCreator + +所有解析器,因为都是对BeanDefinitionParser接口的统一实现,入口都是从parse函数开始的,AspectJAutoProxyBeanDefinitionParser的parse函数如下: + +```java +@Override +@Nullable +public BeanDefinition parse(Element element, ParserContext parserContext) { + // 注册AnnotationAwareAspectJAutoProxyCreator + AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); + // 对于注解中子类的处理 + extendBeanDefinition(element, parserContext); + return null; +} +``` + +通过代码可以了解到函数的具体逻辑是在registerAspectJAnnotationAutoProxyCreatorIfecessary方法中实现的,继续进入到函数体内: + +```java +/** + * 注册AnnotationAwareAspectJAutoProxyCreator + * @param parserContext + * @param sourceElement + */ +public static void registerAspectJAnnotationAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + // 注册或升级AutoProxyCreator定义beanName为org.springframework.aop.config.internalAutoProxyCreator的BeanDefinition + BeanDefinition beanDefinition = AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + // 对于proxy-target-class以及expose-proxy属性的处理 + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + // 注册组件并通知,便于监听器做进一步处理 + registerComponentIfNecessary(beanDefinition, parserContext); +} +``` + +在registerAspectJAnnotationAutoProxyCreatorIfNeccessary方法中主要完成了3件事情,基本上每行代码都是一个完整的逻辑。接下来我们详细分析每一行代码。 + +#### 注册或升级AnnotationAwareAspectJAutoProxyCreator + +对于AOP的实现,基本上都是靠AnnotationAwareAspectJAutoProxyCreator去完成,它可以根据@Point注解定义的切点来自动代理相匹配的bean。但是为了配置简便,Spring使用了自定义配置来帮助我们自动注册AnnotationAwareAspectJAutoProxyCreator,其注册过程就是在这里实现的。我们继续跟进到方法内部: + +```java +public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, + @Nullable Object source) { + return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source); +} + +public static final String AUTO_PROXY_CREATOR_BEAN_NAME = "org.springframework.aop.config.internalAutoProxyCreator"; + +private static BeanDefinition registerOrEscalateApcAsRequired(Class cls, BeanDefinitionRegistry registry, + @Nullable Object source) { + + Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); + //如果已经存在了自动代理创建器且存在的自动代理创建器与现在的不一致那么需要根据优先级来判断到底需要使用哪个 + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition apcDefinition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + if (!cls.getName().equals(apcDefinition.getBeanClassName())) { + int currentPriority = findPriorityForClass(apcDefinition.getBeanClassName()); + int requiredPriority = findPriorityForClass(cls); + if (currentPriority < requiredPriority) { + //改变bean最重要的就是改变bean所对应的className属性 + apcDefinition.setBeanClassName(cls.getName()); + } + } + return null; + } + //注册beanDefinition,Class为AnnotationAwareAspectJAutoProxyCreator.class,beanName为internalAutoProxyCreator + RootBeanDefinition beanDefinition = new RootBeanDefinition(cls); + beanDefinition.setSource(source); + beanDefinition.getPropertyValues().add("order", Ordered.HIGHEST_PRECEDENCE); + beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registry.registerBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME, beanDefinition); + return beanDefinition; +} +``` + +以上代码实现了自动注册AnnotationAwareAspectJAutoProxyCreator类的功能,同时这里还涉及了一个优先级的问题,如果已经存在了自动代理创建器,而且存在的自动代理创建器与现在的不一致,那么需要根据优先级来判断到底需要使用哪个。 + +### 处理proxy-target-class以及expose-proxy属性 + +useClassProxyingIfNecessary实现了proxy-target-class属性以及expose-proxy属性的处理,进入到方法内部: + +```java +private static void useClassProxyingIfNecessary(BeanDefinitionRegistry registry, @Nullable Element sourceElement) { + if (sourceElement != null) { + //实现了对proxy-target-class的处理 + boolean proxyTargetClass = Boolean.parseBoolean(sourceElement.getAttribute(PROXY_TARGET_CLASS_ATTRIBUTE)); + if (proxyTargetClass) { + AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); + } + //对expose-proxy的处理 + boolean exposeProxy = Boolean.parseBoolean(sourceElement.getAttribute(EXPOSE_PROXY_ATTRIBUTE)); + if (exposeProxy) { + AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); + } + } +} +``` + +在上述代码中用到了两个强制使用的方法,强制使用的过程其实也是一个属性设置的过程,两个函数的方法如下: + +```java +public static void forceAutoProxyCreatorToUseClassProxying(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + definition.getPropertyValues().add("proxyTargetClass", Boolean.TRUE); + } +} + +public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry registry) { + if (registry.containsBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME)) { + BeanDefinition definition = registry.getBeanDefinition(AUTO_PROXY_CREATOR_BEAN_NAME); + definition.getPropertyValues().add("exposeProxy", Boolean.TRUE); + } +} +``` + + + +proxy-target-class:Spring AOP部分使用JDK动态代理或者CGLIB来为目标对象创建代理。(建议尽量使用JDK的动态代理),如果被代理的目标对象实现了至少一个接口, 則会使用JDK动态代理。所有该目标类型实现的接口都将被代理。若该目标对象没有实现任何接口,则创建一个CGLIB代理。如果你希望强制使用CGLIB代理,(例如希望代理目标对象的所有方法,而不只是实现自接口的方法)那也可以。但是需要考虑以下两个问题。 + +1. 无法通知(advise) Final方法,因为它们不能被覆写。 +2. 你需要将CGLIB二进制发行包放在classpath下面。 + +与之相较,JDK本身就提供了动态代理,强制使用CGLIB代理需要将的 proxy-target-class 厲性设为 true: + +``` +... +``` + +当需要使用CGLIB代理和@AspectJ自动代理支持,可以按照以下方式设罝的 proxy-target-class 属性: + +``` + +``` + +- JDK动态代理:其代理对象必须是某个接口的实现,它是通过在运行期间创建一个接口的实现类来完成对目标对象的代理。 +- CGIJB代理:实现原理类似于JDK动态代理,只是它在运行期间生成的代理对象是针对目标类扩展的子类。CGLIB是高效的代码生成包,底层是依靠ASM (开源的Java字节码编辑类库)操作字节码实现的,性能比JDK强。 +- expose-proxy:有时候目标对象内部的自我调用将无法实施切面中的增强,如下示例: + +```java +public interface AService { + public void a(); + public void b(); +} + +@Service() +public class AServicelmpll implements AService { + @Transactional(propagation = Propagation.REQUIRED) + public void a() { + this.b{); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + public void b() { + } +} +``` + +此处的this指向目标对象,因此调用this.b()将不会执行b事务切面,即不会执行事务增强, 因此 b 方法的事务定义“@Transactional(propagation = Propagation.REQUIRES_NEW)” 将不会实施,为了解决这个问题,我们可以这样做: + +``` + +``` + +然后将以上代码中的 “this.b();” 修改为 “((AService) AopContext.currentProxy()).b();” 即可。 通过以上的修改便可以完成对a和b方法的同时增强。 \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\233\256\346\240\207\346\226\271\346\263\225\345\222\214\345\242\236\345\274\272\346\226\271\346\263\225\347\232\204\346\211\247\350\241\214.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\233\256\346\240\207\346\226\271\346\263\225\345\222\214\345\242\236\345\274\272\346\226\271\346\263\225\347\232\204\346\211\247\350\241\214.md" new file mode 100644 index 0000000..901cfed --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\233\256\346\240\207\346\226\271\346\263\225\345\222\214\345\242\236\345\274\272\346\226\271\346\263\225\347\232\204\346\211\247\350\241\214.md" @@ -0,0 +1,154 @@ +**正文** + +上一篇博文中我们讲了代理类的生成,这一篇主要讲解剩下的部分,当代理类调用时,目标方法和代理方法是如何执行的,我们还是接着上篇的ReflectiveMethodInvocation类Proceed方法来看 + +```java +public Object proceed() throws Throwable { + // 首先,判断是不是所有的interceptor(也可以想像成advisor)都被执行完了。 + // 判断的方法是看currentInterceptorIndex这个变量的值,增加到Interceptor总个数这个数值没有, + // 如果到了,就执行被代理方法(invokeJoinpoint());如果没到,就继续执行Interceptor。 + if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { + return invokeJoinpoint(); + } + + // 如果Interceptor没有被全部执行完,就取出要执行的Interceptor,并执行。 + // currentInterceptorIndex先自增 + Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex); + // 如果Interceptor是PointCut类型 + if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) { + InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice; + // 如果当前方法符合Interceptor的PointCut限制,就执行Interceptor + if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) { +    // 这里将this当变量传进去,这是非常重要的一点 + return dm.interceptor.invoke(this); + } + // 如果不符合,就跳过当前Interceptor,执行下一个Interceptor + else { + return proceed(); + } + } + // 如果Interceptor不是PointCut类型,就直接执行Interceptor里面的增强。 + else { + return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this); + } +} +``` + +我们先来看一张方法调用顺序图 + +![](http://img.topjavaer.cn/img/202310011108435.png) + +我们看到链中的顺序是AspectJAfterThrowingAdvice、AfterReturningAdviceInterceptor、AspectJAfterAdvice、MethodBeforeAdviceInterceptor,这些拦截器是按顺序执行的,那我们来看看第一个拦截器AspectJAfterThrowingAdvice中的invoke方法 + +## **AspectJAfterThrowingAdvice** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + try { + //直接调用MethodInvocation的proceed方法 + //从proceed()方法中我们知道dm.interceptor.invoke(this)传过来的参数就是ReflectiveMethodInvocation执行器本身 + //这里又直接调用了ReflectiveMethodInvocation的proceed()方法 + return mi.proceed(); + } + catch (Throwable ex) { + if (shouldInvokeOnThrowing(ex)) { + invokeAdviceMethod(getJoinPointMatch(), null, ex); + } + throw ex; + } + } +``` + +第一个拦截器AspectJAfterThrowingAdvice的invoke方法中,直接调用mi.proceed();,从proceed()方法中我们知道dm.interceptor.invoke(this)传过来的参数就是ReflectiveMethodInvocation执行器本身,所以又会执行proceed()方法,拦截器下标currentInterceptorIndex自增,获取下一个拦截器AfterReturningAdviceInterceptor,并调用拦截器中的invoke方法,,此时第一个拦截器在invoke()方法的第七行卡住了,接下来我们看第二个拦截器的执行 + +## **AfterReturningAdviceInterceptor** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + //直接调用MethodInvocation的proceed方法 + Object retVal = mi.proceed(); + this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis()); + return retVal; + } +``` + +AfterReturningAdviceInterceptor还是直接调用mi.proceed(),又回到了ReflectiveMethodInvocation的proceed()方法中,此时AfterReturningAdviceInterceptor方法卡在第四行,接着回到ReflectiveMethodInvocation的proceed()方法中,拦截器下标currentInterceptorIndex自增,获取下一个拦截器AspectJAfterAdvice,并调用AspectJAfterAdvice中的invoke方法 + +## **AspectJAfterAdvice** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + try { + //直接调用MethodInvocation的proceed方法 + return mi.proceed(); + } + finally { + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + } +``` + +AspectJAfterAdvice还是直接调用mi.proceed(),又回到了ReflectiveMethodInvocation的proceed()方法中,此时**AspectJAfterAdvice**方法卡在第五行,接着回到ReflectiveMethodInvocation的proceed()方法中,拦截器下标currentInterceptorIndex自增,获取下一个拦截器MethodBeforeAdviceInterceptor,并调用MethodBeforeAdviceInterceptor中的invoke方法 + +## **MethodBeforeAdviceInterceptor** + +```java + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + //终于开始做事了,调用增强器的before方法,明显是通过反射的方式调用 + //到这里增强方法before的业务逻辑执行 + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + //又调用了调用MethodInvocation的proceed方法 + return mi.proceed(); + } +``` + +第5行代码终于通过反射调用了切面里面的before方法,接着又调用mi.proceed(),我们知道这是最后一个拦截器了,此时this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1应该为true了,那么就会执行 return invokeJoinpoint();,也就是执行bean中的目标方法,接着我们来看看目标方法的执行 + +```java +@Nullable +protected Object invokeJoinpoint() throws Throwable { + return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments); +} + + @Nullable +public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args) + throws Throwable { + + // Use reflection to invoke the method. + try { + ReflectionUtils.makeAccessible(method); + //直接通过反射调用目标bean中的method + return method.invoke(target, args); + } + catch (InvocationTargetException ex) { + // Invoked method threw a checked exception. + // We must rethrow it. The client won't see the interceptor. + throw ex.getTargetException(); + } + catch (IllegalArgumentException ex) { + throw new AopInvocationException("AOP configuration seems to be invalid: tried calling method [" + + method + "] on target [" + target + "]", ex); + } + catch (IllegalAccessException ex) { + throw new AopInvocationException("Could not access method [" + method + "]", ex); + } +} +``` + +before方法执行完后,就通过反射的方式执行目标bean中的method,并且返回结果,接下来我们想想程序该怎么执行呢? + +1 、MethodBeforeAdviceInterceptor执行完了后,开始退栈,AspectJAfterAdvice中invoke卡在第5行的代码继续往下执行, 我们看到在AspectJAfterAdvice的invoke方法中的finally中第8行有这样一句话 invokeAdviceMethod(getJoinPointMatch(), null, null);,就是通过反射调用AfterAdvice的方法,意思是切面类中的 @After方法不管怎样都会执行,因为在finally中 + + 2、AspectJAfterAdvice中invoke方法发执行完后,也开始退栈,接着就到了AfterReturningAdviceInterceptor的invoke方法的第4行开始恢复,但是此时如果目标bean和前面增强器中出现了异常,此时AfterReturningAdviceInterceptor中第5行代码就不会执行了,直接退栈;如果没有出现异常,则执行第5行,也就是通过反射执行切面类中@AfterReturning注解的方法,然后退栈 + +3、AfterReturningAdviceInterceptor退栈后,就到了AspectJAfterThrowingAdvice拦截器,此拦截器中invoke方法的第7行开始恢复,我们看到在 catch (Throwable ex) { 代码中,也就是第11行 invokeAdviceMethod(getJoinPointMatch(), null, ex);,如果目标bean的method或者前面的增强方法中出现了异常,则会被这里的catch捕获,也是通过反射的方式执行@AfterThrowing注解的方法,然后退栈\ + + + +## 总结 + +这个代理类调用过程,我们可以看到是一个递归的调用过程,通过ReflectiveMethodInvocation类中Proceed方法递归调用,顺序执行拦截器链中AspectJAfterThrowingAdvice、AfterReturningAdviceInterceptor、AspectJAfterAdvice、MethodBeforeAdviceInterceptor这几个拦截器,在拦截器中反射调用增强方法 \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224ApplicationContext\345\256\271\345\231\250refresh\350\277\207\347\250\213.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224ApplicationContext\345\256\271\345\231\250refresh\350\277\207\347\250\213.md" new file mode 100644 index 0000000..b0126d2 --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224ApplicationContext\345\256\271\345\231\250refresh\350\277\207\347\250\213.md" @@ -0,0 +1,1164 @@ +**正文** + +在之前的博文中我们一直以BeanFactory接口以及它的默认实现类XmlBeanFactory为例进行分析,但是Spring中还提供了另一个接口ApplicationContext,用于扩展BeanFactory中现有的功能。 + +ApplicationContext和BeanFactory两者都是用于加载Bean的,但是相比之下,ApplicationContext提供了更多的扩展功能,简而言之:ApplicationContext包含BeanFactory的所有功能。通常建议比优先使用ApplicationContext,除非在一些限制的场合,比如字节长度对内存有很大的影响时(Applet),绝大多数“典型的”企业应用系统,ApplicationContext就是需要使用的。 + +那么究竟ApplicationContext比BeanFactory多了哪些功能?首先我们来看看使用两个不同的类去加载配置文件在写法上的不同如下代码: + +```java +//使用BeanFactory方式加载XML. +BeanFactory bf = new XmlBeanFactory(new ClassPathResource("beanFactoryTest.xml")); + +//使用ApplicationContext方式加载XML. +ApplicationContext bf = new ClassPathXmlApplicationContext("beanFactoryTest.xml"); +``` + +接下来我们就以ClassPathXmlApplicationContext作为切入点,开始对整体功能进行分析。首先看下其构造函数: + +```java +public ClassPathXmlApplicationContext() { +} + +public ClassPathXmlApplicationContext(ApplicationContext parent) { + super(parent); +} + +public ClassPathXmlApplicationContext(String configLocation) throws BeansException { + this(new String[] {configLocation}, true, null); +} + +public ClassPathXmlApplicationContext(String... configLocations) throws BeansException { + this(configLocations, true, null); +} + +public ClassPathXmlApplicationContext(String[] configLocations, @Nullable ApplicationContext parent) + throws BeansException { + + this(configLocations, true, parent); +} + +public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh) throws BeansException { + this(configLocations, refresh, null); +} + +public ClassPathXmlApplicationContext(String[] configLocations, boolean refresh, @Nullable ApplicationContext parent) + throws BeansException { + super(parent); + setConfigLocations(configLocations); + if (refresh) { + refresh(); + } +} +``` + +设置路径是必不可少的步骤,ClassPathXmlApplicationContext中可以将配置文件路径以数组的方式传入,ClassPathXmlApplicationContext可以对数组进行解析并进行加载。而对于解析及功能实现都在refresh()中实现。 + +## 设置配置路径 + +在ClassPathXmlApplicationContext中支持多个配置文件以数组方式同时传入,以下是设置配置路径方法代码: + +```java +public void setConfigLocations(@Nullable String... locations) { + if (locations != null) { + Assert.noNullElements(locations, "Config locations must not be null"); + this.configLocations = new String[locations.length]; + for (int i = 0; i < locations.length; i++) { + this.configLocations[i] = resolvePath(locations[i]).trim(); + } + } + else { + this.configLocations = null; + } +} +``` + +其中如果给定的路径中包含特殊符号,如${var},那么会在方法resolvePath中解析系统变量并替换 + +## 扩展功能 + +设置了路径之后,便可以根据路径做配置文件的解析以及各种功能的实现了。可以说refresh函数中包含了几乎ApplicationContext中提供的全部功能,而且此函数中逻辑非常清晰明了,使我们很容易分析对应的层次及逻辑,我们看下方法代码: + +```java +public void refresh() throws BeansException, IllegalStateException { + synchronized (this.startupShutdownMonitor) { + //准备刷新的上下文 环境 + prepareRefresh(); + //初始化BeanFactory,并进行XML文件读取 + /* + * ClassPathXMLApplicationContext包含着BeanFactory所提供的一切特征,在这一步骤中将会复用 + * BeanFactory中的配置文件读取解析及其他功能,这一步之后,ClassPathXmlApplicationContext + * 实际上就已经包含了BeanFactory所提供的功能,也就是可以进行Bean的提取等基础操作了。 + */ + ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory(); + //对beanFactory进行各种功能填充 + prepareBeanFactory(beanFactory); + try { + //子类覆盖方法做额外处理 + /* + * Spring之所以强大,为世人所推崇,除了它功能上为大家提供了便利外,还有一方面是它的 + * 完美架构,开放式的架构让使用它的程序员很容易根据业务需要扩展已经存在的功能。这种开放式 + * 的设计在Spring中随处可见,例如在本例中就提供了一个空的函数实现postProcessBeanFactory来 + * 方便程序猿在业务上做进一步扩展 + */ + postProcessBeanFactory(beanFactory); + //激活各种beanFactory处理器 + invokeBeanFactoryPostProcessors(beanFactory); + //注册拦截Bean创建的Bean处理器,这里只是注册,真正的调用实在getBean时候 + registerBeanPostProcessors(beanFactory); + //为上下文初始化Message源,即不同语言的消息体,国际化处理 + initMessageSource(); + //初始化应用消息广播器,并放入“applicationEventMulticaster”bean中 + initApplicationEventMulticaster(); + //留给子类来初始化其它的Bean + onRefresh(); + //在所有注册的bean中查找Listener bean,注册到消息广播器中 + registerListeners(); + //初始化剩下的单实例(非惰性的) + finishBeanFactoryInitialization(beanFactory); + //完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人 + finishRefresh(); + } + catch (BeansException ex) { + if (logger.isWarnEnabled()) { + logger.warn("Exception encountered during context initialization - " + + "cancelling refresh attempt: " + ex); + } + destroyBeans(); + cancelRefresh(ex); + throw ex; + } + finally { + resetCommonCaches(); + } + } +} +``` + +我们简单的分析下代码的步骤: + +(1)初始化前的准备工作,例如对系统属性或者环境变量进行准备及验证。 + +在某种情况下项目的使用需要读取某些系统变量,而这个变量的设置很可能会影响着系统的正确性,那么ClassPathXmlApplicationContext为我们提供的这个准备函数就显得非常必要,他可以在spring启动的时候提前对必须的环境变量进行存在性验证。 + +(2)初始化BeanFactory,并进行XML文件读取。 + +之前提到ClassPathXmlApplicationContext包含着对BeanFactory所提供的一切特征,那么这一步中将会复用BeanFactory中的配置文件读取解析其他功能,这一步之后ClassPathXmlApplicationContext实际上就已经包含了BeanFactory所提供的功能,也就是可以进行Bean的提取等基本操作了。 + +(3)对BeanFactory进行各种功能填充 + +@Qualifier和@Autowired应该是大家非常熟悉的注解了,那么这两个注解正是在这一步骤中增加支持的。 + +(4)子类覆盖方法做额外处理。 + +spring之所以强大,为世人所推崇,除了它功能上为大家提供了遍历外,还有一方面是它完美的架构,开放式的架构让使用它的程序员很容易根据业务需要扩展已经存在的功能。这种开放式的设计在spring中随处可见,例如本利中就提供了一个空的函数实现postProcessBeanFactory来方便程序员在业务上做进一步的扩展。 + +(5)激活各种BeanFactory处理器 + +(6)注册拦截bean创建的bean处理器,这里只是注册,真正的调用是在getBean时候 + +(7)为上下文初始化Message源,及对不同语言的小西天进行国际化处理 + +(8)初始化应用消息广播器,并放入“applicationEventMulticaster”bean中 + +(9)留给子类来初始化其他的bean + +(10)在所有注册的bean中查找listener bean,注册到消息广播器中 + +(11)初始化剩下的单实例(非惰性的) + +(12)完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知别人。 + +接下来我们就详细的讲解每一个过程 + +## prepareRefresh刷新上下文的准备工作 + +```java +/** + * 准备刷新上下文环境,设置它的启动日期和活动标志,以及执行任何属性源的初始化。 + * Prepare this context for refreshing, setting its startup date and + * active flag as well as performing any initialization of property sources. + */ +protected void prepareRefresh() { + this.startupDate = System.currentTimeMillis(); + this.closed.set(false); + this.active.set(true); + + // 在上下文环境中初始化任何占位符属性源。(空的方法,留给子类覆盖) + initPropertySources(); + + // 验证需要的属性文件是否都已放入环境中 + getEnvironment().validateRequiredProperties(); + + // 允许收集早期的应用程序事件,一旦有了多播器,就可以发布…… + this.earlyApplicationEvents = new LinkedHashSet<>(); +} +``` + + + +## obtainFreshBeanFactory->读取xml并初始化BeanFactory + +obtainFreshBeanFactory方法从字面理解是获取beanFactory.ApplicationContext是对BeanFactory的扩展,在其基础上添加了大量的基础应用,obtainFreshBeanFactory正式实现beanFactory的地方,经过这个函数后ApplicationContext就有了BeanFactory的全部功能。我们看下此方法的代码: + +```java +protected ConfigurableListableBeanFactory obtainFreshBeanFactory() { + //初始化BeanFactory,并进行XML文件读取,并将得到的BeanFactory记录在当前实体的属性中 + refreshBeanFactory(); + //返回当前实体的beanFactory属性 + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + if (logger.isDebugEnabled()) { + logger.debug("Bean factory for " + getDisplayName() + ": " + beanFactory); + } + return beanFactory; +} +``` + +继续深入到refreshBeanFactory方法中,方法的实现是在AbstractRefreshableApplicationContext中: + +```java +@Override +protected final void refreshBeanFactory() throws BeansException { + if (hasBeanFactory()) { + destroyBeans(); + closeBeanFactory(); + } + try { + //创建DefaultListableBeanFactory + /* + * 以前我们分析BeanFactory的时候,不知道是否还有印象,声明方式为:BeanFactory bf = + * new XmlBeanFactory("beanFactoryTest.xml"),其中的XmlBeanFactory继承自DefaulltListableBeanFactory; + * 并提供了XmlBeanDefinitionReader类型的reader属性,也就是说DefaultListableBeanFactory是容器的基础。必须 + * 首先要实例化。 + */ + DefaultListableBeanFactory beanFactory = createBeanFactory(); + //为了序列化指定id,如果需要的话,让这个BeanFactory从id反序列化到BeanFactory对象 + beanFactory.setSerializationId(getId()); + //定制beanFactory,设置相关属性,包括是否允许覆盖同名称的不同定义的对象以及循环依赖以及设置 + //@Autowired和Qualifier注解解析器QualifierAnnotationAutowireCandidateResolver + customizeBeanFactory(beanFactory); + //加载BeanDefiniton + loadBeanDefinitions(beanFactory); + synchronized (this.beanFactoryMonitor) { + //使用全局变量记录BeanFactory实例。 + //因为DefaultListableBeanFactory类型的变量beanFactory是函数内部的局部变量, + //所以要使用全局变量记录解析结果 + this.beanFactory = beanFactory; + } + } + catch (IOException ex) { + throw new ApplicationContextException("I/O error parsing bean definition source for " + getDisplayName(), ex); + } +} +``` + +### 加载BeanDefinition + +在第一步中提到了将ClassPathXmlApplicationContext与XMLBeanFactory创建的对比,除了初始化DefaultListableBeanFactory外,还需要XmlBeanDefinitionReader来读取XML,那么在loadBeanDefinitions方法中首先要做的就是初始化XmlBeanDefinitonReader,我们跟着到loadBeanDefinitions(beanFactory)方法体中,我们看到的是在AbstractXmlApplicationContext中实现的,具体代码如下: + +```java +@Override +protected void loadBeanDefinitions(DefaultListableBeanFactory beanFactory) throws BeansException, IOException { + // Create a new XmlBeanDefinitionReader for the given BeanFactory. + XmlBeanDefinitionReader beanDefinitionReader = new XmlBeanDefinitionReader(beanFactory); + + // Configure the bean definition reader with this context's + // resource loading environment. + beanDefinitionReader.setEnvironment(this.getEnvironment()); + beanDefinitionReader.setResourceLoader(this); + beanDefinitionReader.setEntityResolver(new ResourceEntityResolver(this)); + + // Allow a subclass to provide custom initialization of the reader, + // then proceed with actually loading the bean definitions. + initBeanDefinitionReader(beanDefinitionReader); + loadBeanDefinitions(beanDefinitionReader); +} +``` + +在初始化了DefaultListableBeanFactory和XmlBeanDefinitionReader后,就可以进行配置文件的读取了。继续进入到loadBeanDefinitions(beanDefinitionReader)方法体中,代码如下: + +``` +protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansException, IOException { + Resource[] configResources = getConfigResources(); + if (configResources != null) { + reader.loadBeanDefinitions(configResources); + } + String[] configLocations = getConfigLocations(); + if (configLocations != null) { + reader.loadBeanDefinitions(configLocations); + } +} +``` + +因为在XmlBeanDefinitionReader中已经将之前初始化的DefaultListableBeanFactory注册进去了,所以XmlBeanDefinitionReader所读取的BeanDefinitionHolder都会注册到DefinitionListableBeanFactory中,也就是经过这个步骤,DefaultListableBeanFactory的变量beanFactory已经包含了所有解析好的配置。 + +## 功能扩展 + +如上图所示prepareBeanFactory(beanFactory)就是在功能上扩展的方法,而在进入这个方法前spring已经完成了对配置的解析,接下来我们详细分析下次函数,进入方法体: + +```java +protected void prepareBeanFactory(ConfigurableListableBeanFactory beanFactory) { + // Tell the internal bean factory to use the context's class loader etc. + //设置beanFactory的classLoader为当前context的classloader + beanFactory.setBeanClassLoader(getClassLoader()); + //设置beanFactory的表达式语言处理器,Spring3增加了表达式语言的支持, + //默认可以使用#{bean.xxx}的形式来调用相关属性值 + beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver(beanFactory.getBeanClassLoader())); + //为beanFactory增加了一个的propertyEditor,这个主要是对bean的属性等设置管理的一个工具 + beanFactory.addPropertyEditorRegistrar(new ResourceEditorRegistrar(this, getEnvironment())); + + // Configure the bean factory with context callbacks. + beanFactory.addBeanPostProcessor(new ApplicationContextAwareProcessor(this)); + //设置了几个忽略自动装配的接口 + beanFactory.ignoreDependencyInterface(EnvironmentAware.class); + beanFactory.ignoreDependencyInterface(EmbeddedValueResolverAware.class); + beanFactory.ignoreDependencyInterface(ResourceLoaderAware.class); + beanFactory.ignoreDependencyInterface(ApplicationEventPublisherAware.class); + beanFactory.ignoreDependencyInterface(MessageSourceAware.class); + beanFactory.ignoreDependencyInterface(ApplicationContextAware.class); + + // BeanFactory interface not registered as resolvable type in a plain factory. + // MessageSource registered (and found for autowiring) as a bean. + //设置了几个自动装配的特殊规则 + beanFactory.registerResolvableDependency(BeanFactory.class, beanFactory); + beanFactory.registerResolvableDependency(ResourceLoader.class, this); + beanFactory.registerResolvableDependency(ApplicationEventPublisher.class, this); + beanFactory.registerResolvableDependency(ApplicationContext.class, this); + + // Register early post-processor for detecting inner beans as ApplicationListeners. + beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(this)); + + // Detect a LoadTimeWeaver and prepare for weaving, if found. + //增加对AspectJ的支持 + if (beanFactory.containsBean(LOAD_TIME_WEAVER_BEAN_NAME)) { + beanFactory.addBeanPostProcessor(new LoadTimeWeaverAwareProcessor(beanFactory)); + // Set a temporary ClassLoader for type matching. + beanFactory.setTempClassLoader(new ContextTypeMatchClassLoader(beanFactory.getBeanClassLoader())); + } + + // Register default environment beans. + //添加默认的系统环境bean + if (!beanFactory.containsLocalBean(ENVIRONMENT_BEAN_NAME)) { + beanFactory.registerSingleton(ENVIRONMENT_BEAN_NAME, getEnvironment()); + } + if (!beanFactory.containsLocalBean(SYSTEM_PROPERTIES_BEAN_NAME)) { + beanFactory.registerSingleton(SYSTEM_PROPERTIES_BEAN_NAME, getEnvironment().getSystemProperties()); + } + if (!beanFactory.containsLocalBean(SYSTEM_ENVIRONMENT_BEAN_NAME)) { + beanFactory.registerSingleton(SYSTEM_ENVIRONMENT_BEAN_NAME, getEnvironment().getSystemEnvironment()); + } +} +``` + +详细分析下代码发现上面函数主要是在以下方法进行了扩展: + +(1)对SPEL语言的支持 + +(2)增加对属性编辑器的支持 + +(3)增加对一些内置类的支持,如EnvironmentAware、MessageSourceAware的注入 + +(4)设置了依赖功能可忽略的接口 + +(5)注册一些固定依赖的属性 + +(6)增加了AspectJ的支持 + +(7)将相关环境变量及属性以单例模式注册 + + + +### 增加对SPEL语言的支持 + +Spring表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,类似于Struts 2x中使用的OGNL语言,SpEL是单独模块,只依赖于core模块,不依赖于其他模块,可以单独使用 + +SpEL使用#{…}作为定界符,所有在大框号中的字符都将被认为是SpEL,使用格式如下: + +```xml + + + + + + + + +``` + +上面只是列举了其中最简单的使用方式,SpEL功能非常强大,使用好可以大大提高开发效率。在源码中通过代码beanFactory.setBeanExpressionResolver(new StandardBeanExpressionResolver()),注册语言解析器,就可以对SpEL进行解析了,那么之后是在什么地方调用这个解析器的呢? + +之前说beanFactory中说过Spring在bean进行初始化的时候会有属性填充的一步,而在这一步中Spring会调用AbstractAutowireCapabelBeanFactory类的applyPropertyValues来进行属性值得解析。同时这个步骤中一般通过AbstractBeanFactory中的evaluateBeanDefinitionString方法进行SpEL解析,方法代码如下: + +```java +protected Object evaluateBeanDefinitionString(String value, BeanDefinition beanDefinition) { + if (this.beanExpressionResolver == null) { + return value; + } + Scope scope = (beanDefinition != null ? getRegisteredScope(beanDefinition.getScope()) : null); + return this.beanExpressionResolver.evaluate(value, new BeanExpressionContext(this, scope)); +} +``` + + + +## BeanFactory的后处理 + +BeanFactory作为spring中容器功能的基础,用于存放所有已经加载的bean,为例保证程序上的高可扩展性,spring针对BeanFactory做了大量的扩展,比如我们熟悉的PostProcessor就是在这里实现的。接下来我们就深入分析下BeanFactory后处理 + +### 激活注册的BeanFactoryPostProcessor + +在正是介绍BeanFactoryPostProcessor的后处理前我们先简单的了解下其用法,BeanFactoryPostProcessor接口跟BeanPostProcessor类似,都可以对bean的定义(配置元数据)进行处理,也就是说spring IoC容器允许BeanFactoryPostProcessor在容器实际实例化任何其他的bean之前读取配置元数据,并可能修改他。也可以配置多个BeanFactoryPostProcessor,可以通过order属性来控制BeanFactoryPostProcessor的执行顺序(此属性必须当BeanFactoryPostProcessor实现了Ordered的接口时才可以赊账,因此在实现BeanFactoryPostProcessor时应该考虑实现Ordered接口) + +如果想改变世界的bean实例(例如从配置元数据创建的对象),那最好使用BeanPostProcessor。同样的BeanFactoryPostProcessor的作用域范围是容器级别的,它只是和你锁使用的容器有关。如果你在容器中定义了一个BeanFactoryPostProcessor,它仅仅对此容器中的bean进行后置处理。BeanFactoryPostProcessor不会对定义在另一个容器中的bean进行后置处理,即使这两个容器都在同一层次上。在spring中存在对于BeanFactoryPostProcessor的典型应用,如PropertyPlaceholderConfigurer。 + + + +#### BeanFactoryPostProcessor的典型应用:PropertyPlaceholderConfigurer + +有时候我们在阅读spring的配置文件中的Bean的描述时,会遇到类似如下情况: + +```xml + + + + +``` + +这其中出现了变量:user.name、user.name、{user.birthday},这是spring的分散配置,可以在另外的配置文件中为user.name、user.birthday指定值,例如在bean.properties文件中定义: + +``` +user.name = xiaoming +user.birthday = 2019-04-19 +``` + +当访问名为user的bean时,其name属性就会被字符串xiaoming替换,那spring框架是怎么知道存在这样的配置文件呢,这个就是PropertyPlaceholderConfigurer,需要在配置文件中添加一下代码: + +```xml + + + + classpath:bean.properties + + + +``` + +在这个bean中指定了配置文件的位置。其实还是有个问题,这个userHandler只不过是spring框架管理的一个bean,并没有被别的bean或者对象引用,spring的beanFactory是怎么知道这个需要从这个bean中获取配置信息呢?我们看下PropertyPlaceholderConfigurer这个类的层次结构,如下图: + +![](http://img.topjavaer.cn/img/202309241051602.png) + +从上图中我们可以看到PropertyPlaceholderConfigurer间接的继承了BeanFactoryPostProcessor接口,这是一个很特别的A接口,当spring加载任何实现了这个接口的bean的配置时,都会在bean工厂载入所有bean的配置之后执行postProcessBeanFactory方法。在PropertyResourceConfigurer类中实现了postProcessBeanFactory方法,在方法中先后调用了mergeProperties、convertProperties、processProperties这三个方法,分别得到配置,将得到的配置转换为合适的类型,最后将配置内容告知BeanFactory。 + +正是通过实现BeanFactoryPostProcessor接口,BeanFactory会在实例化任何bean之前获得配置信息,从而能够正确的解析bean描述文件中的变量引用。 + +```java +public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + try { + Properties mergedProps = this.mergeProperties(); + this.convertProperties(mergedProps); + this.processProperties(beanFactory, mergedProps); + } catch (IOException var3) { + throw new BeanInitializationException("Could not load properties", var3); + } +} +``` + + + +### 自定义BeanFactoryPostProcessor + +编写实现了BeanFactoryPostProcessor接口的MyBeanFactoryPostProcessor的容器后处理器,如下代码: + +```java +public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor { + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + System.out.println("对容器进行后处理。。。。"); + } +} +``` + +然后在配置文件中注册这个bean,如下: + +```xml + +``` + +最后编写测试代码: + +```java +public class Test { + public static void main(String[] args) { + ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml"); + + User user = (User)context.getBean("user"); + System.out.println(user.getName()); + + } +} +``` + + + +### 激活BeanFactoryPostProcessor + +在了解BeanFactoryPostProcessor的用法后我们便可以深入的研究BeanFactoryPostProcessor的调用过程了,其是在方法invokeBeanFactoryPostProcessors(beanFactory)中实现的,进入到方法内部: + +```java +public static void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory, List beanFactoryPostProcessors) { + + // Invoke BeanDefinitionRegistryPostProcessors first, if any. + // 1、首先调用BeanDefinitionRegistryPostProcessors + Set processedBeans = new HashSet<>(); + + // beanFactory是BeanDefinitionRegistry类型 + if (beanFactory instanceof BeanDefinitionRegistry) { + BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory; + // 定义BeanFactoryPostProcessor + List regularPostProcessors = new ArrayList<>(); + // 定义BeanDefinitionRegistryPostProcessor集合 + List registryProcessors = new ArrayList<>(); + + // 循环手动注册的beanFactoryPostProcessors + for (BeanFactoryPostProcessor postProcessor : beanFactoryPostProcessors) { + // 如果是BeanDefinitionRegistryPostProcessor的实例话,则调用其postProcessBeanDefinitionRegistry方法,对bean进行注册操作 + if (postProcessor instanceof BeanDefinitionRegistryPostProcessor) { + // 如果是BeanDefinitionRegistryPostProcessor类型,则直接调用其postProcessBeanDefinitionRegistry + BeanDefinitionRegistryPostProcessor registryProcessor = (BeanDefinitionRegistryPostProcessor) postProcessor; + registryProcessor.postProcessBeanDefinitionRegistry(registry); + registryProcessors.add(registryProcessor); + } + // 否则则将其当做普通的BeanFactoryPostProcessor处理,直接加入regularPostProcessors集合,以备后续处理 + else { + regularPostProcessors.add(postProcessor); + } + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the bean factory post-processors apply to them! + // Separate between BeanDefinitionRegistryPostProcessors that implement + // PriorityOrdered, Ordered, and the rest. + List currentRegistryProcessors = new ArrayList<>(); + + // First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered. + // 首先调用实现了PriorityOrdered(有限排序接口)的BeanDefinitionRegistryPostProcessors + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + } + } + // 排序 + sortPostProcessors(currentRegistryProcessors, beanFactory); + // 加入registryProcessors集合 + registryProcessors.addAll(currentRegistryProcessors); + // 调用所有实现了PriorityOrdered的的BeanDefinitionRegistryPostProcessors的postProcessBeanDefinitionRegistry方法,注册bean + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + // 清空currentRegistryProcessors,以备下次使用 + currentRegistryProcessors.clear(); + + // Next, invoke the BeanDefinitionRegistryPostProcessors that implement Ordered. + // 其次,调用实现了Ordered(普通排序接口)的BeanDefinitionRegistryPostProcessors + postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (!processedBeans.contains(ppName) && beanFactory.isTypeMatch(ppName, Ordered.class)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + } + } + // 排序 + sortPostProcessors(currentRegistryProcessors, beanFactory); + // 加入registryProcessors集合 + registryProcessors.addAll(currentRegistryProcessors); + // 调用所有实现了PriorityOrdered的的BeanDefinitionRegistryPostProcessors的postProcessBeanDefinitionRegistry方法,注册bean + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + // 清空currentRegistryProcessors,以备下次使用 + currentRegistryProcessors.clear(); + + // Finally, invoke all other BeanDefinitionRegistryPostProcessors until no further ones appear. + // 最后,调用其他的BeanDefinitionRegistryPostProcessors + boolean reiterate = true; + while (reiterate) { + reiterate = false; + postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); + for (String ppName : postProcessorNames) { + if (!processedBeans.contains(ppName)) { + currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); + processedBeans.add(ppName); + reiterate = true; + } + } + // 排序 + sortPostProcessors(currentRegistryProcessors, beanFactory); + // 加入registryProcessors集合 + registryProcessors.addAll(currentRegistryProcessors); + // 调用其他的BeanDefinitionRegistryPostProcessors的postProcessBeanDefinitionRegistry方法,注册bean + invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry); + // 清空currentRegistryProcessors,以备下次使用 + currentRegistryProcessors.clear(); + } + + // Now, invoke the postProcessBeanFactory callback of all processors handled so far. + // 调用所有BeanDefinitionRegistryPostProcessor(包括手动注册和通过配置文件注册) + // 和BeanFactoryPostProcessor(只有手动注册)的回调函数-->postProcessBeanFactory + invokeBeanFactoryPostProcessors(registryProcessors, beanFactory); + invokeBeanFactoryPostProcessors(regularPostProcessors, beanFactory); + } + + // 2、如果不是BeanDefinitionRegistry的实例,那么直接调用其回调函数即可-->postProcessBeanFactory + else { + // Invoke factory processors registered with the context instance. + invokeBeanFactoryPostProcessors(beanFactoryPostProcessors, beanFactory); + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let the bean factory post-processors apply to them! + // 3、上面的代码已经处理完了所有的BeanDefinitionRegistryPostProcessors和手动注册的BeanFactoryPostProcessor + // 接下来要处理通过配置文件注册的BeanFactoryPostProcessor + // 首先获取所有的BeanFactoryPostProcessor(注意:这里获取的集合会包含BeanDefinitionRegistryPostProcessors) + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanFactoryPostProcessor.class, true, false); + + // Separate between BeanFactoryPostProcessors that implement PriorityOrdered, Ordered, and the rest. + // 这里,将实现了PriorityOrdered,Ordered的处理器和其他的处理器区分开来,分别进行处理 + // PriorityOrdered有序处理器 + List priorityOrderedPostProcessors = new ArrayList<>(); + // Ordered有序处理器 + List orderedPostProcessorNames = new ArrayList<>(); + // 无序处理器 + List nonOrderedPostProcessorNames = new ArrayList<>(); + for (String ppName : postProcessorNames) { + // 判断processedBeans是否包含当前处理器(processedBeans中的处理器已经被处理过);如果包含,则不做任何处理 + if (processedBeans.contains(ppName)) { + // skip - already processed in first phase above + } + else if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + // 加入到PriorityOrdered有序处理器集合 + priorityOrderedPostProcessors.add(beanFactory.getBean(ppName, BeanFactoryPostProcessor.class)); + } + else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { + // 加入到Ordered有序处理器集合 + orderedPostProcessorNames.add(ppName); + } + else { + // 加入到无序处理器集合 + nonOrderedPostProcessorNames.add(ppName); + } + } + + // First, invoke the BeanFactoryPostProcessors that implement PriorityOrdered. + // 首先调用实现了PriorityOrdered接口的处理器 + sortPostProcessors(priorityOrderedPostProcessors, beanFactory); + invokeBeanFactoryPostProcessors(priorityOrderedPostProcessors, beanFactory); + + // Next, invoke the BeanFactoryPostProcessors that implement Ordered. + // 其次,调用实现了Ordered接口的处理器 + List orderedPostProcessors = new ArrayList<>(); + for (String postProcessorName : orderedPostProcessorNames) { + orderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); + } + sortPostProcessors(orderedPostProcessors, beanFactory); + invokeBeanFactoryPostProcessors(orderedPostProcessors, beanFactory); + + // Finally, invoke all other BeanFactoryPostProcessors. + // 最后,调用无序处理器 + List nonOrderedPostProcessors = new ArrayList<>(); + for (String postProcessorName : nonOrderedPostProcessorNames) { + nonOrderedPostProcessors.add(beanFactory.getBean(postProcessorName, BeanFactoryPostProcessor.class)); + } + invokeBeanFactoryPostProcessors(nonOrderedPostProcessors, beanFactory); + + // Clear cached merged bean definitions since the post-processors might have + // modified the original metadata, e.g. replacing placeholders in values... + // 清理元数据 + beanFactory.clearMetadataCache(); +} +``` + +循环遍历 BeanFactoryPostProcessor 中的 postProcessBeanFactory 方法 + +```java +private static void invokeBeanFactoryPostProcessors( + Collection postProcessors, ConfigurableListableBeanFactory beanFactory) { + + for (BeanFactoryPostProcessor postProcessor : postProcessors) { + postProcessor.postProcessBeanFactory(beanFactory); + } +} +``` + +## 注册BeanPostProcessor + +在上文中提到了BeanFactoryPostProcessor的调用,接下来我们就探索下BeanPostProcessor。但这里并不是调用,而是注册,真正的调用其实是在bean的实例化阶段进行的,这是一个很重要的步骤,也是很多功能BeanFactory不知道的重要原因。spring中大部分功能都是通过后处理器的方式进行扩展的,这是spring框架的一个特写,但是在BeanFactory中其实并没有实现后处理器的自动注册,所以在调用的时候如果没有进行手动注册其实是不能使用的。但是ApplicationContext中却添加了自动注册功能,如自定义一个后处理器: + +```java +public class MyInstantiationAwareBeanPostProcessor implements InstantiationAwareBeanPostProcessor { + public Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { + System.out.println("before"); + return null; + } +} +``` + +然后在配置文件中添加bean的配置: + +```xml + +``` + +这样的话再使用BeanFactory的方式进行加载的bean在加载时不会有任何改变的,而在使用ApplicationContext方式获取的bean时就会打印出“before”,而这个特性就是咋registryBeanPostProcessor方法中完成的。 + +我们继续深入分析registryBeanPostProcessors的方法实现: + +```java +protected void registerBeanPostProcessors(ConfigurableListableBeanFactory beanFactory) { + PostProcessorRegistrationDelegate.registerBeanPostProcessors(beanFactory, this); +} +public static void registerBeanPostProcessors( + ConfigurableListableBeanFactory beanFactory, AbstractApplicationContext applicationContext) { + + String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false); + + /* + * BeanPostProcessorChecker是一个普通的信息打印,可能会有些情况当spring的配置中的后 + * 处理器还没有被注册就已经开了bean的初始化,这时就会打印出BeanPostProcessorChecker中 + * 设定的信息 + */ + int beanProcessorTargetCount = beanFactory.getBeanPostProcessorCount() + 1 + postProcessorNames.length; + beanFactory.addBeanPostProcessor(new BeanPostProcessorChecker(beanFactory, beanProcessorTargetCount)); + + //使用PriorityOrdered来保证顺序 + List priorityOrderedPostProcessors = new ArrayList<>(); + List internalPostProcessors = new ArrayList<>(); + //使用Ordered来保证顺序 + List orderedPostProcessorNames = new ArrayList<>(); + //无序BeanPostProcessor + List nonOrderedPostProcessorNames = new ArrayList<>(); + for (String ppName : postProcessorNames) { + if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + priorityOrderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + else if (beanFactory.isTypeMatch(ppName, Ordered.class)) { + orderedPostProcessorNames.add(ppName); + } + else { + nonOrderedPostProcessorNames.add(ppName); + } + } + + //第一步,注册所有实现了PriorityOrdered的BeanPostProcessor + sortPostProcessors(priorityOrderedPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, priorityOrderedPostProcessors); + + //注册实现了Ordered的BeanPostProcessor + List orderedPostProcessors = new ArrayList<>(); + for (String ppName : orderedPostProcessorNames) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + orderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + sortPostProcessors(orderedPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, orderedPostProcessors); + + //注册所有的无序的BeanPostProcessor + List nonOrderedPostProcessors = new ArrayList<>(); + for (String ppName : nonOrderedPostProcessorNames) { + BeanPostProcessor pp = beanFactory.getBean(ppName, BeanPostProcessor.class); + nonOrderedPostProcessors.add(pp); + if (pp instanceof MergedBeanDefinitionPostProcessor) { + internalPostProcessors.add(pp); + } + } + registerBeanPostProcessors(beanFactory, nonOrderedPostProcessors); + + //注册所有的内部BeanFactoryProcessor + sortPostProcessors(internalPostProcessors, beanFactory); + registerBeanPostProcessors(beanFactory, internalPostProcessors); + + // Re-register post-processor for detecting inner beans as ApplicationListeners, + //添加ApplicationListener探测器 + beanFactory.addBeanPostProcessor(new ApplicationListenerDetector(applicationContext)); +} +``` + +我们可以看到先从容器中获取所有类型为 **BeanPostProcessor.class** 的Bean的name数组,然后通过 **BeanPostProcessor pp** **= beanFactory.getBean(ppName, BeanPostProcessor.class****);** 获取Bean的实例,最后通过 **registerBeanPostProcessors(beanFactory, orderedPostProcessors);**将获取到的**BeanPostProcessor**实例添加到容器的属性中,如下 + +```java +private static void registerBeanPostProcessors( + ConfigurableListableBeanFactory beanFactory, List postProcessors) { + + for (BeanPostProcessor postProcessor : postProcessors) { + beanFactory.addBeanPostProcessor(postProcessor); + } +} + +@Override +public void addBeanPostProcessor(BeanPostProcessor beanPostProcessor) { + Assert.notNull(beanPostProcessor, "BeanPostProcessor must not be null"); + // Remove from old position, if any + this.beanPostProcessors.remove(beanPostProcessor); + // Track whether it is instantiation/destruction aware + if (beanPostProcessor instanceof InstantiationAwareBeanPostProcessor) { + this.hasInstantiationAwareBeanPostProcessors = true; + } + if (beanPostProcessor instanceof DestructionAwareBeanPostProcessor) { + this.hasDestructionAwareBeanPostProcessors = true; + } + // Add to end of list + this.beanPostProcessors.add(beanPostProcessor); +} +``` + +可以看到将 **beanPostProcessor 实例添加到容器的** **beanPostProcessors 属性中** + + + +## 初始化Message资源 + +该方法不是很重要,留在以后分析吧。 + + + +## 初始事件广播器 + +初始化ApplicationEventMulticaster是在方法initApplicationEventMulticaster()中实现的,进入到方法体,如下: + +```java +protected void initApplicationEventMulticaster() { + ConfigurableListableBeanFactory beanFactory = getBeanFactory(); + // 1、默认使用内置的事件广播器,如果有的话. + // 我们可以在配置文件中配置Spring事件广播器或者自定义事件广播器 + // 例如: + if (beanFactory.containsLocalBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME)) { + this.applicationEventMulticaster = beanFactory.getBean(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, ApplicationEventMulticaster.class); + } + // 2、否则,新建一个事件广播器,SimpleApplicationEventMulticaster是spring的默认事件广播器 + else { + this.applicationEventMulticaster = new SimpleApplicationEventMulticaster(beanFactory); + beanFactory.registerSingleton(APPLICATION_EVENT_MULTICASTER_BEAN_NAME, this.applicationEventMulticaster); + } +} +``` + +通过源码可以看到其实现逻辑与initMessageSource基本相同,其步骤如下: + +(1)查找是否有name为applicationEventMulticaster的bean,如果有放到容器里,如果没有,初始化一个系统默认的SimpleApplicationEventMulticaster放入容器 + +(2)查找手动设置的applicationListeners,添加到applicationEventMulticaster里 + +(3)查找定义的类型为ApplicationListener的bean,设置到applicationEventMulticaster + +(4)初始化完成、对earlyApplicationEvents里的事件进行通知(此容器仅仅是广播器未建立的时候保存通知信息,一旦容器建立完成,以后均直接通知) + +(5)在系统操作时候,遇到的各种bean的通知事件进行通知 + +可以看到的是applicationEventMulticaster是一个标准的观察者模式,对于他内部的监听者applicationListeners,每次事件到来都会一一获取通知。 + + + +## 注册监听器 + +```java +protected void registerListeners() { + // Register statically specified listeners first. + // 首先,注册指定的静态事件监听器,在spring boot中有应用 + for (ApplicationListener listener : getApplicationListeners()) { + getApplicationEventMulticaster().addApplicationListener(listener); + } + + // Do not initialize FactoryBeans here: We need to leave all regular beans + // uninitialized to let post-processors apply to them! + // 其次,注册普通的事件监听器 + String[] listenerBeanNames = getBeanNamesForType(ApplicationListener.class, true, false); + for (String listenerBeanName : listenerBeanNames) { + getApplicationEventMulticaster().addApplicationListenerBean(listenerBeanName); + } + + // Publish early application events now that we finally have a multicaster... + // 如果有早期事件的话,在这里进行事件广播 + // 因为前期SimpleApplicationEventMulticaster尚未注册,无法发布事件, + // 因此早期的事件会先存放在earlyApplicationEvents集合中,这里把它们取出来进行发布 + // 所以早期事件的发布时间节点是早于其他事件的 + Set earlyEventsToProcess = this.earlyApplicationEvents; + // 早期事件广播器是一个Set集合,保存了无法发布的早期事件,当SimpleApplicationEventMulticaster + // 创建完之后随即进行发布,同事也要将其保存的事件释放 + this.earlyApplicationEvents = null; + if (earlyEventsToProcess != null) { + for (ApplicationEvent earlyEvent : earlyEventsToProcess) { + getApplicationEventMulticaster().multicastEvent(earlyEvent); + } + } +} +``` + +我们来看一下Spring的事件监昕的简单用法 + + + +### 定义监听事件 + +```java +public class TestEvent extends ApplicationonEvent { + public String msg; + public TestEvent (Object source ) { + super (source ); + } + public TestEvent (Object source , String msg ) { + super(source); + this.msg = msg ; + } + public void print () { + System.out.println(msg) ; + } +} +``` + + + +### 定义监昕器 + +```java +public class TestListener implement ApplicationListener { + public void onApplicationEvent (ApplicationEvent event ) { + if (event instanceof TestEvent ) { + TestEvent testEvent = (TestEvent) event ; + testEvent print () ; + } + } +} +``` + + + + + +### 添加配置文件 + +```xml + +``` + +### 测试 + +```java +@Test +public void MyAopTest() { + ApplicationContext ac = new ClassPathXmlApplicationContext("spring-aop.xml"); + TestEvent event = new TestEvent (“hello” ,”msg”) ; + context.publishEvent(event); +} +``` + + + +### 源码分析 + +```java +protected void publishEvent(Object event, ResolvableType eventType) { + Assert.notNull(event, "Event must not be null"); + if (logger.isTraceEnabled()) { + logger.trace("Publishing event in " + getDisplayName() + ": " + event); + } + + // Decorate event as an ApplicationEvent if necessary + ApplicationEvent applicationEvent; + //支持两种事件1、直接继承ApplicationEvent,2、其他时间,会被包装为PayloadApplicationEvent,可以使用getPayload获取真实的通知内容 + if (event instanceof ApplicationEvent) { + applicationEvent = (ApplicationEvent) event; + } + else { + applicationEvent = new PayloadApplicationEvent(this, event); + if (eventType == null) { + eventType = ((PayloadApplicationEvent)applicationEvent).getResolvableType(); + } + } + + // Multicast right now if possible - or lazily once the multicaster is initialized + if (this.earlyApplicationEvents != null) { + //如果有预制行添加到预制行,预制行在执行一次后被置为null,以后都是直接执行 + this.earlyApplicationEvents.add(applicationEvent); + } + else { + //广播event事件 + getApplicationEventMulticaster().multicastEvent(applicationEvent, eventType); + } + + // Publish event via parent context as well... + //父bean同样广播 + if (this.parent != null) { + if (this.parent instanceof AbstractApplicationContext) { + ((AbstractApplicationContext) this.parent).publishEvent(event, eventType); + } + else { + this.parent.publishEvent(event); + } + } +} +``` + +查找所有的监听者,依次遍历,如果有线程池,利用线程池进行发送,如果没有则直接发送,如果针对比较大的并发量,我们应该采用线程池模式,将发送通知和真正的业务逻辑进行分离 + +```java +public void multicastEvent(final ApplicationEvent event, ResolvableType eventType) { + ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event)); + for (final ApplicationListener listener : getApplicationListeners(event, type)) { + Executor executor = getTaskExecutor(); + if (executor != null) { + executor.execute(new Runnable() { + @Override + public void run() { + invokeListener(listener, event); + } + }); + } + else { + invokeListener(listener, event); + } + } +} +``` + +调用invokeListener + +```java +protected void invokeListener(ApplicationListener listener, ApplicationEvent event) { + ErrorHandler errorHandler = getErrorHandler(); + if (errorHandler != null) { + try { + listener.onApplicationEvent(event); + } + catch (Throwable err) { + errorHandler.handleError(err); + } + } + else { + try { + listener.onApplicationEvent(event); + } + catch (ClassCastException ex) { + // Possibly a lambda-defined listener which we could not resolve the generic event type for + LogFactory.getLog(getClass()).debug("Non-matching event type for listener: " + listener, ex); + } + } +} +``` + +初始化其他的单例Bean(非延迟加载的) + +```java +protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) { + // Initialize conversion service for this context. + // 判断有无ConversionService(bean属性类型转换服务接口),并初始化 + if (beanFactory.containsBean(CONVERSION_SERVICE_BEAN_NAME) + && beanFactory.isTypeMatch(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)) { + beanFactory.setConversionService(beanFactory.getBean(CONVERSION_SERVICE_BEAN_NAME, ConversionService.class)); + } + + // + // Register a default embedded value resolver if no bean post-processor + // (such as a PropertyPlaceholderConfigurer bean) registered any before: + // at this point, primarily for resolution in annotation attribute values. + // 如果beanFactory中不包含EmbeddedValueResolver,则向其中添加一个EmbeddedValueResolver + // EmbeddedValueResolver-->解析bean中的占位符和表达式 + if (!beanFactory.hasEmbeddedValueResolver()) { + beanFactory.addEmbeddedValueResolver(strVal -> getEnvironment().resolvePlaceholders(strVal)); + } + + // Initialize LoadTimeWeaverAware beans early to allow for registering their transformers early. + // 初始化LoadTimeWeaverAware类型的bean + // LoadTimeWeaverAware-->加载Spring Bean时织入第三方模块,如AspectJ + String[] weaverAwareNames = beanFactory.getBeanNamesForType(LoadTimeWeaverAware.class, false, false); + for (String weaverAwareName : weaverAwareNames) { + getBean(weaverAwareName); + } + + // Stop using the temporary ClassLoader for type matching. + // 释放临时类加载器 + beanFactory.setTempClassLoader(null); + + // Allow for caching all bean definition metadata, not expecting further changes. + // 冻结缓存的BeanDefinition元数据 + beanFactory.freezeConfiguration(); + + // Instantiate all remaining (non-lazy-init) singletons. + // 初始化其他的非延迟加载的单例bean + beanFactory.preInstantiateSingletons(); +} +``` + + + +我们重点看 **beanFactory.preInstantiateSingletons();** + +```java +@Override +public void preInstantiateSingletons() throws BeansException { + if (logger.isTraceEnabled()) { + logger.trace("Pre-instantiating singletons in " + this); + } + + // Iterate over a copy to allow for init methods which in turn register new bean definitions. + // While this may not be part of the regular factory bootstrap, it does otherwise work fine. + List beanNames = new ArrayList<>(this.beanDefinitionNames); + + // Trigger initialization of all non-lazy singleton beans... + for (String beanName : beanNames) { + RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName); + if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) { + if (isFactoryBean(beanName)) { + Object bean = getBean(FACTORY_BEAN_PREFIX + beanName); + if (bean instanceof FactoryBean) { + final FactoryBean factory = (FactoryBean) bean; + boolean isEagerInit; + if (System.getSecurityManager() != null && factory instanceof SmartFactoryBean) { + isEagerInit = AccessController.doPrivileged((PrivilegedAction) + ((SmartFactoryBean) factory)::isEagerInit, + getAccessControlContext()); + } + else { + isEagerInit = (factory instanceof SmartFactoryBean && + ((SmartFactoryBean) factory).isEagerInit()); + } + if (isEagerInit) { + getBean(beanName); + } + } + } + else { + getBean(beanName); + } + } + } + +} +``` + +完成刷新过程,通知生命周期处理器lifecycleProcessor刷新过程,同时发出ContextRefreshEvent通知 + +```java +protected void finishRefresh() { + // Clear context-level resource caches (such as ASM metadata from scanning). + // 清空资源缓存 + clearResourceCaches(); + + // Initialize lifecycle processor for this context. + // 初始化生命周期处理器 + initLifecycleProcessor(); + + // Propagate refresh to lifecycle processor first. + // 调用生命周期处理器的onRefresh方法 + getLifecycleProcessor().onRefresh(); + + // Publish the final event. + // 推送容器刷新事件 + publishEvent(new ContextRefreshedEvent(this)); + + // Participate in LiveBeansView MBean, if active. + LiveBeansView.registerApplicationContext(this); +} +``` \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean \347\232\204\345\210\235\345\247\213\345\214\226.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean \347\232\204\345\210\235\345\247\213\345\214\226.md" new file mode 100644 index 0000000..87e8a80 --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean \347\232\204\345\210\235\345\247\213\345\214\226.md" @@ -0,0 +1,321 @@ +**正文** + +一个 bean 经历了 `createBeanInstance()` 被创建出来,然后又经过一番属性注入,依赖处理,历经千辛万苦,千锤百炼,终于有点儿 bean 实例的样子,能堪大任了,只需要经历最后一步就破茧成蝶了。这最后一步就是初始化,也就是 `initializeBean()`,所以这篇文章我们分析 `doCreateBean()` 中最后一步:初始化 bean。 +我回到之前的doCreateBean方法中,如下 + +![](http://img.topjavaer.cn/img/202309232326239.png) + +在populateBean方法下面有一个initializeBean(beanName, exposedObject, mbd)方法,这个就是用来执行用户设定的初始化操作。我们看下方法体: + +```java +protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) { + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + // 激活 Aware 方法 + invokeAwareMethods(beanName, bean); + return null; + }, getAccessControlContext()); + } + else { + // 对特殊的 bean 处理:Aware、BeanClassLoaderAware、BeanFactoryAware + invokeAwareMethods(beanName, bean); + } + + Object wrappedBean = bean; + if (mbd == null || !mbd.isSynthetic()) { + // 后处理器 + wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName); + } + + try { + // 激活用户自定义的 init 方法 + invokeInitMethods(beanName, wrappedBean, mbd); + } + catch (Throwable ex) { + throw new BeanCreationException( + (mbd != null ? mbd.getResourceDescription() : null), + beanName, "Invocation of init method failed", ex); + } + if (mbd == null || !mbd.isSynthetic()) { + // 后处理器 + wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName); + } + return wrappedBean; +} +``` + +初始化 bean 的方法其实就是三个步骤的处理,而这三个步骤主要还是根据用户设定的来进行初始化,这三个过程为: + +1. 激活 Aware 方法 +2. 后置处理器的应用 +3. 激活自定义的 init 方法 + +## 激Aware方法 + +我们先了解一下Aware方法的使用。Spring中提供了一些Aware接口,比如BeanFactoryAware,ApplicationContextAware,ResourceLoaderAware,ServletContextAware等,实现这些Aware接口的bean在被初始化后,可以取得一些相对应的资源,例如实现BeanFactoryAware的bean在初始化之后,Spring容器将会注入BeanFactory实例,而实现ApplicationContextAware的bean,在bean被初始化后,将会被注入ApplicationContext实例等。我们先通过示例方法了解下Aware的使用。 + +定义普通bean,如下代码: + +```java +public class HelloBean { + public void say() + { + System.out.println("Hello"); + } +} +``` + +定义beanFactoryAware类型的bean + +```java +public class MyBeanAware implements BeanFactoryAware { + private BeanFactory beanFactory; + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + public void testAware() + { + //通过hello这个bean id从beanFactory获取实例 + HelloBean hello = (HelloBean)beanFactory.getBean("hello"); + hello.say(); + } +} +``` + +进行测试 + +```java +public class Test { + public static void main(String[] args) { + ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); + MyBeanAware test = (MyBeanAware)ctx.getBean("myBeanAware"); + test.testAware(); + } +} + + + + + + + + + + +``` + +输出 + +``` +Hello +``` + +上面的方法我们获取到Spring中BeanFactory,并且可以根据BeanFactory获取所有的bean,以及进行相关设置。还有其他Aware的使用都是大同小异,看一下Spring的实现方式: + +```java +private void invokeAwareMethods(final String beanName, final Object bean) { + if (bean instanceof Aware) { + if (bean instanceof BeanNameAware) { + ((BeanNameAware) bean).setBeanName(beanName); + } + if (bean instanceof BeanClassLoaderAware) { + ((BeanClassLoaderAware) bean).setBeanClassLoader(getBeanClassLoader()); + } + if (bean instanceof BeanFactoryAware) { + ((BeanFactoryAware) bean).setBeanFactory(AbstractAutowireCapableBeanFactory.this); + } + } +} +``` + + + +## 处理器的应用 + +BeanPostPrecessor我们经常看到Spring中使用,这是Spring开放式架构的一个必不可少的亮点,给用户充足的权限去更改或者扩展Spring,而除了BeanPostProcessor外还有很多其他的PostProcessor,当然大部分都以此为基础,集成自BeanPostProcessor。BeanPostProcessor在调用用户自定义初始化方法前或者调用自定义初始化方法后分别会调用BeanPostProcessor的postProcessBeforeInitialization和postProcessAfterinitialization方法,使用户可以根据自己的业务需求就行相应的处理。 + +```java +public Object applyBeanPostProcessorsBeforeInitialization(Object existingBean, String beanName) + throws BeansException { + + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + result = beanProcessor.postProcessBeforeInitialization(result, beanName); + if (result == null) { + return result; + } + } + return result; +} + +public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName) + throws BeansException { + + Object result = existingBean; + for (BeanPostProcessor beanProcessor : getBeanPostProcessors()) { + result = beanProcessor.postProcessAfterInitialization(result, beanName); + if (result == null) { + return result; + } + } + return result; +} +``` + + + +## 激活自定义的init方法 + +客户定制的初始化方法除了我们熟知的使用配置init-method外,还有使自定义的bean实现InitializingBean接口,并在afterPropertiesSet中实现自己的初始化业务逻辑。 + +init-method与afterPropertiesSet都是在初始化bean时执行,执行顺序是afterPropertiesSet先执行,而init-method后执行。 +在invokeInitMethods方法中就实现了这两个步骤的初始化调用。 + +```java +protected void invokeInitMethods(String beanName, final Object bean, @Nullable RootBeanDefinition mbd) + throws Throwable { + + // 是否实现 InitializingBean + // 如果实现了 InitializingBean 接口,则只掉调用bean的 afterPropertiesSet() + boolean isInitializingBean = (bean instanceof InitializingBean); + if (isInitializingBean && (mbd == null || !mbd.isExternallyManagedInitMethod("afterPropertiesSet"))) { + if (logger.isDebugEnabled()) { + logger.debug("Invoking afterPropertiesSet() on bean with name '" + beanName + "'"); + } + if (System.getSecurityManager() != null) { + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> { + ((InitializingBean) bean).afterPropertiesSet(); + return null; + }, getAccessControlContext()); + } + catch (PrivilegedActionException pae) { + throw pae.getException(); + } + } + else { + // 直接调用 afterPropertiesSet() + ((InitializingBean) bean).afterPropertiesSet(); + } + } + + if (mbd != null && bean.getClass() != NullBean.class) { + // 判断是否指定了 init-method(), + // 如果指定了 init-method(),则再调用制定的init-method + String initMethodName = mbd.getInitMethodName(); + if (StringUtils.hasLength(initMethodName) && + !(isInitializingBean && "afterPropertiesSet".equals(initMethodName)) && + !mbd.isExternallyManagedInitMethod(initMethodName)) { + // 利用反射机制执行 + invokeCustomInitMethod(beanName, bean, mbd); + } + } +} +``` + +首先检测当前 bean 是否实现了 InitializingBean 接口,如果实现了则调用其 `afterPropertiesSet()`,然后再检查是否也指定了 `init-method()`,如果指定了则通过反射机制调用指定的 `init-method()`。 + +### init-method() + +```java +public class InitializingBeanTest { + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public void setOtherName(){ + System.out.println("InitializingBeanTest setOtherName..."); + + this.name = "dabin"; + } +} + +// 配置文件 + + + +``` + +执行结果: + +``` +dabin +``` + +我们可以使用 `` 标签的 `default-init-method` 属性来统一指定初始化方法,这样就省了需要在每个 `` 标签中都设置 `init-method` 这样的繁琐工作了。比如在 `default-init-method` 规定所有初始化操作全部以 `initBean()` 命名。如下: + +![](http://img.topjavaer.cn/img/202309232330774.png) + +我们看看 invokeCustomInitMethod 方法: + +```java +protected void invokeCustomInitMethod(String beanName, final Object bean, RootBeanDefinition mbd) + throws Throwable { + + String initMethodName = mbd.getInitMethodName(); + Assert.state(initMethodName != null, "No init method set"); + Method initMethod = (mbd.isNonPublicAccessAllowed() ? + BeanUtils.findMethod(bean.getClass(), initMethodName) : + ClassUtils.getMethodIfAvailable(bean.getClass(), initMethodName)); + + if (initMethod == null) { + if (mbd.isEnforceInitMethod()) { + throw new BeanDefinitionValidationException("Could not find an init method named '" + + initMethodName + "' on bean with name '" + beanName + "'"); + } + else { + if (logger.isTraceEnabled()) { + logger.trace("No default init method named '" + initMethodName + + "' found on bean with name '" + beanName + "'"); + } + // Ignore non-existent default lifecycle methods. + return; + } + } + + if (logger.isTraceEnabled()) { + logger.trace("Invoking init method '" + initMethodName + "' on bean with name '" + beanName + "'"); + } + Method methodToInvoke = ClassUtils.getInterfaceMethodIfPossible(initMethod); + + if (System.getSecurityManager() != null) { + AccessController.doPrivileged((PrivilegedAction) () -> { + ReflectionUtils.makeAccessible(methodToInvoke); + return null; + }); + try { + AccessController.doPrivileged((PrivilegedExceptionAction) () -> + methodToInvoke.invoke(bean), getAccessControlContext()); + } + catch (PrivilegedActionException pae) { + InvocationTargetException ex = (InvocationTargetException) pae.getException(); + throw ex.getTargetException(); + } + } + else { + try { + ReflectionUtils.makeAccessible(initMethod); + initMethod.invoke(bean); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } + } +} +``` + + + +我们看出最后是使用反射的方式来执行初始化方法。 \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\345\210\233\345\273\272AOP\344\273\243\347\220\206\344\271\213\350\216\267\345\217\226\345\242\236\345\274\272\345\231\250.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\345\210\233\345\273\272AOP\344\273\243\347\220\206\344\271\213\350\216\267\345\217\226\345\242\236\345\274\272\345\231\250.md" new file mode 100644 index 0000000..b826341 --- /dev/null +++ "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\345\210\233\345\273\272AOP\344\273\243\347\220\206\344\271\213\350\216\267\345\217\226\345\242\236\345\274\272\345\231\250.md" @@ -0,0 +1,803 @@ +**正文** + +在上一篇的博文中我们讲解了通过自定义配置完成了对AnnotationAwareAspectJAutoProxyCreator类型的自动注册,那么这个类到底做了什么工作来完成AOP的操作呢?首先我们看看AnnotationAwareAspectJAutoProxyCreator的层次结构,如下图所示: + +![](http://img.topjavaer.cn/img/202309262317016.png) + + + +从上图的类层次结构图中我们看到这个类实现了BeanPostProcessor接口,而实现BeanPostProcessor后,当Spring加载这个Bean时会在实例化前调用其postProcesssAfterIntialization方法,而我们对于AOP逻辑的分析也由此开始。 +首先看下其父类AbstractAutoProxyCreator中的postProcessAfterInitialization方法: + +```java +public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) throws BeansException { + if (bean != null) { + //根据给定的bean的class和name构建出个key,格式:beanClassName_beanName + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (!this.earlyProxyReferences.contains(cacheKey)) { + //如果它适合被代理,则需要封装指定bean + return wrapIfNecessary(bean, beanName, cacheKey); + } + } + return bean; +} +``` + +在上面的代码中用到了方法wrapIfNecessary,继续跟踪到方法内部: + +```java +protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + // 如果已经处理过 + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + // 无需增强 + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + // 给定的bean类是否代表一个基础设施类,基础设施类不应代理,或者配置了指定bean不需要自动代理 + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // Create proxy if we have advice. + // 如果存在增强方法则创建代理 + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + // 如果获取到了增强则需要针对增强创建代理 + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + // 创建代理 + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + return proxy; + } + + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; +} +``` + +函数中我们已经看到了代理创建的雏形。当然,真正开始之前还需要经过一些判断,比如是否已经处理过或者是否是需要跳过的bean,而真正创建代理的代码是从getAdvicesAndAdvisorsForBean开始的。 + +创建代理主要包含了两个步骤: + +(1)获取增强方法或者增强器; + +(2)根据获取的增强进行代理。 + +其中逻辑复杂,我们首先来看看获取增强方法的实现逻辑。是在AbstractAdvisorAutoProxyCreator中实现的,代码如下: + +**AbstractAdvisorAutoProxyCreator** + +```java +protected Object[] getAdvicesAndAdvisorsForBean( + Class beanClass, String beanName, @Nullable TargetSource targetSource) { + + List advisors = findEligibleAdvisors(beanClass, beanName); + if (advisors.isEmpty()) { + return DO_NOT_PROXY; + } + return advisors.toArray(); +} +protected List findEligibleAdvisors(Class beanClass, String beanName) { + List candidateAdvisors = findCandidateAdvisors(); + List eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); + extendAdvisors(eligibleAdvisors); + if (!eligibleAdvisors.isEmpty()) { + eligibleAdvisors = sortAdvisors(eligibleAdvisors); + } + return eligibleAdvisors; +} +``` + +对于指定bean的增强方法的获取一定是包含两个步骤的,获取所有的增强以及寻找所有增强中使用于bean的增强并应用,那么**findCandidateAdvisors**与**findAdvisorsThatCanApply**便是做了这两件事情。当然,如果无法找到对应的增强器便返回DO_NOT_PROXY,其中DO_NOT_PROXY=null。 + + + +## 获取增强器 + +由于我们分析的是使用注解进行的AOP,所以对于findCandidateAdvisors的实现其实是由AnnotationAwareAspectJAutoProxyCreator类完成的,我们继续跟踪AnnotationAwareAspectJAutoProxyCreator的findCandidateAdvisors方法。代码如下 + +```java +@Override +protected List findCandidateAdvisors() { + // Add all the Spring advisors found according to superclass rules. + // 当使用注解方式配置AOP的时候并不是丢弃了对XML配置的支持, + // 在这里调用父类方法加载配置文件中的AOP声明 + List advisors = super.findCandidateAdvisors(); + // Build Advisors for all AspectJ aspects in the bean factory. + if (this.aspectJAdvisorsBuilder != null) { + advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors()); + } + return advisors; +} +``` + +AnnotationAwareAspectJAutoProxyCreator间接继承了AbstractAdvisorAutoProxyCreator,在实现获取增强的方法中除了保留父类的获取配置文件中定义的增强外,同时添加了获取Bean的注解增强的功能,那么其实现正是由this.aspectJAdvisorsBuilder.buildAspectJAdvisors()来实现的。 + +在真正研究代码之前读者可以尝试着自己去想象一下解析思路,看看自己的实现与Spring是否有差别呢? + +(1)获取所有beanName,这一步骤中所有在beanFactory中注册的Bean都会被提取出来。 + +(2)遍历所有beanName,并找出声明AspectJ注解的类,进行进一步的处理。 + +(3)对标记为AspectJ注解的类进行增强器的提取。 + +(4)将提取结果加入缓存。 + +```java +public List buildAspectJAdvisors() { + List aspectNames = this.aspectBeanNames; + + if (aspectNames == null) { + synchronized (this) { + aspectNames = this.aspectBeanNames; + if (aspectNames == null) { + List advisors = new ArrayList<>(); + aspectNames = new ArrayList<>(); + // 获取所有的beanName + String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Object.class, true, false); + // 循环所有的beanName找出对应的增强方法 + for (String beanName : beanNames) { + // 不合法的bean则略过,由子类定义规则,默认返回true + if (!isEligibleBean(beanName)) { + continue; + } + // We must be careful not to instantiate beans eagerly as in this case they + // would be cached by the Spring container but would not have been weaved. + // 获取对应的bean的Class类型 + Class beanType = this.beanFactory.getType(beanName); + if (beanType == null) { + continue; + } + // 如果存在Aspect注解 + if (this.advisorFactory.isAspect(beanType)) { + aspectNames.add(beanName); + AspectMetadata amd = new AspectMetadata(beanType, beanName); + if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) { + MetadataAwareAspectInstanceFactory factory = + new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName); + // 解析标记Aspect注解中的增强方法 + List classAdvisors = this.advisorFactory.getAdvisors(factory); + if (this.beanFactory.isSingleton(beanName)) { + //将增强器存入缓存中,下次可以直接取 + this.advisorsCache.put(beanName, classAdvisors); + } + else { + this.aspectFactoryCache.put(beanName, factory); + } + advisors.addAll(classAdvisors); + } + else { + // Per target or per this. + if (this.beanFactory.isSingleton(beanName)) { + throw new IllegalArgumentException("Bean with name '" + beanName + + "' is a singleton, but aspect instantiation model is not singleton"); + } + MetadataAwareAspectInstanceFactory factory = + new PrototypeAspectInstanceFactory(this.beanFactory, beanName); + this.aspectFactoryCache.put(beanName, factory); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + } + this.aspectBeanNames = aspectNames; + return advisors; + } + } + } + + if (aspectNames.isEmpty()) { + return Collections.emptyList(); + } + // 记录在缓存中 + List advisors = new ArrayList<>(); + for (String aspectName : aspectNames) { + List cachedAdvisors = this.advisorsCache.get(aspectName); + if (cachedAdvisors != null) { + advisors.addAll(cachedAdvisors); + } + else { + MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); + advisors.addAll(this.advisorFactory.getAdvisors(factory)); + } + } + return advisors; +} +``` + +至此,我们已经完成了Advisor的提取,在上面的步骤中最为重要也最为繁杂的就是增强器的获取,而这一切功能委托给了getAdvisors方法去实现(this.advisorFactory.getAdvisors(factory))。 + +我们先来看看 **this.advisorFactory.isAspect(beanType)** + +```java +@Override +public boolean isAspect(Class clazz) { + return (hasAspectAnnotation(clazz) && !compiledByAjc(clazz)); +} +private boolean hasAspectAnnotation(Class clazz) { + return (AnnotationUtils.findAnnotation(clazz, Aspect.class) != null); +} + +@Nullable +private static A findAnnotation(Class clazz, Class annotationType, Set visited) { + try { + //判断此Class 是否存在Aspect.class注解 + A annotation = clazz.getDeclaredAnnotation(annotationType); + if (annotation != null) { + return annotation; + } + for (Annotation declaredAnn : getDeclaredAnnotations(clazz)) { + Class declaredType = declaredAnn.annotationType(); + if (!isInJavaLangAnnotationPackage(declaredType) && visited.add(declaredAnn)) { + annotation = findAnnotation(declaredType, annotationType, visited); + if (annotation != null) { + return annotation; + } + } + } + } +} +``` + +如果 bean 存在 Aspect.class注解,就可以获取此bean中的增强器了,接着我们来看看 List classAdvisors = this.advisorFactory.getAdvisors(factory); + +```java +@Override +public List getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) { + // 获取标记为AspectJ的类 + Class aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + // 获取标记为AspectJ的name + String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName(); + validate(aspectClass); + + // We need to wrap the MetadataAwareAspectInstanceFactory with a decorator + // so that it will only instantiate once. + MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory = + new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory); + + List advisors = new ArrayList<>(); + + // 对aspectClass的每一个带有注解的方法进行循环(带有PointCut注解的方法除外),取得Advisor,并添加到集合里。 + // (这是里应该是取得Advice,然后取得我们自己定义的切面类中PointCut,组合成Advisor) + for (Method method : getAdvisorMethods(aspectClass)) { + //将类中的方法封装成Advisor + Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, advisors.size(), aspectName); + if (advisor != null) { + advisors.add(advisor); + } + } + + // If it's a per target aspect, emit the dummy instantiating aspect. + if (!advisors.isEmpty() && lazySingletonAspectInstanceFactory.getAspectMetadata().isLazilyInstantiated()) { + Advisor instantiationAdvisor = new SyntheticInstantiationAdvisor(lazySingletonAspectInstanceFactory); + advisors.add(0, instantiationAdvisor); + } + + // Find introduction fields. + for (Field field : aspectClass.getDeclaredFields()) { + Advisor advisor = getDeclareParentsAdvisor(field); + if (advisor != null) { + advisors.add(advisor); + } + } + + return advisors; +} +``` + + + +### 普通增强器的获取 + +普通增强器的获取逻辑通过getAdvisor方法实现,实现步骤包括对切点的注解的获取以及根据注解信息生成增强。我们先来看看 getAdvisorMethods(aspectClass),这个方法,通过很巧妙的使用接口,定义一个匿名回调,把带有注解的Method都取得出来,放到集合里 + +```java +private List getAdvisorMethods(Class aspectClass) { + final List methods = new LinkedList(); + ReflectionUtils.doWithMethods(aspectClass, new ReflectionUtils.MethodCallback() { + @Override + public void doWith(Method method) throws IllegalArgumentException { + // Exclude pointcuts + // 将没有使用Pointcut.class注解的方法加入到集合中 + if (AnnotationUtils.getAnnotation(method, Pointcut.class) == null) { + methods.add(method); + } + } + }); + Collections.sort(methods, METHOD_COMPARATOR); + return methods; +} + +public static void doWithMethods(Class clazz, MethodCallback mc, @Nullable MethodFilter mf) { + // Keep backing up the inheritance hierarchy. + // 通过反射获取类中所有的方法 + Method[] methods = getDeclaredMethods(clazz); + //遍历所有的方法 + for (Method method : methods) { + if (mf != null && !mf.matches(method)) { + continue; + } + try { + //调用函数体 + mc.doWith(method); + } + catch (IllegalAccessException ex) { + throw new IllegalStateException("Not allowed to access method '" + method.getName() + "': " + ex); + } + } + if (clazz.getSuperclass() != null) { + doWithMethods(clazz.getSuperclass(), mc, mf); + } + else if (clazz.isInterface()) { + for (Class superIfc : clazz.getInterfaces()) { + doWithMethods(superIfc, mc, mf); + } + } +} +``` + +普通增强器的获取逻辑通过 **getAdvisor** 方法来实现,实现步骤包括对切点的注解以及根据注解信息生成增强。 + +```java +public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aif, + int declarationOrderInAspect, String aspectName) { + + validate(aif.getAspectMetadata().getAspectClass()); + // 获取PointCut信息(主要是PointCut里的表达式) + // 把Method对象也传进去的目的是,比较Method对象上的注解,是不是下面注解其中一个 + // 如果不是,返回null;如果是,就把取得PointCut内容包装返回 + // 被比较注解:Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class + AspectJExpressionPointcut ajexp = + getPointcut(candidateAdviceMethod, aif.getAspectMetadata().getAspectClass()); + if (ajexp == null) { + return null; + } + // 根据PointCut信息生成增强器 + return new InstantiationModelAwarePointcutAdvisorImpl( + this, ajexp, aif, candidateAdviceMethod, declarationOrderInAspect, aspectName); +} +``` + +**(1)切点信息的获取** + +所谓获取切点信息就是指定注解的表达式信息的获取,如@Before("test()")。 + +```java +private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { + // 获取方法上的注解 + // 比较Method对象上的注解,是不是下面注解其中一个,如果不是返回null + // 被比较注解:Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class + AspectJAnnotation aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } + // 使用AspectJExpressionPointcut 实例封装获取的信息 + AspectJExpressionPointcut ajexp = new AspectJExpressionPointcut(candidateAspectClass, new String[0], new Class[0]); + + // 提取得到的注解中的表达式如: + // @Pointcut("execution(* test.TestBean.*(..))") + ajexp.setExpression(aspectJAnnotation.getPointcutExpression()); + return ajexp; +} +``` + +详细看下上面方法中使用到的方法findAspectJAnnotationOnMethod + +```java +protected static AspectJAnnotation findAspectJAnnotationOnMethod(Method method) { + // 设置要查找的注解类,看看方法的上注解是不是这些注解其中之一 + Class[] classesToLookFor = new Class[] { + Before.class, Around.class, After.class, AfterReturning.class, AfterThrowing.class, Pointcut.class}; + for (Class c : classesToLookFor) { + AspectJAnnotation foundAnnotation = findAnnotation(method, (Class) c); + if (foundAnnotation != null) { + return foundAnnotation; + } + } + return null; +} +``` + +在上面方法中又用到了方法findAnnotation,继续跟踪代码: + +```java +// 获取指定方法上的注解并使用 AspectJAnnotation 封装 +private static AspectJAnnotation findAnnotation(Method method, Class toLookFor) { + A result = AnnotationUtils.findAnnotation(method, toLookFor); + if (result != null) { + return new AspectJAnnotation(result); + } + else { + return null; + } +} +``` + +此方法的功能是获取指定方法上的注解并使用AspectJAnnotation封装。 + +**(2)根据切点信息生成增强类** + +所有的增强都有Advisor实现类InstantiationModelAwarePontcutAdvisorImpl进行统一封装的。我们看下其构造函数: + +```java +public InstantiationModelAwarePointcutAdvisorImpl(AspectJAdvisorFactory af, AspectJExpressionPointcut ajexp, + MetadataAwareAspectInstanceFactory aif, Method method, int declarationOrderInAspect, String aspectName) { + + this.declaredPointcut = ajexp; + this.method = method; + this.atAspectJAdvisorFactory = af; + this.aspectInstanceFactory = aif; + this.declarationOrder = declarationOrderInAspect; + this.aspectName = aspectName; + + if (aif.getAspectMetadata().isLazilyInstantiated()) { + // Static part of the pointcut is a lazy type. + Pointcut preInstantiationPointcut = + Pointcuts.union(aif.getAspectMetadata().getPerClausePointcut(), this.declaredPointcut); + + // Make it dynamic: must mutate from pre-instantiation to post-instantiation state. + // If it's not a dynamic pointcut, it may be optimized out + // by the Spring AOP infrastructure after the first evaluation. + this.pointcut = new PerTargetInstantiationModelPointcut(this.declaredPointcut, preInstantiationPointcut, aif); + this.lazy = true; + } + else { + // A singleton aspect. + // 初始化Advice + this.instantiatedAdvice = instantiateAdvice(this.declaredPointcut); + this.pointcut = declaredPointcut; + this.lazy = false; + } +} +``` + +通过对上面构造函数的分析,发现封装过程只是简单地将信息封装在类的实例中,所有的额信息单纯地复制。在实例初始化的过程中还完成了对于增强器的初始化。因为不同的增强所体现的逻辑是不同的,比如@Before(“test()”)与After(“test()”)标签的不同就是增强器增强的位置不同,所以就需要不同的增强器来完成不同的逻辑,而根据注解中的信息初始化对应的额增强器就是在instantiateAdvice函数中实现的,继续跟踪代码: + +```java +private Advice instantiateAdvice(AspectJExpressionPointcut pointcut) { + Advice advice = this.aspectJAdvisorFactory.getAdvice(this.aspectJAdviceMethod, pointcut, + this.aspectInstanceFactory, this.declarationOrder, this.aspectName); + return (advice != null ? advice : EMPTY_ADVICE); +} + + @Override +@Nullable +public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, + MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { + + Class candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); + validate(candidateAspectClass); + + AspectJAnnotation aspectJAnnotation = + AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); + if (aspectJAnnotation == null) { + return null; + } + + // If we get here, we know we have an AspectJ method. + // Check that it's an AspectJ-annotated class + if (!isAspect(candidateAspectClass)) { + throw new AopConfigException("Advice must be declared inside an aspect type: " + + "Offending method '" + candidateAdviceMethod + "' in class [" + + candidateAspectClass.getName() + "]"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Found AspectJ method: " + candidateAdviceMethod); + } + + AbstractAspectJAdvice springAdvice; + // 根据不同的注解类型封装不同的增强器 + switch (aspectJAnnotation.getAnnotationType()) { + case AtBefore: + springAdvice = new AspectJMethodBeforeAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtAfter: + springAdvice = new AspectJAfterAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtAfterReturning: + springAdvice = new AspectJAfterReturningAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + AfterReturning afterReturningAnnotation = (AfterReturning) aspectJAnnotation.getAnnotation(); + if (StringUtils.hasText(afterReturningAnnotation.returning())) { + springAdvice.setReturningName(afterReturningAnnotation.returning()); + } + break; + case AtAfterThrowing: + springAdvice = new AspectJAfterThrowingAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + AfterThrowing afterThrowingAnnotation = (AfterThrowing) aspectJAnnotation.getAnnotation(); + if (StringUtils.hasText(afterThrowingAnnotation.throwing())) { + springAdvice.setThrowingName(afterThrowingAnnotation.throwing()); + } + break; + case AtAround: + springAdvice = new AspectJAroundAdvice( + candidateAdviceMethod, expressionPointcut, aspectInstanceFactory); + break; + case AtPointcut: + if (logger.isDebugEnabled()) { + logger.debug("Processing pointcut '" + candidateAdviceMethod.getName() + "'"); + } + return null; + default: + throw new UnsupportedOperationException( + "Unsupported advice type on method: " + candidateAdviceMethod); + } + + // Now to configure the advice... + springAdvice.setAspectName(aspectName); + springAdvice.setDeclarationOrder(declarationOrder); + String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod); + if (argNames != null) { + springAdvice.setArgumentNamesFromStringArray(argNames); + } + springAdvice.calculateArgumentBindings(); + + return springAdvice; +} +``` + +从上述函数代码中可以看到,Spring会根据不同的注解生成不同的增强器,正如代码switch (aspectJAnnotation.getAnnotationType()),根据不同的类型来生成。例如AtBefore会对应AspectJMethodBeforeAdvice。在AspectJMethodBeforeAdvice中完成了增强逻辑,这里的 **AspectJMethodBeforeAdvice 最后会被适配器封装成\**MethodBeforeAdviceInterceptor,\****下一篇文章中我们具体分析,具体看下其代码: + +**MethodBeforeAdviceInterceptor** + +```java +public class MethodBeforeAdviceInterceptor implements MethodInterceptor, Serializable { + + private MethodBeforeAdvice advice; + + public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) { + Assert.notNull(advice, "Advice must not be null"); + this.advice = advice; + } + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); + return mi.proceed(); + } + +} +``` + +其中的MethodBeforeAdvice代表着前置增强的AspectJMethodBeforeAdvice,跟踪before方法: + +**AspectJMethodBeforeAdvice.java** + +```java +public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdvice, Serializable { + + public AspectJMethodBeforeAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + //直接调用增强方法 + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + +} + +protected Object invokeAdviceMethod( + @Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable ex) + throws Throwable { + + return invokeAdviceMethodWithGivenArgs(argBinding(getJoinPoint(), jpMatch, returnValue, ex)); +} + +protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable { + Object[] actualArgs = args; + if (this.aspectJAdviceMethod.getParameterCount() == 0) { + actualArgs = null; + } + try { + ReflectionUtils.makeAccessible(this.aspectJAdviceMethod); + // TODO AopUtils.invokeJoinpointUsingReflection + // 通过反射调用AspectJ注解类中的增强方法 + return this.aspectJAdviceMethod.invoke(this.aspectInstanceFactory.getAspectInstance(), actualArgs); + } + catch (IllegalArgumentException ex) { + throw new AopInvocationException("Mismatch on arguments to advice method [" + + this.aspectJAdviceMethod + "]; pointcut expression [" + + this.pointcut.getPointcutExpression() + "]", ex); + } + catch (InvocationTargetException ex) { + throw ex.getTargetException(); + } +} +``` + +invokeAdviceMethodWithGivenArgs方法中的aspectJAdviceMethod正是对与前置增强的方法,在这里实现了调用。 + +后置增强与前置增强有稍许不一致的地方。回顾之前讲过的前置增强,大致的结构是在拦截器链中放置MethodBeforeAdviceInterceptor,而在MethodBeforeAdviceInterceptor中又放置了AspectJMethodBeforeAdvice,并在调用invoke时首先串联调用。但是在后置增强的时候却不一样,没有提供中间的类,而是直接在拦截器中使用了中间的AspectJAfterAdvice,也就是直接实现了**MethodInterceptor**。 + +```java +public class AspectJAfterAdvice extends AbstractAspectJAdvice + implements MethodInterceptor, AfterAdvice, Serializable { + + public AspectJAfterAdvice( + Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) { + + super(aspectJBeforeAdviceMethod, pointcut, aif); + } + + + @Override + public Object invoke(MethodInvocation mi) throws Throwable { + try { + return mi.proceed(); + } + finally { + // 激活增强方法 + invokeAdviceMethod(getJoinPointMatch(), null, null); + } + } + + @Override + public boolean isBeforeAdvice() { + return false; + } + + @Override + public boolean isAfterAdvice() { + return true; + } + +} +``` + + + + + +## 寻找匹配的增强器 + +前面的函数中已经完成了所有增强器的解析,但是对于所有增强器来讲,并不一定都适用于当前的bean,还要挑取出适合的增强器,也就是满足我们配置的通配符的增强器。具体实现在findAdvisorsThatCanApply中。 + +```java +protected List findAdvisorsThatCanApply( + List candidateAdvisors, Class beanClass, String beanName) { + + ProxyCreationContext.setCurrentProxiedBeanName(beanName); + try { + // 过滤已经得到的advisors + return AopUtils.findAdvisorsThatCanApply(candidateAdvisors, beanClass); + } + finally { + ProxyCreationContext.setCurrentProxiedBeanName(null); + } +} +``` + +继续看findAdvisorsThatCanApply: + +```java +public static List findAdvisorsThatCanApply(List candidateAdvisors, Class clazz) { + if (candidateAdvisors.isEmpty()) { + return candidateAdvisors; + } + List eligibleAdvisors = new ArrayList<>(); + // 首先处理引介增强 + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { + eligibleAdvisors.add(candidate); + } + } + boolean hasIntroductions = !eligibleAdvisors.isEmpty(); + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor) { + // already processed + continue; + } + // 对于普通bean的处理 + if (canApply(candidate, clazz, hasIntroductions)) { + eligibleAdvisors.add(candidate); + } + } + return eligibleAdvisors; +} +``` + +findAdvisorsThatCanApply函数的主要功能是寻找增强器中适用于当前class的增强器。引介增强与普通的增强的处理是不一样的,所以分开处理。而对于真正的匹配在canApply中实现。 + +``` +public static boolean canApply(Advisor advisor, Class targetClass, boolean hasIntroductions) { + if (advisor instanceof IntroductionAdvisor) { + return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); + } + else if (advisor instanceof PointcutAdvisor) { + PointcutAdvisor pca = (PointcutAdvisor) advisor; + return canApply(pca.getPointcut(), targetClass, hasIntroductions); + } + else { + // It doesn't have a pointcut so we assume it applies. + return true; + } +} + +public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) { + Assert.notNull(pc, "Pointcut must not be null"); + //通过Pointcut的条件判断此类是否能匹配 + if (!pc.getClassFilter().matches(targetClass)) { + return false; + } + + MethodMatcher methodMatcher = pc.getMethodMatcher(); + if (methodMatcher == MethodMatcher.TRUE) { + // No need to iterate the methods if we're matching any method anyway... + return true; + } + + IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; + if (methodMatcher instanceof IntroductionAwareMethodMatcher) { + introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; + } + + Set> classes = new LinkedHashSet<>(); + if (!Proxy.isProxyClass(targetClass)) { + classes.add(ClassUtils.getUserClass(targetClass)); + } + classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); + + for (Class clazz : classes) { + //反射获取类中所有的方法 + Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); + for (Method method : methods) { + //根据匹配原则判断该方法是否能匹配Pointcut中的规则,如果有一个方法能匹配,则返回true + if (introductionAwareMethodMatcher != null ? + introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : + methodMatcher.matches(method, targetClass)) { + return true; + } + } + } + + return false; +} +``` + + + +首先判断bean是否满足切点的规则,如果能满足,则获取bean的所有方法,判断是否有方法能匹配规则,有方法匹配规则,就代表此 Advisor 能作用于该bean,然后将该Advisor加入 eligibleAdvisors 集合中。 + +我们以注解的规则来看看bean中的method是怎样匹配 Pointcut中的规则 + +**AnnotationMethodMatcher** + +```java +@Override +public boolean matches(Method method, Class targetClass) { + if (matchesMethod(method)) { + return true; + } + // Proxy classes never have annotations on their redeclared methods. + if (Proxy.isProxyClass(targetClass)) { + return false; + } + // The method may be on an interface, so let's check on the target class as well. + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + return (specificMethod != method && matchesMethod(specificMethod)); +} + +private boolean matchesMethod(Method method) { + //可以看出判断该Advisor是否使用于bean中的method,只需看method上是否有Advisor的注解 + return (this.checkInherited ? AnnotatedElementUtils.hasAnnotation(method, this.annotationType) : + method.isAnnotationPresent(this.annotationType)); +} +``` + + + +至此,我们在后置处理器中找到了所有匹配Bean中的增强器,下一篇讲解如何使用找到切面,来创建代理。 + diff --git "a/docs/zsxq/question/\346\200\216\346\240\267\345\207\206\345\244\207\346\211\215\350\203\275\346\211\276\345\210\260\344\270\200\344\273\275\345\256\236\344\271\240\345\267\245\344\275\234\357\274\237.md" "b/docs/zsxq/question/\346\200\216\346\240\267\345\207\206\345\244\207\346\211\215\350\203\275\346\211\276\345\210\260\344\270\200\344\273\275\345\256\236\344\271\240\345\267\245\344\275\234\357\274\237.md" new file mode 100644 index 0000000..9714a26 --- /dev/null +++ "b/docs/zsxq/question/\346\200\216\346\240\267\345\207\206\345\244\207\346\211\215\350\203\275\346\211\276\345\210\260\344\270\200\344\273\275\345\256\236\344\271\240\345\267\245\344\275\234\357\274\237.md" @@ -0,0 +1,39 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【如何找实习工作】的提问。 + + + +**球友提问**: + +目前状况:学校是**双非一本**,目前阶段是大三上开始,9月份中旬提问。 然后希望能在十月,十一月份的时候**找一份寒假时期实习或者日常实习**,希望能够为简历添加一份实习经历,也为了熟悉面试流程,为明年秋招做好准备。 + +提问目的:大彬哥你好,麻烦大彬哥帮我看看,我目前的**个人总结**,麻烦你针对性的给出目前我和实习的差距,因为时间就这一两个月,想知道具体准备哪些部分才能找到一份实习。问题很多,麻烦大彬哥了! + +然后这里看图片。 + +![](http://img.topjavaer.cn/img/202309280816580.png) + + 最后提问 + +1. 麻烦大彬哥根据以上,针对性的给出我目前和寒假日常实习的**差距**,目标是通过面试,因为时间就这一两个月,想知道具体**重点准备**哪些部分才能找到一份实习。 +2. 如果有机会还是希望能去大厂实习,是否差距是太大了? +3. 因为没有突出优势,基础部分没有系统学习如计网,操作系统,**只背面试题**够用吗。 +4. 项目部分还要学习spring boot和redis和spring cloud 这些是必要的吗,学完这些做项目会不会太慢了。如果和大厂太遥远,目标放在小厂的话哪些知识点是重点,哪些是可以先放放的。 + +--- + +**大彬的回答**: + +学弟你好。 + +1. 按照你目前的情况,想要一份实习工作,之前需要补充学习这些内容:Redis,SpringBoot,微服务,消息队列,Nginx,至少一个项目。如果时间比较紧的话,建议刚开始学**先掌握基本的api用法**,等到后面有时间再去深入学习、看源码。 +2. 关于项目,很多同学刚开始时,总想自己从零开始敲代码,或者以从零开始搭建一个项目为学习目标。其实刚开始学的时候,会遇到很多坑,比如一个分号一个单词拼错都可能会导致卡进展,从而影响到学习效率和学习积极性。正确的做法是,先运行跑通现有代码,运行时通过结果理解关键性语法和技能点,然后再尝试修改人家的代码看结果,这样就能达到边学边进步的效果。 +3. 做什么项目呢?可以参考星球**置顶帖**分享的一些优质项目(https://t.zsxq.com/12mxN9kMJ) + +2. 想进大厂的话,力扣算法之前刷300道左右,**计算机基础要扎实**,计算机网络、操作系统这些单靠背面试题是不够的,需要花时间去看看书,系统学习一下。 + +3. SpringBoot、Redis、Springcloud这些有必要学吗?如第一点所说的,肯定要学的,这些都是Java开发必备的知识了,其实学习简单使用还是挺快的,原理那些可以后面有时间在捡起来 + +4. **建议你先冲一下中小厂**,后面还有暑期实习和秋招,进大厂还是有机会的。优先把上面提到的那些知识点先学了,找个项目做做,然后准备下面试题和力扣(中小厂力扣前200就够了) +5. 一定别只学技术,也要去“背“面试题。建议把[**星球**](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)的**面试手册**(在**星球置顶帖**:**https://t.zsxq.com/12mxN9kMJ**)过一遍,基本包括绝大部分常考面试题了。实习岗位不可能因为你技术到位自己跑过来,而是要你通过面试证明你的能力才能争取到,所以掌握面试技巧也很重要。面试前先找小公司练练手,亲历面试,并在面试中进一步学习面试技巧、查漏补缺。 \ No newline at end of file From 83b2e652ec890105117e682bc4ecad1ad4f2a7f2 Mon Sep 17 00:00:00 2001 From: dabin <1713476357@qq.com> Date: Thu, 23 May 2024 10:41:08 +0800 Subject: [PATCH 4/8] update --- docs/.vuepress/navbar.ts | 48 + docs/README.md | 12 + .../excellent-article/10-file-upload.md | 4 +- ...15\346\226\271\345\274\217\357\274\201.md" | 430 ++++ .../2-order-timeout-auto-cancel.md | 14 +- docs/advance/system-design/README.md | 6 +- docs/campus-recruit/interview/3-baidu.md | 22 +- docs/campus-recruit/interview/4-ali.md | 24 +- docs/campus-recruit/interview/5-kuaishou.md | 24 +- docs/campus-recruit/interview/6-meituan.md | 9 + docs/computer-basic/network.md | 4 +- docs/database/mysql.md | 16 + docs/framework/mybatis.md | 13 +- docs/framework/spring.md | 22 + docs/framework/springboot.md | 66 + docs/framework/springcloud-interview.md | 53 +- docs/framework/springmvc.md | 190 +- docs/java/java-basic.md | 45 +- docs/java/java-collection.md | 4 +- docs/java/jvm.md | 6 +- docs/other/site-diary.md | 4 + docs/redis/redis.md | 39 +- ...32\346\216\245\345\217\243\345\261\202.md" | 266 +++ docs/source/mybatis/1-overview.md | 43 + docs/source/mybatis/2-reflect.md | 634 ++++++ ...57\346\214\201\346\250\241\345\235\227.md" | 1563 +++++++++++++ ...15\347\275\256\350\247\243\346\236\220.md" | 609 +++++ ...15\347\275\256\350\247\243\346\236\220.md" | 641 ++++++ ...06--statement \350\247\243\346\236\220.md" | 1382 ++++++++++++ ...--\346\211\247\350\241\214\345\231\250.md" | 765 +++++++ docs/source/spring-mvc/1-overview.md | 1444 ++++++++++++ docs/source/spring-mvc/2-guide.md | 93 + docs/source/spring-mvc/3-scene.md | 1982 +++++++++++++++++ .../spring-mvc/4-fileupload-interceptor.md | 631 ++++++ .../source/spring/1-architect.md | 15 + .../source/spring/10-bean-initial.md | 27 + .../source/spring/11-application-refresh.md | 41 +- .../source/spring/12-aop-custom-tag.md | 43 +- .../source/spring/13-aop-proxy-advisor.md | 44 +- .../source/spring/14-aop-proxy-create.md | 41 +- .../source/spring/15-aop-advice-create.md | 43 +- .../source/spring/16-transactional.md | 45 +- .../spring/17-spring-transaction-aop.md | 687 ++++++ docs/source/spring/18-transaction-advice.md | 840 +++++++ .../spring/19-transaction-rollback-commit.md | 975 ++++++++ .../source/spring/2-ioc-overview.md | 15 + .../source/spring/3-ioc-tag-parse-1.md | 17 +- .../source/spring/4-ioc-tag-parse-2.md | 16 + .../source/spring/5-ioc-tag-custom.md.md | 16 + .../source/spring/6-bean-load.md | 21 +- .../source/spring/7-bean-build.md | 16 + .../source/spring/8-ioc-attribute-fill.md | 27 + .../spring/9-ioc-circular-dependency.md | 26 + ...64\344\275\223\346\236\266\346\236\204.md" | 73 - docs/system-design/README.md | 2 +- docs/zsxq/article/site-hack.md | 84 + ...45\351\203\275\346\262\241\345\271\262.md" | 95 + docs/zsxq/inner-material.md | 2 +- docs/zsxq/introduce.md | 36 +- docs/zsxq/mianshishouce.md | 2 +- ...\344\271\246vs\351\230\277\351\207\214.md" | 63 + ...72\345\233\275\350\257\273\347\240\224.md" | 40 + ...02\344\275\225\350\260\210\350\226\252.md" | 42 + ...20\345\215\207\346\212\200\346\234\257.md" | 38 + ...02\345\216\273\345\244\226\345\214\205.md" | 70 + ...56\351\242\230\346\261\207\346\200\273.md" | 116 + ...350\256\255\347\217\255\344\272\206....md" | 44 + ...63\350\241\245\345\237\272\347\241\200.md" | 40 + ...67\345\201\267\345\215\267\357\274\201.md" | 95 + ...25\351\242\230\345\272\223\344\272\206.md" | 34 + ...30\345\205\261\344\272\253\347\276\244.md" | 59 + 71 files changed, 14683 insertions(+), 315 deletions(-) create mode 100644 "docs/advance/excellent-article/\345\256\236\347\216\260\345\274\202\346\255\245\347\274\226\347\250\213\357\274\214\346\210\221\346\234\211\345\205\253\347\247\215\346\226\271\345\274\217\357\274\201.md" create mode 100644 "docs/source/mybatis/# MyBatis \346\272\220\347\240\201\345\210\206\346\236\220\357\274\210\344\270\203\357\274\211\357\274\232\346\216\245\345\217\243\345\261\202.md" create mode 100644 docs/source/mybatis/1-overview.md create mode 100644 docs/source/mybatis/2-reflect.md create mode 100644 "docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2203--\345\237\272\347\241\200\346\224\257\346\214\201\346\250\241\345\235\227.md" create mode 100644 "docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2204--\350\277\220\350\241\214\346\227\266\351\205\215\347\275\256\350\247\243\346\236\220.md" create mode 100644 "docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2205--Mapper \351\200\232\347\224\250\351\205\215\347\275\256\350\247\243\346\236\220.md" create mode 100644 "docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2206--statement \350\247\243\346\236\220.md" create mode 100644 "docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2208--\346\211\247\350\241\214\345\231\250.md" create mode 100644 docs/source/spring-mvc/1-overview.md create mode 100644 docs/source/spring-mvc/2-guide.md create mode 100644 docs/source/spring-mvc/3-scene.md create mode 100644 docs/source/spring-mvc/4-fileupload-interceptor.md rename "docs/source/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" => docs/source/spring/1-architect.md (91%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean \347\232\204\345\210\235\345\247\213\345\214\226.md" => docs/source/spring/10-bean-initial.md (91%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224ApplicationContext\345\256\271\345\231\250refresh\350\277\207\347\250\213.md" => docs/source/spring/11-application-refresh.md (96%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\232\204\344\275\277\347\224\250\345\217\212AOP\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276.md" => docs/source/spring/12-aop-custom-tag.md (88%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\345\210\233\345\273\272AOP\344\273\243\347\220\206\344\271\213\350\216\267\345\217\226\345\242\236\345\274\272\345\231\250.md" => docs/source/spring/13-aop-proxy-advisor.md (94%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 AOP\344\273\243\347\220\206\347\232\204\347\224\237\346\210\220.md" => docs/source/spring/14-aop-proxy-create.md (94%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\233\256\346\240\207\346\226\271\346\263\225\345\222\214\345\242\236\345\274\272\346\226\271\346\263\225\347\232\204\346\211\247\350\241\214.md" => docs/source/spring/15-aop-advice-create.md (82%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224@Transactional\346\263\250\350\247\243\347\232\204\345\243\260\346\230\216\345\274\217\344\272\213\347\211\251\344\273\213\347\273\215.md" => docs/source/spring/16-transactional.md (90%) create mode 100644 docs/source/spring/17-spring-transaction-aop.md create mode 100644 docs/source/spring/18-transaction-advice.md create mode 100644 docs/source/spring/19-transaction-rollback-commit.md rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC \345\256\271\345\231\250\345\237\272\346\234\254\345\256\236\347\216\260.md" => docs/source/spring/2-ioc-overview.md (99%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\212\357\274\211.md" => docs/source/spring/3-ioc-tag-parse-1.md (98%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\213\357\274\211.md" => docs/source/spring/4-ioc-tag-parse-2.md (98%) rename "docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276\350\247\243\346\236\220.md" => docs/source/spring/5-ioc-tag-custom.md.md (98%) rename "docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC-\345\274\200\345\220\257 bean \347\232\204\345\212\240\350\275\275.md" => docs/source/spring/6-bean-load.md (97%) rename "docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean\345\210\233\345\273\272.md" => docs/source/spring/7-bean-build.md (98%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\345\261\236\346\200\247\345\241\253\345\205\205.md" => docs/source/spring/8-ioc-attribute-fill.md (95%) rename "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\345\276\252\347\216\257\344\276\235\350\265\226\345\244\204\347\220\206.md" => docs/source/spring/9-ioc-circular-dependency.md (90%) delete mode 100644 "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" create mode 100644 docs/zsxq/article/site-hack.md create mode 100644 "docs/zsxq/article/\346\257\217\345\271\264\345\210\260\345\271\264\345\272\225\346\200\273\347\273\223\345\267\245\344\275\234\350\277\260\350\201\214\347\232\204\346\227\266\345\200\231\346\204\237\350\247\211\350\207\252\345\267\261\345\245\275\345\203\217\345\225\245\351\203\275\346\262\241\345\271\262.md" create mode 100644 "docs/zsxq/question/offer\351\200\211\346\213\251\357\274\232\345\260\217\347\272\242\344\271\246vs\351\230\277\351\207\214.md" create mode 100644 "docs/zsxq/question/\344\272\214\346\234\254\345\255\246\345\216\206\357\274\214\346\203\263\345\207\272\345\233\275\350\257\273\347\240\224.md" create mode 100644 "docs/zsxq/question/\345\246\202\344\275\225\350\260\210\350\226\252.md" create mode 100644 "docs/zsxq/question/\346\203\263\350\267\263\346\247\275\350\277\233\345\244\247\345\216\202\357\274\214\346\200\216\344\271\210\346\217\220\345\215\207\346\212\200\346\234\257.md" create mode 100644 "docs/zsxq/question/\346\224\276\345\274\203\345\244\247\345\216\202\345\216\273\345\244\226\345\214\205.md" create mode 100644 "docs/zsxq/question/\346\230\237\347\220\203\346\212\200\346\234\257\351\227\256\351\242\230\346\261\207\346\200\273.md" create mode 100644 "docs/zsxq/question/\347\247\213\346\213\2330offer\357\274\214\346\211\223\347\256\227\345\216\273\344\270\212\345\237\271\350\256\255\347\217\255\344\272\206....md" create mode 100644 "docs/zsxq/question/\351\235\236\347\247\221\347\217\255\357\274\214\346\203\263\350\241\245\345\237\272\347\241\200.md" create mode 100644 "docs/zsxq/\346\230\245\346\213\233\346\235\245\344\272\206\357\274\214\345\244\247\345\256\266\351\203\275\345\234\250\345\201\267\345\201\267\345\215\267\357\274\201.md" create mode 100644 "docs/zsxq/\350\277\231\345\217\257\350\203\275\346\230\257\346\234\200\345\205\250\346\260\221\347\232\204\351\235\242\350\257\225\351\242\230\345\272\223\344\272\206.md" create mode 100644 "docs/zsxq/\351\235\242\350\257\225\347\234\237\351\242\230\345\205\261\344\272\253\347\276\244.md" diff --git a/docs/.vuepress/navbar.ts b/docs/.vuepress/navbar.ts index 6713299..d2fcdc3 100644 --- a/docs/.vuepress/navbar.ts +++ b/docs/.vuepress/navbar.ts @@ -215,6 +215,54 @@ export default navbar([ }, ] }, + + { + text: "源码解读", + icon: "source", + children: [ + { + text: "Spring", + children: [ + {text: "整体架构", link: "/source/spring/1-architect.md", icon: "book"}, + {text: "IOC 容器基本实现", link: "/source/spring/2-ioc-overview", icon: "book"}, + {text: "IOC默认标签解析(上)", link: "/source/spring/3-ioc-tag-parse-1", icon: "book"}, + {text: "IOC默认标签解析(下)", link: "/source/spring/4-ioc-tag-parse-2", icon: "book"}, + {text: "IOC之自定义标签解析", link: "/source/spring/5-ioc-tag-custom.md", icon: "book"}, + {text: "IOC-开启 bean 的加载", link: "/source/spring/6-bean-load", icon: "book"}, + {text: "IOC之bean创建", link: "/source/spring/7-bean-build", icon: "book"}, + {text: "IOC属性填充", link: "/source/spring/8-ioc-attribute-fill", icon: "book"}, + {text: "IOC之循环依赖处理", link: "/source/spring/9-ioc-circular-dependency", icon: "book"}, + {text: "IOC之bean 的初始化", link: "/source/spring/10-bean-initial", icon: "book"}, + {text: "ApplicationContext容器refresh过程", link: "/source/spring/11-application-refresh", icon: "book"}, + {text: "AOP的使用及AOP自定义标签", link: "/source/spring/12-aop-custom-tag", icon: "book"}, + {text: "创建AOP代理之获取增强器", link: "/source/spring/13-aop-proxy-advisor", icon: "book"}, + {text: "AOP代理的生成", link: "/source/spring/14-aop-proxy-create", icon: "book"}, + {text: "AOP目标方法和增强方法的执行", link: "/source/spring/15-aop-advice-create", icon: "book"}, + {text: "@Transactional注解的声明式事物介绍", link: "/source/spring/16-transactional", icon: "book"}, + {text: "Spring事务是怎么通过AOP实现的?", link: "/source/spring/17-spring-transaction-aop", icon: "book"}, + {text: "事务增强器", link: "/source/spring/18-transaction-advice", icon: "book"}, + {text: "事务的回滚和提交", link: "/source/spring/19-transaction-rollback-commit", icon: "book"}, + ] + }, + { + text: "SpringMVC", + children: [ + {text: "文件上传和拦截器", link: "/source/spring-mvc/1-overview", icon: "book"}, + {text: "导读篇", link: "/source/spring-mvc/2-guide", icon: "book"}, + {text: "场景分析", link: "/source/spring-mvc/3-scene", icon: "book"}, + {text: "事务的回滚和提交", link: "/source/spring-mvc/4-fileupload-interceptor", icon: "book"}, + ] + }, + { + text: "MyBatis(更新中)", + children: [ + {text: "整体架构", link: "/source/mybatis/1-overview", icon: "book"}, + {text: "反射模块", link: "/source/mybatis/2-reflect", icon: "book"}, + ] + }, + + ] + }, //{ // text: "场景题", // icon: "design", diff --git a/docs/README.md b/docs/README.md index f38c835..4b49ed1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -55,6 +55,18 @@ projects: 本网站所有内容已经汇总成**PDF电子版**,**PDF电子版**在我的[**学习圈**](zsxq/introduce.md)可以获取~ +## 计算机经典书籍PDF + +给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 + ## 学习路线 ![](http://img.topjavaer.cn/img/20220530232715.png) diff --git a/docs/advance/excellent-article/10-file-upload.md b/docs/advance/excellent-article/10-file-upload.md index f483d02..0064765 100644 --- a/docs/advance/excellent-article/10-file-upload.md +++ b/docs/advance/excellent-article/10-file-upload.md @@ -345,9 +345,7 @@ public abstract class SliceUploadTemplate implements SliceUploadStrategy { 本示例代码在电脑配置为4核内存8G情况下,上传24G大小的文件,上传时间需要30多分钟,主要时间耗费在前端的**md5**值计算,后端写入的速度还是比较快。 -如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的oss服务器,其介绍可以查看官网: - -> https://help.aliyun.com/product/31815.html +如果项目组觉得自建文件服务器太花费时间,且项目的需求仅仅只是上传下载,那么推荐使用阿里的oss服务器。 阿里的oss它本质是一个对象存储服务器,而非文件服务器,因此如果有涉及到大量删除或者修改文件的需求,oss可能就不是一个好的选择。 diff --git "a/docs/advance/excellent-article/\345\256\236\347\216\260\345\274\202\346\255\245\347\274\226\347\250\213\357\274\214\346\210\221\346\234\211\345\205\253\347\247\215\346\226\271\345\274\217\357\274\201.md" "b/docs/advance/excellent-article/\345\256\236\347\216\260\345\274\202\346\255\245\347\274\226\347\250\213\357\274\214\346\210\221\346\234\211\345\205\253\347\247\215\346\226\271\345\274\217\357\274\201.md" new file mode 100644 index 0000000..92ca41f --- /dev/null +++ "b/docs/advance/excellent-article/\345\256\236\347\216\260\345\274\202\346\255\245\347\274\226\347\250\213\357\274\214\346\210\221\346\234\211\345\205\253\347\247\215\346\226\271\345\274\217\357\274\201.md" @@ -0,0 +1,430 @@ +# 实现异步编程,我有八种方式! + +## **一、前言** + +> 异步执行对于开发者来说并不陌生,在实际的开发过程中,很多场景多会使用到异步,相比同步执行,异步可以大大缩短请求链路耗时时间,比如:**发送短信、邮件、异步更新等**,这些都是典型的可以通过异步实现的场景。 + +## **二、异步的八种实现方式** + +1. 线程Thread +2. Future +3. 异步框架`CompletableFuture` +4. Spring注解@Async +5. Spring `ApplicationEvent`事件 +6. 消息队列 +7. 第三方异步框架,比如Hutool的`ThreadUtil` +8. Guava异步 + +## **三、什么是异步?** + +首先我们先看一个常见的用户下单的场景: + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/1Wxib6Z0MOJZ43bIQGgKEIK4G1LTnyzHah6HIeYkttOlF0y1ia4PMKjqdDWibtnJv1pBQIDP1vW4OpB8Fm5xEb5uw/640?wx_fmt=jpeg&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) + +在同步操作中,我们执行到 **发送短信** 的时候,我们必须等待这个方法彻底执行完才能执行 **赠送积分** 这个操作,如果 **赠送积分** 这个动作执行时间较长,发送短信需要等待,这就是典型的同步场景。 + +实际上,发送短信和赠送积分没有任何的依赖关系,通过异步,我们可以实现`赠送积分`和`发送短信`这两个操作能够同时进行,比如: + +![图片](https://mmbiz.qpic.cn/mmbiz_jpg/1Wxib6Z0MOJZ43bIQGgKEIK4G1LTnyzHa844UkQPwo59jkAYiaQ8RUYIxUpRGk99HehD2nAXvRx6aGZchRveICDA/640?wx_fmt=jpeg&tp=wxpic&wxfrom=5&wx_lazy=1&wx_co=1) + +这就是所谓的异步,是不是非常简单,下面就说说异步的几种实现方式吧。 + +## **四、异步编程** + +#### **4.1 线程异步** + +```java +public class AsyncThread extends Thread { + + @Override + public void run() { + System.out.println("Current thread name:" + Thread.currentThread().getName() + " Send email success!"); + } + + public static void main(String[] args) { + AsyncThread asyncThread = new AsyncThread(); + asyncThread.run(); + } +} +``` + +当然如果每次都创建一个`Thread`线程,频繁的创建、销毁,浪费系统资源,我们可以采用线程池: + +```java +private ExecutorService executorService = Executors.newCachedThreadPool(); + +public void fun() { + executorService.submit(new Runnable() { + @Override + public void run() { + log.info("执行业务逻辑..."); + } + }); +} +``` + +可以将业务逻辑封装到`Runnable`或`Callable`中,交由线程池来执行。 + +#### **4.2 Future异步** + +```java +@Slf4j +public class FutureManager { + + public String execute() throws Exception { + + ExecutorService executor = Executors.newFixedThreadPool(1); + Future future = executor.submit(new Callable() { + @Override + public String call() throws Exception { + + System.out.println(" --- task start --- "); + Thread.sleep(3000); + System.out.println(" --- task finish ---"); + return "this is future execute final result!!!"; + } + }); + + //这里需要返回值时会阻塞主线程 + String result = future.get(); + log.info("Future get result: {}", result); + return result; + } + + @SneakyThrows + public static void main(String[] args) { + FutureManager manager = new FutureManager(); + manager.execute(); + } +} +``` + +输出结果: + +``` + --- task start --- + --- task finish --- + Future get result: this is future execute final result!!! +``` + +##### **4.2.1 Future的不足之处** + +Future的不足之处的包括以下几点: + +1️⃣ 无法被动接收异步任务的计算结果:虽然我们可以主动将异步任务提交给线程池中的线程来执行,但是待异步任务执行结束之后,主线程无法得到任务完成与否的通知,它需要通过get方法主动获取任务执行的结果。 + +2️⃣ Future件彼此孤立:有时某一个耗时很长的异步任务执行结束之后,你想利用它返回的结果再做进一步的运算,该运算也会是一个异步任务,两者之间的关系需要程序开发人员手动进行绑定赋予,Future并不能将其形成一个任务流(pipeline),每一个Future都是彼此之间都是孤立的,所以才有了后面的`CompletableFuture`,`CompletableFuture`就可以将多个Future串联起来形成任务流。 + +3️⃣ Futrue没有很好的错误处理机制:截止目前,如果某个异步任务在执行发的过程中发生了异常,调用者无法被动感知,必须通过捕获get方法的异常才知晓异步任务执行是否出现了错误,从而在做进一步的判断处理。 + +#### **4.3 CompletableFuture实现异步** + +```java +public class CompletableFutureCompose { + + /** + * thenAccept子任务和父任务公用同一个线程 + */ + @SneakyThrows + public static void thenRunAsync() { + CompletableFuture cf1 = CompletableFuture.supplyAsync(() -> { + System.out.println(Thread.currentThread() + " cf1 do something...."); + return 1; + }); + CompletableFuture cf2 = cf1.thenRunAsync(() -> { + System.out.println(Thread.currentThread() + " cf2 do something..."); + }); + //等待任务1执行完成 + System.out.println("cf1结果->" + cf1.get()); + //等待任务2执行完成 + System.out.println("cf2结果->" + cf2.get()); + } + + public static void main(String[] args) { + thenRunAsync(); + } +} +``` + +我们不需要显式使用`ExecutorService,CompletableFuture `内部使用了`ForkJoinPool`来处理异步任务,如果在某些业务场景我们想自定义自己的异步线程池也是可以的。 + +#### **4.4 Spring的@Async异步** + +##### **4.4.1 自定义异步线程池** + +``` +/** + * 线程池参数配置,多个线程池实现线程池隔离,@Async注解,默认使用系统自定义线程池,可在项目中设置多个线程池,在异步调用的时候,指明需要调用的线程池名称,比如:@Async("taskName") + * + * @author: jacklin + * @since: 2021/5/18 11:44 + **/ +@EnableAsync +@Configuration +public class TaskPoolConfig { + + /** + * 自定义线程池 + * + * @author: jacklin + * @since: 2021/11/16 17:41 + **/ + @Bean("taskExecutor") + public Executor taskExecutor() { + //返回可用处理器的Java虚拟机的数量 12 + int i = Runtime.getRuntime().availableProcessors(); + System.out.println("系统最大线程数 : " + i); + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + //核心线程池大小 + executor.setCorePoolSize(16); + //最大线程数 + executor.setMaxPoolSize(20); + //配置队列容量,默认值为Integer.MAX_VALUE + executor.setQueueCapacity(99999); + //活跃时间 + executor.setKeepAliveSeconds(60); + //线程名字前缀 + executor.setThreadNamePrefix("asyncServiceExecutor -"); + //设置此执行程序应该在关闭时阻止的最大秒数,以便在容器的其余部分继续关闭之前等待剩余的任务完成他们的执行 + executor.setAwaitTerminationSeconds(60); + //等待所有的任务结束后再关闭线程池 + executor.setWaitForTasksToCompleteOnShutdown(true); + return executor; + } +} +``` + +##### **4.4.2 AsyncService** + +```java +public interface AsyncService { + + MessageResult sendSms(String callPrefix, String mobile, String actionType, String content); + + MessageResult sendEmail(String email, String subject, String content); +} + +@Slf4j +@Service +public class AsyncServiceImpl implements AsyncService { + + @Autowired + private IMessageHandler mesageHandler; + + @Override + @Async("taskExecutor") + public MessageResult sendSms(String callPrefix, String mobile, String actionType, String content) { + try { + + Thread.sleep(1000); + mesageHandler.sendSms(callPrefix, mobile, actionType, content); + + } catch (Exception e) { + log.error("发送短信异常 -> ", e) + } + } + + + @Override + @Async("taskExecutor") + public sendEmail(String email, String subject, String content) { + try { + + Thread.sleep(1000); + mesageHandler.sendsendEmail(email, subject, content); + + } catch (Exception e) { + log.error("发送email异常 -> ", e) + } + } +} +``` + +在实际项目中, 使用`@Async`调用线程池,推荐等方式是是使用自定义线程池的模式,不推荐直接使用@Async直接实现异步。 + +#### **4.5 Spring ApplicationEvent事件实现异步** + +##### **4.5.1 定义事件** + +```java +public class AsyncSendEmailEvent extends ApplicationEvent { + + /** + * 邮箱 + **/ + private String email; + + /** + * 主题 + **/ + private String subject; + + /** + * 内容 + **/ + private String content; + + /** + * 接收者 + **/ + private String targetUserId; + +} +``` + +##### **4.5.2 定义事件处理器** + +```java +@Slf4j +@Component +public class AsyncSendEmailEventHandler implements ApplicationListener { + + @Autowired + private IMessageHandler mesageHandler; + + @Async("taskExecutor") + @Override + public void onApplicationEvent(AsyncSendEmailEvent event) { + if (event == null) { + return; + } + + String email = event.getEmail(); + String subject = event.getSubject(); + String content = event.getContent(); + String targetUserId = event.getTargetUserId(); + mesageHandler.sendsendEmailSms(email, subject, content, targerUserId); + } +} +``` + +另外,可能有些时候采用ApplicationEvent实现异步的使用,当程序出现异常错误的时候,需要考虑补偿机制,那么这时候可以结合Spring Retry重试来帮助我们避免这种异常造成数据不一致问题。 + +#### **4.6 消息队列** + +##### **4.6.1 回调事件消息生产者** + +``` +@Slf4j +@Component +public class CallbackProducer { + + @Autowired + AmqpTemplate amqpTemplate; + + public void sendCallbackMessage(CallbackDTO allbackDTO, final long delayTimes) { + + log.info("生产者发送消息,callbackDTO,{}", callbackDTO); + + amqpTemplate.convertAndSend(CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getExchange(), CallbackQueueEnum.QUEUE_GENSEE_CALLBACK.getRoutingKey(), JsonMapper.getInstance().toJson(genseeCallbackDTO), new MessagePostProcessor() { + @Override + public Message postProcessMessage(Message message) throws AmqpException { + //给消息设置延迟毫秒值,通过给消息设置x-delay头来设置消息从交换机发送到队列的延迟时间 + message.getMessageProperties().setHeader("x-delay", delayTimes); + message.getMessageProperties().setCorrelationId(callbackDTO.getSdkId()); + return message; + } + }); + } +} +``` + +##### **4.6.2 回调事件消息消费者** + +``` +@Slf4j +@Component +@RabbitListener(queues = "message.callback", containerFactory = "rabbitListenerContainerFactory") +public class CallbackConsumer { + + @Autowired + private IGlobalUserService globalUserService; + + @RabbitHandler + public void handle(String json, Channel channel, @Headers Map map) throws Exception { + + if (map.get("error") != null) { + //否认消息 + channel.basicNack((Long) map.get(AmqpHeaders.DELIVERY_TAG), false, true); + return; + } + + try { + + CallbackDTO callbackDTO = JsonMapper.getInstance().fromJson(json, CallbackDTO.class); + //执行业务逻辑 + globalUserService.execute(callbackDTO); + //消息消息成功手动确认,对应消息确认模式acknowledge-mode: manual + channel.basicAck((Long) map.get(AmqpHeaders.DELIVERY_TAG), false); + + } catch (Exception e) { + log.error("回调失败 -> {}", e); + } + } +} +``` + +#### **4.7 ThreadUtil异步工具类** + +```java +@Slf4j +public class ThreadUtils { + + public static void main(String[] args) { + for (int i = 0; i < 3; i++) { + ThreadUtil.execAsync(() -> { + ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current(); + int number = threadLocalRandom.nextInt(20) + 1; + System.out.println(number); + }); + log.info("当前第:" + i + "个线程"); + } + + log.info("task finish!"); + } +} +``` + +#### **4.8 Guava异步** + +`Guava`的`ListenableFuture`顾名思义就是可以监听的`Future`,是对java原生Future的扩展增强。我们知道Future表示一个异步计算任务,当任务完成时可以得到计算结果。 + +如果我们希望一旦计算完成就拿到结果展示给用户或者做另外的计算,就必须使用另一个线程不断的查询计算状态。这样做,代码复杂,而且效率低下。 + +使用**Guava ListenableFuture**可以帮我们检测Future是否完成了,不需要再通过get()方法苦苦等待异步的计算结果,如果完成就自动调用回调函数,这样可以减少并发程序的复杂度。 + +`ListenableFuture`是一个接口,它从`jdk`的`Future`接口继承,添加了`void addListener(Runnable listener, Executor executor)`方法。 + +我们看下如何使用`ListenableFuture`。首先需要定义`ListenableFuture`的实例: + +```java +ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool()); + final ListenableFuture listenableFuture = executorService.submit(new Callable() { + @Override + public Integer call() throws Exception { + log.info("callable execute...") + TimeUnit.SECONDS.sleep(1); + return 1; + } + }); +``` + +首先通过`MoreExecutors`类的静态方法`listeningDecorator`方法初始化一个`ListeningExecutorService`的方法,然后使用此实例的`submit`方法即可初始化`ListenableFuture`对象。 + +`ListenableFuture`要做的工作,在Callable接口的实现类中定义,这里只是休眠了1秒钟然后返回一个数字1,有了`ListenableFuture`实例,可以执行此Future并执行Future完成之后的回调函数。 + +```java + Futures.addCallback(listenableFuture, new FutureCallback() { + @Override + public void onSuccess(Integer result) { + //成功执行... + System.out.println("Get listenable future's result with callback " + result); + } + + @Override + public void onFailure(Throwable t) { + //异常情况处理... + t.printStackTrace(); + } +}); +``` \ No newline at end of file diff --git a/docs/advance/system-design/2-order-timeout-auto-cancel.md b/docs/advance/system-design/2-order-timeout-auto-cancel.md index 5ffaf4e..b100f91 100644 --- a/docs/advance/system-design/2-order-timeout-auto-cancel.md +++ b/docs/advance/system-design/2-order-timeout-auto-cancel.md @@ -42,11 +42,11 @@ head: 对上述的任务,我们给一个专业的名字来形容,那就是延时任务。那么这里就会产生一个问题,这个延时任务和定时任务的区别究竟在哪里呢?一共有如下几点区别 -定时任务有明确的触发时间,延时任务没有 +1、定时任务有明确的触发时间,延时任务没有 -定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期 +2、定时任务有执行周期,而延时任务在某事件触发后一段时间内执行,没有执行周期 -定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务 +3、定时任务一般执行的是批处理操作是多个任务,而延时任务一般是单个任务 下面,我们以判断订单是否超时为例,进行方案分析 @@ -340,13 +340,13 @@ public class HashedWheelTimerTest { - 集群扩展相当麻烦 - 因为内存条件限制的原因,比如下单未付款的订单数太多,那么很容易就出现 OOM 异常 -## 方案 4:redis 缓存 +## 方案 4:Redis 缓存 ### 思路一 利用 redis 的 zset,zset 是一个有序集合,每一个元素(member)都关联了一个 score,通过 score 排序来取集合中的值 -添加元素:ZADD key score member [[score member][score member] …] +添加元素:ZADD key score member [score member …] 按顺序查询元素:ZRANGE key start stop [WITHSCORES] @@ -619,11 +619,11 @@ ps:redis 的 pub/sub 机制存在一个硬伤,官网内容如下 ### 思路 -我们可以采用 rabbitMQ 的延时队列。RabbitMQ 具有以下两个特性,可以实现延迟队列 +我们可以采用 RabbitMQ 的延时队列。RabbitMQ 具有以下两个特性,可以实现延迟队列 RabbitMQ 可以针对 Queue 和 Message 设置 x-message-tt,来控制消息的生存时间,如果超时,则消息变为 dead letter -lRabbitMQ 的 Queue 可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了 deadletter,则按照这两个参数重新路由。结合以上两个特性,就可以模拟出延迟消息的功能,具体的,我改天再写一篇文章,这里再讲下去,篇幅太长。 +lRabbitMQ 的 Queue 可以配置 x-dead-letter-exchange 和 x-dead-letter-routing-key(可选)两个参数,用来控制队列内出现了 deadletter,则按照这两个参数重新路由。结合以上两个特性,就可以模拟出延迟消息的功能。 ### 优点 diff --git a/docs/advance/system-design/README.md b/docs/advance/system-design/README.md index 8f7b4c1..515b3d5 100644 --- a/docs/advance/system-design/README.md +++ b/docs/advance/system-design/README.md @@ -26,8 +26,8 @@ 怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**218**元,减去**50**元的优惠券,等于说只需要**168**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.46元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**218**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -![](http://img.topjavaer.cn/img/202304212233017.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202312280808000.png) \ No newline at end of file diff --git a/docs/campus-recruit/interview/3-baidu.md b/docs/campus-recruit/interview/3-baidu.md index d5918e5..3cd2f26 100644 --- a/docs/campus-recruit/interview/3-baidu.md +++ b/docs/campus-recruit/interview/3-baidu.md @@ -36,6 +36,18 @@ - 刷脏页的流程 - 算法题:平方根 +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + ## 面经3 - 自我介绍 @@ -104,6 +116,12 @@ -**最后给大家分享一份精心整理的大厂高频面试题PDF,需要的小伙伴可以自行下载:** +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# -[大厂面试手册](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd) \ No newline at end of file +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git a/docs/campus-recruit/interview/4-ali.md b/docs/campus-recruit/interview/4-ali.md index 2c99b2e..00b2a7a 100644 --- a/docs/campus-recruit/interview/4-ali.md +++ b/docs/campus-recruit/interview/4-ali.md @@ -48,6 +48,18 @@ 24. 操作系统的内存管理的页面淘汰 算法 ,介绍下LRU(最近最少使用算法 ) 25. B+树的特点与优势 +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + ## 面经3 - 自我介绍,说简历里没有的东西 @@ -74,6 +86,14 @@ - 服务注册的时候发现没有注册成功会是什么原因。 - 讲讲你认为的rpc和service mesh之间的关系。 -**最后给大家分享一份精心整理的大厂高频面试题PDF,需要的小伙伴可以自行下载:** -[大厂面试手册](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd) \ No newline at end of file + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git a/docs/campus-recruit/interview/5-kuaishou.md b/docs/campus-recruit/interview/5-kuaishou.md index 1b24e6d..539a84a 100644 --- a/docs/campus-recruit/interview/5-kuaishou.md +++ b/docs/campus-recruit/interview/5-kuaishou.md @@ -67,6 +67,18 @@ 1. 对于部门的业务、技术栈 2. 对我的建议、和整个面试的感觉 +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + ## 面经1-二面 > **Java基础** @@ -307,6 +319,14 @@ while (true) { -**最后给大家分享一份精心整理的大厂高频面试题PDF,需要的小伙伴可以自行下载:** -[大厂面试手册](http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd) \ No newline at end of file + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git a/docs/campus-recruit/interview/6-meituan.md b/docs/campus-recruit/interview/6-meituan.md index 43a1b71..32cb5bf 100644 --- a/docs/campus-recruit/interview/6-meituan.md +++ b/docs/campus-recruit/interview/6-meituan.md @@ -68,3 +68,12 @@ +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git a/docs/computer-basic/network.md b/docs/computer-basic/network.md index 127e6ab..c407f6d 100644 --- a/docs/computer-basic/network.md +++ b/docs/computer-basic/network.md @@ -41,8 +41,8 @@ head: 怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**218**元,减去**50**元的优惠券,等于说只需要**168**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.46元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**218**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 ![](http://img.topjavaer.cn/img/202304212233017.png) diff --git a/docs/database/mysql.md b/docs/database/mysql.md index 4edc5fb..b7946d9 100644 --- a/docs/database/mysql.md +++ b/docs/database/mysql.md @@ -20,6 +20,10 @@ head: ::: +## 更新记录 + +- 2024.5.15,新增[B树和B+树的区别?](###B树和B+树的区别?) + ## 什么是MySQL MySQL是一个关系型数据库,它采用表的形式来存储数据。你可以理解成是Excel表格,既然是表的形式存储数据,就有表结构(行和列)。行代表每一行数据,列代表该行中的每个值。列上的值是有数据类型的,比如:整数、字符串、日期等等。 @@ -262,6 +266,18 @@ Index_comment: - 哈希索引**不支持模糊查询**及多列索引的最左前缀匹配。 - 因为哈希表中会**存在哈希冲突**,所以哈希索引的性能是不稳定的,而B+树索引的性能是相对稳定的,每次查询都是从根节点到叶子节点。 +### B树和B+树的区别? + +![](http://img.topjavaer.cn/img/202405150843832.png) + +![](http://img.topjavaer.cn/img/202405150843657.png) + +1. **数据存储方式**:在B树中,每个节点都包含键和对应的值,叶子节点存储了实际的数据记录;而B+树中,仅仅只有叶子节点存储了实际的数据记录,非叶子节点只包含键信息和指向子节点的指针。 +2. **数据检索方式**:在B树中,由于非叶子节点也存储了数据,所以查询时可以直接在非叶子节点找到对应的数据,具有更短的查询路径; + 而B+树的所有数据都存储在叶子节点上,只有通过子节点才能获取到完整的数据。 +3. **范围查询效率**:由于B+树的所有数据都存储在叶子节点上,并且叶子节点之间使用链表连接,所以范围查询的效率比较高。而在B树中,范围查询需要通过遍历多个层级的节点,效率相对较低。 +4. **适用场景**:B树适合进行随机读写操作,因为每个节点都包含了数据。而B+树适合进行范围查询和顺序访问,因为数据都存储在叶子节点上,并且叶子节点之间使用链表连接,便于进行顺序遍历。 + ### 为什么B+树比B树更适合实现数据库索引? - 由于B+树的数据都存储在叶子结点中,叶子结点均为索引,方便扫库,只需要扫一遍叶子结点即可,但是B树因为其分支结点同样存储着数据,我们要找到具体的数据,需要进行一次中序遍历按序来扫,所以B+树更加适合在区间查询的情况,而在数据库中基于范围的查询是非常频繁的,所以通常B+树用于数据库索引。 diff --git a/docs/framework/mybatis.md b/docs/framework/mybatis.md index 0337815..b21cb9d 100644 --- a/docs/framework/mybatis.md +++ b/docs/framework/mybatis.md @@ -27,6 +27,17 @@ head: - MyBatis作为持久层框架,其主要思想是将程序中的大量SQL语句剥离出来,配置在配置文件当中,实现SQL的灵活配置。 - 这样做的好处是将SQL与程序代码分离,可以在不修改代码的情况下,直接在配置文件当中修改SQL。 +## 为什么使用Mybatis代替JDBC? + +MyBatis 是一种优秀的 ORM(Object-Relational Mapping)框架,与 JDBC 相比,有以下几点优势: + +1. 简化了 JDBC 的繁琐操作:使用 JDBC 进行数据库操作需要编写大量的样板代码,如获取连接、创建 Statement/PreparedStatement,设置参数,处理结果集等。而使用 MyBatis 可以将这些操作封装起来,通过简单的配置文件和 SQL 语句就能完成数据库操作,从而大大简化了开发过程。 +2. 提高了 SQL 的可维护性:使用 JDBC 进行数据库操作,SQL 语句通常会散布在代码中的各个位置,当 SQL 语句需要修改时,需要找到所有使用该语句的地方进行修改,这非常不方便,也容易出错。而使用 MyBatis,SQL 语句都可以集中在配置文件中,可以更加方便地修改和维护,同时也提高了 SQL 语句的可读性。 +3. 支持动态 SQL:MyBatis 提供了强大的动态 SQL 功能,可以根据不同的条件生成不同的 SQL 语句,这对于复杂的查询操作非常有用。 +4. 易于集成:MyBatis 可以与 Spring 等流行的框架集成使用,可以通过 XML 或注解配置进行灵活的配置,同时 MyBatis 也提供了非常全面的文档和示例代码,学习和使用 MyBatis 非常方便。 + +综上所述,使用 MyBatis 可以大大简化数据库操作的代码,提高 SQL 语句的可维护性和可读性,同时还提供了强大的动态 SQL 功能,易于集成使用。因此,相比于直接使用 JDBC,使用 MyBatis 更为便捷、高效和方便。 + ## **ORM是什么** ORM(Object Relational Mapping),对象关系映射,是一种为了解决关系型数据库数据与简单Java对象(POJO)的映射关系的技术。简单的说,ORM是通过使用描述对象和数据库之间映射的元数据,将程序中的对象自动持久化到关系型数据库中。 @@ -107,7 +118,7 @@ Mybatis仅可以编写针对 `ParameterHandler`、`ResultSetHandler`、`Statemen 编写插件:实现 Mybatis 的 Interceptor 接口并复写 intercept()方法,然后再给插件编写注解,指定要拦截哪一个接口的哪些方法即可,最后在配置文件中配置你编写的插件。 -## .Mybatis 是否支持延迟加载? +## Mybatis 是否支持延迟加载? Mybatis 仅支持 association 关联对象和 collection 关联集合对象的延迟加载,association 指的就是一对一,collection 指的就是一对多查询。在 Mybatis 配置文件中,可以配置是否启用延迟加载`lazyLoadingEnabled=true|false`。 diff --git a/docs/framework/spring.md b/docs/framework/spring.md index 410e4e3..4ea008b 100644 --- a/docs/framework/spring.md +++ b/docs/framework/spring.md @@ -102,6 +102,18 @@ AOP有两种实现方式:静态代理和动态代理。 动态代理:代理类在程序运行时创建,AOP框架不会去修改字节码,而是在内存中临时生成一个代理对象,在运行期间对业务方法进行增强,不会生成新类。 +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + ## Spring AOP的实现原理 `Spring`的`AOP`实现原理其实很简单,就是通过**动态代理**实现的。如果我们为`Spring`的某个`bean`配置了切面,那么`Spring`在创建这个`bean`的时候,实际上创建的是这个`bean`的一个代理对象,我们后续对`bean`中方法的调用,实际上调用的是代理类重写的代理方法。而`Spring`的`AOP`使用了两种动态代理,分别是**JDK的动态代理**,以及**CGLib的动态代理**。 @@ -977,4 +989,14 @@ Spring常用的注入方式有:属性注入, 构造方法注入, set 方法注 +> 最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: +> +> ![](http://img.topjavaer.cn/image/Image.png) +> +> ![](http://img.topjavaer.cn/image/image-20221030094126118.png) +> +> **200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# +> +> 备用链接:https://pan.quark.cn/s/3f1321952a16 + ![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/framework/springboot.md b/docs/framework/springboot.md index 496473c..a0b52ad 100644 --- a/docs/framework/springboot.md +++ b/docs/framework/springboot.md @@ -60,12 +60,78 @@ starter提供了一个自动化配置类,一般命名为 XXXAutoConfiguration 启动类上面的注解是@SpringBootApplication,它也是 Spring Boot 的核心注解,主要组合包含了以下 3 个注解: +- @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 +- @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 +- @ComponentScan:Spring组件扫描。 + +## 有哪些常用的SpringBoot注解? + +- @SpringBootApplication。这个注解是Spring Boot最核心的注解,用在 Spring Boot的主类上,标识这是一个 Spring Boot 应用,用来开启 Spring Boot 的各项能力 + - @SpringBootConfiguration:组合了 @Configuration 注解,实现配置文件的功能。 - @EnableAutoConfiguration:打开自动配置的功能,也可以关闭某个自动配置的选项,如关闭数据源自动配置功能: @SpringBootApplication(exclude = { DataSourceAutoConfiguration.class })。 - @ComponentScan:Spring组件扫描。 +- @Repository:用于标注数据访问组件,即DAO组件。 + +- @Service:一般用于修饰service层的组件 + +- **@RestController**。用于标注控制层组件(如struts中的action),表示这是个控制器bean,并且是将函数的返回值直 接填入HTTP响应体中,是REST风格的控制器;它是@Controller和@ResponseBody的合集。 + +- **@ResponseBody**。表示该方法的返回结果直接写入HTTP response body中 + +- **@Component**。泛指组件,当组件不好归类的时候,我们可以使用这个注解进行标注。 + +- **@Bean**,相当于XML中的``,放在方法的上面,而不是类,意思是产生一个bean,并交给spring管理。 + +- **@AutoWired**,byType方式。把配置好的Bean拿来用,完成属性、方法的组装,它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 + +- **@Qualifier**。当有多个同一类型的Bean时,可以用@Qualifier("name")来指定。与@Autowired配合使用 + +- **@Resource(name="name",type="type")**。没有括号内内容的话,默认byName。与@Autowired干类似的事。 + +- **@RequestMapping** + + RequestMapping是一个用来处理请求地址映射的注解;提供路由信息,负责URL到Controller中的具体函数的映射,可用于类或方法上。用于类上,表示类中的所有响应请求的方法都是以该地址作为父路径。 + +- **@RequestParam** + + 用在方法的参数前面。 + +- ### @Scope + + 用于声明一个Spring`Bean`实例的作用域 + +- ### @Primary + + 当同一个对象有多个实例时,优先选择该实例。 + +- ### @PostConstruct + + 用于修饰方法,当对象实例被创建并且依赖注入完成后执行,可用于对象实例的初始化操作。 + +- ### @PreDestroy + + 用于修饰方法,当对象实例将被Spring容器移除时执行,可用于对象实例持有资源的释放。 + +- ### @EnableTransactionManagement + + 启用Spring基于注解的事务管理功能,需要和`@Configuration`注解一起使用。 + +- ### @Transactional + + 表示方法和类需要开启事务,当作用与类上时,类中所有方法均会开启事务,当作用于方法上时,方法开启事务,方法上的注解无法被子类所继承。 + +- ### @ControllerAdvice + + 常与`@ExceptionHandler`注解一起使用,用于捕获全局异常,能作用于所有controller中。 + +- ### @ExceptionHandler + + 修饰方法时,表示该方法为处理全局异常的方法。 + ## 自动配置原理 SpringBoot实现自动配置原理图解: diff --git a/docs/framework/springcloud-interview.md b/docs/framework/springcloud-interview.md index 619e950..f95f36b 100644 --- a/docs/framework/springcloud-interview.md +++ b/docs/framework/springcloud-interview.md @@ -24,17 +24,66 @@ head: ::: +## 更新记录 + +- 2024.5.15,完善[Spring、SpringMVC、Springboot、 Springcloud 的区别是什么?](##Spring、SpringMVC、Springboot、 Springcloud 的区别是什么?) ## 1、什么是Spring Cloud ? Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。 -## spring、 springboot、 springcloud 的区别是什么? +![](http://img.topjavaer.cn/img/202405220918802.png) + +Spring cloud 流应用程序启动器是基于 Spring Boot 的 Spring 集成应用程序,提供与外部系统的集成。Spring cloud Task,一个生命周期短暂的微服务框架,用于快速构建执行有限数据处理的应用程序。 + +## Spring、SpringMVC、Springboot、 Springcloud 的区别是什么? + +### Spring + +Spring是一个生态体系(也可以说是技术体系),是集大成者,它包含了Spring Framework、Spring Boot、Spring Cloud等。**它是一个轻量级控制反转(IOC)和面向切面(AOP)的容器框架**,为开发者提供了一个简易的开发方式。 + +Spring的核心特性思想之一IOC,它实现了容器对Bean对象的管理、降低组件耦合,使各层服务解耦。 + +Spring的另一个核心特性就是AOP,面向切面编程。面向切面编程需要将程序逻辑分解为称为所谓关注点的不同部分。跨越应用程序多个点的功能称为跨领域问题,这些跨领域问题在概念上与应用程序的业务逻辑分离。有许多常见的例子,如日志记录,声明式事务,安全性,缓存等。 + +**如果说IOC依赖注入可以帮助我们将应用程序对象相互分离,那么AOP可以帮助我们将交叉问题与它们所影响的对象分离。二者目的都是使服务解耦,使开发简易。** + +当然,除了Spring 的两大核心功能,还有如下这些,如: + +- Spring JDBC +- Spring MVC +- Spring ORM +- Spring Test -Spring是一个生态体系(也可以说是技术体系),是集大成者,它包含了Spring Framework、Spring Boot、Spring Cloud等。 +### SpringMVC + +Spring与MVC可以更好地解释什么是SpringMVC,MVC为现代web项目开发的一种很常见的模式,简言之C(控制器)将V(视图、用户客户端)与M(模块,业务)分开构成了MVC ,业内常见的MVC模式的开发框架有Struts。 + +Spring MVC是Spring的一部分,主要用于开发WEB应用和网络接口,它是Spring的一个模块,通过DispatcherServlet, ModelAndView 和View Resolver,让应用开发变得很容易。 + +### SpringBoot + +SpringBoot是一套整合了框架的框架。 + +它的初衷:解决Spring框架配置文件的繁琐、搭建服务的复杂性。 + +它的设计理念:**约定优于配置**(convention over configuration)。 + +基于此理念实现了**自动配置**,且降低项目搭建的复杂度。 + +搭建一个接口服务,通过SpringBoot几行代码即可实现。基于Spring Boot,不是说原来的配置没有了,而是Spring Boot有一套默认配置,我们可以把它看做比较通用的约定,而Spring Boot遵循的是**约定优于配置**原则,同时,如果你需要使用到Spring以往提供的各种复杂但功能强大的配置功能,Spring Boot一样支持。 + +在Spring Boot中,你会发现引入的所有包都是starter形式,如: + +- spring-boot-starter-web-services,针对SOAP Web Services +- spring-boot-starter-web,针对Web应用与网络接口 +- spring-boot-starter-jdbc,针对JDBC +- spring-boot-starter-cache,针对缓存支持 Spring Boot是基于 Spring 框架开发的用于开发 Web 应用程序的框架,它帮助开发人员快速搭建和配置一个独立的、可执行的、基于 Spring 的应用程序,从而减少了繁琐和重复的配置工作。 +### Spring Cloud + Spring Cloud事实上是一整套基于Spring Boot的微服务解决方案。它为开发者提供了很多工具,用于快速构建分布式系统的一些通用模式,例如:配置管理、注册中心、服务发现、限流、网关、链路追踪等。Spring Boot是build anything,而Spring Cloud是coordinate anything,Spring Cloud的每一个微服务解决方案都是基于Spring Boot构建的。 ## 2、什么是微服务? diff --git a/docs/framework/springmvc.md b/docs/framework/springmvc.md index e6a39b6..f0bc2ae 100644 --- a/docs/framework/springmvc.md +++ b/docs/framework/springmvc.md @@ -13,194 +13,38 @@ head: content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! --- -::: tip 这是一则或许对你有帮助的信息 -- **面试手册**:这是一份大彬精心整理的[**大厂面试手册**](https://topjavaer.cn/zsxq/mianshishouce.html)最新版,目前已经更新迭代了**19**个版本,质量很高(专为面试打造) -- **知识星球**:**专属面试手册/一对一交流/简历修改/超棒的学习氛围/学习路线规划**,欢迎加入[大彬的知识星球](https://topjavaer.cn/zsxq/introduce.html)(点击链接查看星球的详细介绍) -::: +**Spring MVC高频面试题**是我的[知识星球](https://topjavaer.cn/zsxq/introduce.html)**内部专属资料**,已经整理到**Java面试手册完整版**。 -## 说说你对 SpringMVC 的理解 +![](http://img.topjavaer.cn/img/202311152201457.png) -SpringMVC是一种基于 Java 的实现MVC设计模型的请求驱动类型的轻量级Web框架,属于Spring框架的一个模块。 +如果你正在打算准备跳槽、面试,星球还提供**简历指导、修改服务**,大彬已经帮**120**+个小伙伴修改了简历,相对还是比较有经验的。 -它通过一套注解,让一个简单的Java类成为处理请求的控制器,而无须实现任何接口。同时它还支持RESTful编程风格的请求。 +![](http://img.topjavaer.cn/img/23届-天津工业大学-主修课程-点评.jpg) -## 什么是MVC模式? +![](http://img.topjavaer.cn/img/简历修改1.png) -MVC的全名是`Model View Controller`,是模型(model)-视图(view)-控制器(controller)的缩写,是一种软件设计典范。它是用一种业务逻辑、数据与界面显示分离的方法来组织代码,将众多的业务逻辑聚集到一个部件里面,在需要改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑,达到减少编码的时间。 +另外星球也提供**专属一对一的提问答疑**,帮你解答各种疑难问题,包括自学Java路线、职业规划、面试问题等等。大彬会**优先解答**球友的问题。 -View,视图是指用户看到并与之交互的界面。比如由html元素组成的网页界面,或者软件的客户端界面。MVC的好处之一在于它能为应用程序处理很多不同的视图。在视图中其实没有真正的处理发生,它只是作为一种输出数据并允许用户操纵的方式。 +![](http://img.topjavaer.cn/img/image-20230318103729439.png) -model,模型是指模型表示业务规则。在MVC的三个部件中,模型拥有最多的处理任务。被模型返回的数据是中立的,模型与数据格式无关,这样一个模型能为多个视图提供数据,由于应用于模型的代码只需写一次就可以被多个视图重用,所以减少了代码的重复性。 +![image-20230318104002122](http://img.topjavaer.cn/img/image-20230318104002122.png) -controller,控制器是指控制器接受用户的输入并调用模型和视图去完成用户的需求,控制器本身不输出任何东西和做任何处理。它只是接收请求并决定调用哪个模型构件去处理请求,然后再确定用哪个视图来显示返回的数据。 +![](http://img.topjavaer.cn/img/image-20230102210715391.png) -## SpringMVC 有哪些优点? +星球还有很多其他**优质资料**,比如包括Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源等等。 -1. 与 Spring 集成使用非常方便,生态好。 -2. 配置简单,快速上手。 -3. 支持 RESTful 风格。 -4. 支持各种视图技术,支持各种请求资源映射策略。 +![](http://img.topjavaer.cn/img/image-20221229145413500.png) -## Spring MVC和Struts的区别 +![](http://img.topjavaer.cn/img/image-20221229145455706.png) -1. Spring MVC是基于方法开发,Struts2是基于类开发的。 - - Spring MVC会将用户请求的URL路径信息与Controller的某个方法进行映射,所有请求参数会注入到对应方法的形参上,生成Handler对象,对象中只有一个方法; - - Struts每处理一次请求都会实例一个Action,Action类的所有方法使用的请求参数都是Action类中的成员变量,随着方法增多,整个Action也会变得混乱。 -2. Spring MVC支持单例开发模式,Struts只能使用多例 +![](http://img.topjavaer.cn/img/image-20221229145550185.png) - - Struts由于只能通过类的成员变量接收参数,故只能使用多例。 -3. Struts2 的核心是基于一个Filter即StrutsPreparedAndExcuteFilter,Spring MVC的核心是基于一个Servlet即DispatcherServlet(前端控制器)。 -4. Struts处理速度稍微比Spring MVC慢,Struts使用了Struts标签,加载数据较慢。 +怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -## Spring MVC的工作原理 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**218**元,减去**50**元的优惠券,等于说只需要**168**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.46元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -Spring MVC的工作原理如下: +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**218**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -1. DispatcherServlet 接收用户的请求 -2. 找到用于处理request的 handler 和 Interceptors,构造成 HandlerExecutionChain 执行链 -3. 找到 handler 相对应的 HandlerAdapter -4. 执行所有注册拦截器的preHandler方法 -5. 调用 HandlerAdapter 的 handle() 方法处理请求,返回 ModelAndView -6. 倒序执行所有注册拦截器的postHandler方法 -7. 请求视图解析和视图渲染 - -![](http://img.topjavaer.cn/img/spring_mvc原理.png) - -## Spring MVC的主要组件? - -- 前端控制器(DispatcherServlet):接收用户请求,给用户返回结果。 -- 处理器映射器(HandlerMapping):根据请求的url路径,通过注解或者xml配置,寻找匹配的Handler。 -- 处理器适配器(HandlerAdapter):Handler 的适配器,调用 handler 的方法处理请求。 -- 处理器(Handler):执行相关的请求处理逻辑,并返回相应的数据和视图信息,将其封装到ModelAndView对象中。 -- 视图解析器(ViewResolver):将逻辑视图名解析成真正的视图View。 -- 视图(View):接口类,实现类可支持不同的View类型(JSP、FreeMarker、Excel等)。 - -## Spring MVC的常用注解由有哪些? -- @Controller:用于标识此类的实例是一个控制器。 -- @RequestMapping:映射Web请求(访问路径和参数)。 -- @ResponseBody:注解返回数据而不是返回页面 -- @RequestBody:注解实现接收 http 请求的 json 数据,将 json 数据转换为 java 对象。 -- @PathVariable:获得URL中路径变量中的值 -- @RestController:@Controller+@ResponseBody -- @ExceptionHandler标识一个方法为全局异常处理的方法。 - -## @Controller 注解有什么用? - -`@Controller` 注解标记一个类为 Spring Web MVC 控制器。Spring MVC 会将扫描到该注解的类,然后扫描这个类下面带有 `@RequestMapping` 注解的方法,根据注解信息,为这个方法生成一个对应的处理器对象,在上面的 HandlerMapping 和 HandlerAdapter组件中讲到过。 - -当然,除了添加 `@Controller` 注解这种方式以外,你还可以实现 Spring MVC 提供的 `Controller` 或者 `HttpRequestHandler` 接口,对应的实现类也会被作为一个处理器对象 - -## @RequestMapping 注解有什么用? - -`@RequestMapping` 注解,用于配置处理器的 HTTP 请求方法,URI等信息,这样才能将请求和方法进行映射。这个注解可以作用于类上面,也可以作用于方法上面,在类上面一般是配置这个控制器的 URI 前缀。 - -## @RestController 和 @Controller 有什么区别? - -`@RestController` 注解,在 `@Controller` 基础上,增加了 `@ResponseBody` 注解,更加适合目前前后端分离的架构下,提供 Restful API ,返回 JSON 数据格式。 - -## @RequestMapping 和 @GetMapping 注解有什么不同? - -1. `@RequestMapping`:可注解在类和方法上;`@GetMapping` 仅可注册在方法上 -2. `@RequestMapping`:可进行 GET、POST、PUT、DELETE 等请求方法;`@GetMapping` 是 `@RequestMapping` 的 GET 请求方法的特例。 - -## @RequestParam 和 @PathVariable 两个注解的区别 - -两个注解都用于方法参数,获取参数值的方式不同,`@RequestParam` 注解的参数从请求携带的参数中获取,而 `@PathVariable` 注解从请求的 URI 中获取 - -## @RequestBody和@RequestParam的区别 - -@RequestBody一般处理的是在ajax请求中声明contentType: "application/json; charset=utf-8"时候。也就是json数据或者xml数据。 - -@RequestParam一般就是在ajax里面没有声明contentType的时候,为默认的`x-www-form-urlencoded`格式时。 - -## Spring MVC的异常处理 - -可以将异常抛给Spring框架,由Spring框架来处理;我们只需要配置简单的异常处理器,在异常处理器中添视图页面即可。 - -- 使用系统定义好的异常处理器 SimpleMappingExceptionResolver -- 使用自定义异常处理器 -- 使用异常处理注解 - -## SpringMVC 用什么对象从后台向前台传递数据的? - -1. 将数据绑定到 request; -2. 返回 ModelAndView; -3. 通过ModelMap对象,可以在这个对象里面调用put方法,把对象加到里面,前端就可以通过el表达式拿到; -4. 绑定数据到 Session中。 - -## SpringMvc的Controller是不是单例模式? - -单例模式。在多线程访问的时候有线程安全问题,解决方案是在控制器里面不要写可变状态量,如果需要使用这些可变状态,可以使用ThreadLocal,为每个线程单独生成一份变量副本,独立操作,互不影响。 - -## 介绍下 Spring MVC 拦截器? - -Spring MVC 拦截器对应HandlerInterceor接口,该接口位于org.springframework.web.servlet的包中,定义了三个方法,若要实现该接口,就要实现其三个方法: - -1. **前置处理(preHandle()方法)**:该方法在执行控制器方法之前执行。返回值为Boolean类型,如果返回false,表示拦截请求,不再向下执行,如果返回true,表示放行,程序继续向下执行(如果后面没有其他Interceptor,就会执行controller方法)。所以此方法可对请求进行判断,决定程序是否继续执行,或者进行一些初始化操作及对请求进行预处理。 -2. **后置处理(postHandle()方法)**:该方法在执行控制器方法调用之后,且在返回ModelAndView之前执行。由于该方法会在DispatcherServlet进行返回视图渲染之前被调用,所以此方法多被用于处理返回的视图,可通过此方法对请求域中的模型和视图做进一步的修改。 -3. **已完成处理(afterCompletion()方法)**:该方法在执行完控制器之后执行,由于是在Controller方法执行完毕后执行该方法,所以该方法适合进行一些资源清理,记录日志信息等处理操作。 - -可以通过拦截器进行权限检验,参数校验,记录日志等操作 - -## SpringMvc怎么配置拦截器? - -有两种写法,一种是实现HandlerInterceptor接口,另外一种是继承适配器类,接着在接口方法当中,实现处理逻辑;然后在SpringMvc的配置文件中配置拦截器即可: - -```java - - - - - - - - - - -``` - -## Spring MVC 的拦截器和 Filter 过滤器有什么差别? - -有以下几点: - -- **功能相同**:拦截器和 Filter 都能实现相应的功能 -- **容器不同**:拦截器构建在 Spring MVC 体系中;Filter 构建在 Servlet 容器之上 -- **使用便利性不同**:拦截器提供了三个方法,分别在不同的时机执行;过滤器仅提供一个方法 - -## 什么是REST? - -REST,英文全称,Resource Representational State Transfer,对资源的访问状态的变化通过url的变化表述出来。 - -Resource:**资源**。资源是REST架构或者说整个网络处理的核心。 - -Representational:**某种表现形式**,比如用JSON,XML,JPEG等。 - -State Transfer:**状态变化**。通过HTTP method实现。 - -REST描述的是在网络中client和server的一种交互形式。用大白话来说,就是**通过URL就知道要什么资源,通过HTTP method就知道要干什么,通过HTTP status code就知道结果如何**。 - -举个例子: - -```java -GET /tasks 获取所有任务 -POST /tasks 创建新任务 -GET /tasks/{id} 通过任务id获取任务 -PUT /tasks/{id} 更新任务 -DELETE /tasks/{id} 删除任务 -``` - -GET代表获取一个资源,POST代表添加一个资源,PUT代表修改一个资源,DELETE代表删除一个资源。 - -server提供的RESTful API中,URL中只使用名词来指定资源,原则上不使用动词。用`HTTP Status Code`传递server的状态信息。比如最常用的 200 表示成功,500 表示Server内部错误等。 - -## 使用REST有什么优势呢? - -第一,**风格统一**了,不会出现`delUser/deleteUser/removeUser`各种命名的代码了。 - -第二,**面向资源**,一目了然,具有自解释性。 - -第三,**充分利用 HTTP 协议本身语义**。 - -![](http://img.topjavaer.cn/img/20220612101342.png) +![](http://img.topjavaer.cn/img/202304212233017.png) \ No newline at end of file diff --git a/docs/java/java-basic.md b/docs/java/java-basic.md index b3ef235..ad1b6a4 100644 --- a/docs/java/java-basic.md +++ b/docs/java/java-basic.md @@ -95,6 +95,19 @@ JRE是Java的运行环境,并不是一个开发环境,所以没有包含任 ![](http://img.topjavaer.cn/img/20220402230613.png) + +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + ## Java程序是编译执行还是解释执行? 先看看什么是编译型语言和解释型语言。 @@ -295,7 +308,7 @@ true false ``` -为什么第三个输出是false?看看 Integer 类的源码就知道啦。 +为什么第二个输出是false?看看 Integer 类的源码就知道啦。 ```java public static Integer valueOf(int i) { @@ -305,7 +318,7 @@ public static Integer valueOf(int i) { } ``` -`Integer c = 200;` 会调用 调⽤`Integer.valueOf(200)`。而从Integer的valueOf()源码可以看到,这里的实现并不是简单的new Integer,而是用IntegerCache做一个cache。 +`Integer c = 200;` 会调用`Integer.valueOf(200)`。而从Integer的valueOf()源码可以看到,这里的实现并不是简单的new Integer,而是用IntegerCache做一个cache。 ```java private static class IntegerCache { @@ -1511,21 +1524,19 @@ server { 过滤器和拦截器底层实现不同。过滤器是基于函数回调的,拦截器是基于Java的反射机制(动态代理)实现的。一般自定义的过滤器中都会实现一个doFilter()方法,这个方法有一个FilterChain参数,而实际上它是一个回调接口。 -2、**使用范围不同**。 - -过滤器实现的是 javax.servlet.Filter 接口,而这个接口是在Servlet规范中定义的,也就是说过滤器Filter的使用要依赖于Tomcat等容器,导致它只能在web程序中使用。而拦截器是一个Spring组件,并由Spring容器管理,并不依赖Tomcat等容器,是可以单独使用的。拦截器不仅能应用在web程序中,也可以用于Application、Swing等程序中。 +2、**触发时机不同**。 -3、**使用的场景不同**。 +过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。 -因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:日志记录、权限判断等业务。而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、响应数据压缩等功能。 +拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。 -4、**触发时机不同**。 +![](http://img.topjavaer.cn/img/202405100905076.png) -过滤器Filter是在请求进入容器后,但在进入servlet之前进行预处理,请求结束是在servlet处理完以后。 +3、**使用的场景不同**。 -拦截器 Interceptor 是在请求进入servlet后,在进入Controller之前进行预处理的,Controller 中渲染了对应的视图之后请求结束。 +因为拦截器更接近业务系统,所以拦截器主要用来实现项目中的业务判断的,比如:日志记录、权限判断等业务。而过滤器通常是用来实现通用功能过滤的,比如:敏感词过滤、响应数据压缩等功能。 -5、**拦截的请求范围不同**。 +4、**拦截的请求范围不同**。 请求的执行顺序是:请求进入容器 -> 进入过滤器 -> 进入 Servlet -> 进入拦截器 -> 执行控制器。可以看到过滤器和拦截器的执行时机也是不同的,过滤器会先执行,然后才会执行拦截器,最后才会进入真正的要调用的方法。 @@ -1630,5 +1641,17 @@ server { > 参考:https://juejin.cn/post/7167153109158854687 +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 + + + ![](http://img.topjavaer.cn/img/20220612101342.png) diff --git a/docs/java/java-collection.md b/docs/java/java-collection.md index 76f73f0..df13bad 100644 --- a/docs/java/java-collection.md +++ b/docs/java/java-collection.md @@ -152,7 +152,7 @@ HashMap有扩容机制,就是当达到扩容条件时会进行扩容。扩容 如果我们没有设置初始容量大小,随着元素的不断增加,HashMap会发生多次扩容。而HashMap每次扩容都需要重建hash表,非常影响性能。所以建议开发者在创建HashMap的时候指定初始化容量。 -### 扩容过程? +### HashMap扩容过程是怎样的? 1.8扩容机制:当元素个数大于`threshold`时,会进行扩容,使用2倍容量的数组代替原有数组。采用尾插入的方式将原数组元素拷贝到新数组。1.8扩容之后链表元素相对位置没有变化,而1.7扩容之后链表元素会倒置。 @@ -160,7 +160,7 @@ HashMap有扩容机制,就是当达到扩容条件时会进行扩容。扩容 原数组的元素在重新计算hash之后,因为数组容量n变为2倍,那么n-1的mask范围在高位多1bit。在元素拷贝过程不需要重新计算元素在数组中的位置,只需要看看原来的hash值新增的那个bit是1还是0,是0的话索引没变,是1的话索引变成“原索引+oldCap”(根据`e.hash & oldCap == 0`判断) 。这样可以省去重新计算hash值的时间,而且由于新增的1bit是0还是1可以认为是随机的,因此resize的过程会均匀的把之前的冲突的节点分散到新的bucket。 -### put方法流程? +### 说说HashMapput方法的流程? 1. 如果table没有初始化就先进行初始化过程 2. 使用hash算法计算key的索引 diff --git a/docs/java/jvm.md b/docs/java/jvm.md index c51ab23..9120ef0 100644 --- a/docs/java/jvm.md +++ b/docs/java/jvm.md @@ -26,8 +26,8 @@ 怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**218**元,减去**50**元的优惠券,等于说只需要**168**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.46元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**218**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -![](http://img.topjavaer.cn/img/202304212233017.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202312280805419.png) \ No newline at end of file diff --git a/docs/other/site-diary.md b/docs/other/site-diary.md index 711b790..426ce57 100644 --- a/docs/other/site-diary.md +++ b/docs/other/site-diary.md @@ -8,6 +8,10 @@ sidebar: heading ## 更新记录 +- 2023.12.28,[增加源码解析模块,包含Sprign/SpringMVC/MyBatis(更新中)](/source/mybatis/1-overview.html)。 + +- 2023.12.28,[遭受黑客攻击,植入木马程序](/zsxq/article/site-hack.html)。 + - 2023.08.10,分布式锁实现-[RedLock](http://topjavaer.cn/advance/distributed/2-distributed-lock.html)补充图片 - 2023.05.04,导航栏增加图标。 diff --git a/docs/redis/redis.md b/docs/redis/redis.md index ff4dc4e..0f2a11c 100644 --- a/docs/redis/redis.md +++ b/docs/redis/redis.md @@ -20,6 +20,11 @@ head: ::: +## 更新记录 + +- 2024.1.7,新增[Redis存在线程安全问题吗?](##Redis存在线程安全问题吗?) +- 2024.5.9,补充[Redis应用场景有哪些?](##Redis应用场景有哪些?) + ## Redis是什么? Redis(`Remote Dictionary Server`)是一个使用 C 语言编写的,高性能非关系型的键值对数据库。与传统数据库不同的是,Redis 的数据是存在内存中的,所以读写速度非常快,被广泛应用于缓存方向。Redis可以将数据写入磁盘中,保证了数据的安全不丢失,而且Redis的操作是原子性的。 @@ -72,12 +77,18 @@ Redis基于Reactor模式开发了网络事件处理器,这个处理器被称 ## Redis应用场景有哪些? -1. **缓存热点数据**,缓解数据库的压力。 -2. 利用 Redis 原子性的自增操作,可以实现**计数器**的功能,比如统计用户点赞数、用户访问数等。 -3. **分布式锁**。在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 -4. **简单的消息队列**,可以使用Redis自身的发布/订阅模式或者List来实现简单的消息队列,实现异步操作。 -5. **限速器**,可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来不必要的压力。 -6. **好友关系**,利用集合的一些命令,比如交集、并集、差集等,实现共同好友、共同爱好之类的功能。 +Redis作为一种优秀的基于key/value的缓存,有非常不错的性能和稳定性,无论是在工作中,还是面试中,都经常会出现。 + +下面来聊聊Redis的常见的8种应用场景。 + +1. **缓存热点数据**,缓解数据库的压力。例如:热点数据缓存(例如报表、明星出轨),对象缓存、全页缓存、可以提升热点数据的访问数据。 +2. **计数器**。利用 Redis 原子性的自增操作,可以实现**计数器**的功能,内存操作,性能非常好,非常适用于这些计数场景,比如统计用户点赞数、用户访问数等。为了保证数据实时效,每次浏览都得给+1,并发量高时如果每次都请求数据库操作无疑是种挑战和压力。 +3. **分布式会话**。集群模式下,在应用不多的情况下一般使用容器自带的session复制功能就能满足,当应用增多相对复杂的系统中,一般都会搭建以Redis等内存数据库为中心的session服务,session不再由容器管理,而是由session服务及内存数据库管理。 +4. **分布式锁**。在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。可以使用 Redis 自带的 SETNX(SET if Not eXists) 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。 +5. **简单的消息队列**,消息队列是大型网站必用中间件,如ActiveMQ、RabbitMQ、Kafka等流行的消息队列中间件,主要用于业务解耦、流量削峰及异步处理实时性低的业务。可以使用Redis自身的发布/订阅模式或者List来实现简单的消息队列,实现异步操作。 +6. **限速器**,可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来不必要的压力。 +7. **社交网络**,点赞、踩、关注/被关注、共同好友等是社交网站的基本功能,社交网站的访问量通常来说比较大,而且传统的关系数据库类型不适合存储这种类型的数据,Redis提供的哈希、集合等数据结构能很方便的的实现这些功能。 +8. **排行榜**。很多网站都有排行榜应用的,如京东的月度销量榜单、商品按时间的上新排行榜等。Redis提供的有序集合数据类构能实现各种复杂的排行榜应用。 ## Memcached和Redis的区别? @@ -140,6 +151,22 @@ Redis基于Reactor模式开发了网络事件处理器,这个处理器被称 可以好好利用Hash,list,sorted set,set等集合类型数据,因为通常情况下很多小的Key-Value可以用更紧凑的方式存放到一起。尽可能使用散列表(hashes),散列表(是说散列表里面存储的数少)使用的内存非常小,所以你应该尽可能的将你的数据模型抽象到一个散列表里面。比如你的web系统中有一个用户对象,不要为这个用户的名称,姓氏,邮箱,密码设置单独的key,而是应该把这个用户的所有信息存储到一张散列表里面。 +## Redis存在线程安全问题吗? + +首先,从Redis 服务端层面分析。 + +Redis Server本身是一个线程安全的K-V数据库,也就是说在Redis Server上执行的指令,不需要任何同步机制,不会存在线程安全问题。 + +虽然Redis 6.0里面,增加了多线程的模型,但是增加的多线程只是用来处理网络IO事件,对于指令的执行过程,仍然是由主线程来处理,所以不会存在多个线程通知执行操作指令的情况。 + +第二个,从Redis客户端层面分析。 + +虽然Redis Server中的指令执行是原子的,但是如果有多个Redis客户端同时执行多个指令的时候,就无法保证原子性。 + +假设两个redis client同时获取Redis Server上的key1, 同时进行修改和写入,因为多线程环境下的原子性无法被保障,以及多进程情况下的共享资源访问的竞争问题,使得数据的安全性无法得到保障。 + +当然,对于客户端层面的线程安全性问题,解决方法有很多,比如尽可能的使用Redis里面的原子指令,或者对多个客户端的资源访问加锁,或者通过Lua脚本来实现多个指令的操作等等。 + ## keys命令存在的问题? redis的单线程的。keys指令会导致线程阻塞一段时间,直到执行完毕,服务才能恢复。scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是`O(1)`,但是要真正实现keys的功能,需要执行多次scan。 diff --git "a/docs/source/mybatis/# MyBatis \346\272\220\347\240\201\345\210\206\346\236\220\357\274\210\344\270\203\357\274\211\357\274\232\346\216\245\345\217\243\345\261\202.md" "b/docs/source/mybatis/# MyBatis \346\272\220\347\240\201\345\210\206\346\236\220\357\274\210\344\270\203\357\274\211\357\274\232\346\216\245\345\217\243\345\261\202.md" new file mode 100644 index 0000000..b691b56 --- /dev/null +++ "b/docs/source/mybatis/# MyBatis \346\272\220\347\240\201\345\210\206\346\236\220\357\274\210\344\270\203\357\274\211\357\274\232\346\216\245\345\217\243\345\261\202.md" @@ -0,0 +1,266 @@ +# MyBatis 源码分析(七):接口层 + +## sql 会话创建工厂 + +`SqlSessionFactoryBuilder` 经过复杂的解析逻辑之后,会根据全局配置创建 `DefaultSqlSessionFactory`,该类是 `sql` 会话创建工厂抽象接口 `SqlSessionFactory` 的默认实现,其提供了若干 `openSession` 方法用于打开一个会话,在会话中进行相关数据库操作。这些 `openSession` 方法最终都会调用 `openSessionFromDataSource` 或 `openSessionFromConnection` 创建会话,即基于数据源配置创建还是基于已有连接对象创建。 + +### 基于数据源配置创建会话 + +要使用数据源打开一个会话需要先从全局配置中获取当前生效的数据源环境配置,如果没有生效配置或没用设置可用的事务工厂,就会创建一个 `ManagedTransactionFactory` 实例作为默认事务工厂实现,其与 `MyBatis` 提供的另一个事务工厂实现 `JdbcTransactionFactory` 的区别在于其生成的事务实现 `ManagedTransaction` 的提交和回滚方法是空实现,即希望将事务管理交由外部容器管理。 + +```java + private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) { + Transaction tx = null; + try { + final Environment environment = configuration.getEnvironment(); + // 获取事务工厂 + final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); + // 创建事务配置 + tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit); + // 创建执行器 + final Executor executor = configuration.newExecutor(tx, execType); + // 创建 sql 会话 + return new DefaultSqlSession(configuration, executor, autoCommit); + } catch (Exception e) { + closeTransaction(tx); // may have fetched a connection so lets call close() + throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 获取生效数据源环境配置的事务工厂 + */ + private TransactionFactory getTransactionFactoryFromEnvironment(Environment environment) { + if (environment == null || environment.getTransactionFactory() == null) { + // 未配置数据源环境或事务工厂,默认使用 ManagedTransactionFactory + return new ManagedTransactionFactory(); + } + return environment.getTransactionFactory(); + } +``` + +随后会根据入参传入的 `execType` 选择对应的执行器 `Executor`,`execType` 的取值来源于 `ExecutorType`,这是一个枚举类。在下一章将会详细分析各类 `Executor` 的作用及其实现。 + +获取到事务工厂配置和执行器对象后会结合传入的数据源自动提交属性创建 `DefaultSqlSession`,即 `sql` 会话对象。 + +### 基于数据库连接创建会话 + +基于连接创建会话的流程大致与基于数据源配置创建相同,区别在于自动提交属性 `autoCommit` 是从连接对象本身获取的。 + +```java + private SqlSession openSessionFromConnection(ExecutorType execType, Connection connection) { + try { + // 获取自动提交配置 + boolean autoCommit; + try { + autoCommit = connection.getAutoCommit(); + } catch (SQLException e) { + // Failover to true, as most poor drivers + // or databases won't support transactions + autoCommit = true; + } + final Environment environment = configuration.getEnvironment(); + // 获取事务工厂 + final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment); + // 创建事务配置 + final Transaction tx = transactionFactory.newTransaction(connection); + // 创建执行器 + final Executor executor = configuration.newExecutor(tx, execType); + // 创建 sql 会话 + return new DefaultSqlSession(configuration, executor, autoCommit); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } +``` + +## sql 会话 + +`SqlSession` 是 `MyBatis` 面向用户编程的接口,其提供了一系列方法用于执行相关数据库操作,默认实现为 `DefaultSqlSession`,在该类中,增删查改对应的操作最终会调用 `selectList`、`select` 和 `update` 方法,其分别用于普通查询、执行存储过程和修改数据库记录。 + +```java + /** + * 查询结果集 + */ + @Override + public List selectList(String statement, Object parameter, RowBounds rowBounds) { + try { + MappedStatement ms = configuration.getMappedStatement(statement); + return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 调用存储过程 + */ + @Override + public void select(String statement, Object parameter, RowBounds rowBounds, ResultHandler handler) { + try { + MappedStatement ms = configuration.getMappedStatement(statement); + executor.query(ms, wrapCollection(parameter), rowBounds, handler); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error querying database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 修改 + */ + @Override + public int update(String statement, Object parameter) { + try { + dirty = true; + MappedStatement ms = configuration.getMappedStatement(statement); + return executor.update(ms, wrapCollection(parameter)); + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error updating database. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } +``` + +以上操作均是根据传入的 `statement` 名称到全局配置中查找对应的 `MappedStatement` 对象,并将操作委托给执行器对象 `executor` 完成。`select`、`selectMap` 等方法则是对 `selectList` 方法返回的结果集做处理来实现的。 + +此外,提交和回滚方法也是基于 `executor` 实现的。 + +```java + /** + * 提交事务 + */ + @Override + public void commit(boolean force) { + try { + executor.commit(isCommitOrRollbackRequired(force)); + dirty = false; + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error committing transaction. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 回滚事务 + */ + @Override + public void rollback(boolean force) { + try { + executor.rollback(isCommitOrRollbackRequired(force)); + dirty = false; + } catch (Exception e) { + throw ExceptionFactory.wrapException("Error rolling back transaction. Cause: " + e, e); + } finally { + ErrorContext.instance().reset(); + } + } + + /** + * 非自动提交且事务未提交 || 强制提交或回滚 时返回 true + */ + private boolean isCommitOrRollbackRequired(boolean force) { + return (!autoCommit && dirty) || force; + } +``` + +在执行 `update` 方法时,会设置 `dirty` 属性为 `true` ,意为事务还未提交,当事务提交或回滚后才会将 `dirty` 属性修改为 `false`。如果当前会话不是自动提交且 `dirty` 熟悉为 `true`,或者设置了强制提交或回滚的标志,则会将强制标志提交给 `executor` 处理。 + +## Sql 会话管理器 + +`SqlSessionManager` 同时实现了 `SqlSessionFactory` 和 `SqlSession` 接口,使得其既能够创建 `sql` 会话,又能够执行 `sql` 会话的相关数据库操作。 + +```java + /** + * sql 会话创建工厂 + */ + private final SqlSessionFactory sqlSessionFactory; + + /** + * sql 会话代理对象 + */ + private final SqlSession sqlSessionProxy; + + /** + * 保存线程对应 sql 会话 + */ + private final ThreadLocal localSqlSession = new ThreadLocal<>(); + + private SqlSessionManager(SqlSessionFactory sqlSessionFactory) { + this.sqlSessionFactory = sqlSessionFactory; + this.sqlSessionProxy = (SqlSession) Proxy.newProxyInstance( + SqlSessionFactory.class.getClassLoader(), + new Class[]{SqlSession.class}, + new SqlSessionInterceptor()); + } + + @Override + public SqlSession openSession() { + return sqlSessionFactory.openSession(); + } + + /** + * 设置当前线程对应的 sql 会话 + */ + public void startManagedSession() { + this.localSqlSession.set(openSession()); + } + + /** + * sql 会话代理逻辑 + */ + private class SqlSessionInterceptor implements InvocationHandler { + + public SqlSessionInterceptor() { + // Prevent Synthetic Access + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + // 获取当前线程对应的 sql 会话对象并执行对应方法 + final SqlSession sqlSession = SqlSessionManager.this.localSqlSession.get(); + if (sqlSession != null) { + try { + return method.invoke(sqlSession, args); + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } else { + // 如果当前线程没有对应的 sql 会话,默认创建不自动提交的 sql 会话 + try (SqlSession autoSqlSession = openSession()) { + try { + final Object result = method.invoke(autoSqlSession, args); + autoSqlSession.commit(); + return result; + } catch (Throwable t) { + autoSqlSession.rollback(); + throw ExceptionUtil.unwrapThrowable(t); + } + } + } + } + } +``` + +`SqlSessionManager` 的构造方法要求 `SqlSessionFactory` 对象作为入参传入,其各个创建会话的方法实际是由该传入对象完成的。执行 `sql` 会话的操作由 `sqlSessionProxy` 对象完成,这是一个由 `JDK` 动态代理创建的对象,当执行方法时会去 `ThreadLocal` 对象中查找当前线程有没有对应的 `sql` 会话对象,如果有则使用已有的会话对象执行,否则创建新的会话对象执行,而线程对应的会话对象需要使用 `startManagedSession` 方法来维护。 + +之所以 `SqlSessionManager` 需要为每个线程维护会话对象,是因为 `DefaultSqlSession` 是非线程安全的,多线程操作会导致执行错误。如上文中提到的 `dirty` 属性,其修改是没有经过任何同步操作的。 + +## 小结 + +`SqlSession` 是 `MyBatis` 提供的面向开发者编程的接口,其提供了一系列数据库相关操作,并屏蔽了底层细节。使用 `MyBatis` 的正确方式应该是像 `SqlSessionManager` 那样为每个线程创建 `sql` 会话对象,避免造成线程安全问题。 + +- `org.apache.ibatis.session.SqlSessionFactory`:`sql` 会话创建工厂。 +- `org.apache.ibatis.session.defaults.DefaultSqlSessionFactory`: `sql` 会话创建工厂默认实现。 +- `org.apache.ibatis.session.SqlSession`:`sql` 会话。 +- `org.apache.ibatis.session.defaults.DefaultSqlSession`:`sql` 会话默认实现。 +- `org.apache.ibatis.session.SqlSessionManager`:`sql` 会话管理器 \ No newline at end of file diff --git a/docs/source/mybatis/1-overview.md b/docs/source/mybatis/1-overview.md new file mode 100644 index 0000000..550a96a --- /dev/null +++ b/docs/source/mybatis/1-overview.md @@ -0,0 +1,43 @@ +--- +sidebar: heading +title: MyBatis源码分析 +category: 源码分析 +tag: + - MyBatis +head: + - - meta + - name: keywords + content: MyBatis面试题,MyBatis源码分析,MyBatis整体架构,MyBatis源码,Hibernate,Executor,MyBatis分页,MyBatis插件运行原理,MyBatis延迟加载,MyBatis预编译,一级缓存和二级缓存 + - - meta + - name: description + content: 高质量的MyBatis源码分析总结 +--- + +`MyBatis` 是一款旨在帮助开发人员屏蔽底层重复性原生 `JDBC` 代码的持久化框架,其支持通过映射文件配置或注解将 `ResultSet` 映射为 `Java` 对象。相对于其它 `ORM` 框架,`MyBatis` 更为轻量级,支持定制化 `SQL` 和动态 `SQL`,方便优化查询性能,同时包含了良好的缓存机制。 + +## MyBatis 整体架构 + +![](http://img.topjavaer.cn/img/202312231153527.png) + +### 基础支持层 + +- 反射模块:提供封装的反射 `API`,方便上层调用。 +- 类型转换:为简化配置文件提供了别名机制,并且实现了 `Java` 类型和 `JDBC` 类型的互转。 +- 日志模块:能够集成多种第三方日志框架。 +- 资源加载模块:对类加载器进行封装,提供加载类文件和其它资源文件的功能。 +- 数据源模块:提供数据源实现并能够集成第三方数据源模块。 +- 事务管理:可以和 `Spring` 集成开发,对事务进行管理。 +- 缓存模块:提供一级缓存和二级缓存,将部分请求拦截在缓存层。 +- `Binding` 模块:在调用 `SqlSession` 相应方法执行数据库操作时,需要指定映射文件中的 `SQL` 节点,`MyBatis` 通过 `Binding` 模块将自定义 `Mapper` 接口与映射文件关联,避免拼写等错误导致在运行时才发现相应异常。 + +### 核心处理层 + +- 配置解析:`MyBatis` 初始化时会加载配置文件、映射文件和 `Mapper` 接口的注解信息,解析后会以对象的形式保存到 `Configuration` 对象中。 +- `SQL` 解析与 `scripting` 模块:`MyBatis` 支持通过配置实现动态 `SQL`,即根据不同入参生成 `SQL`。 +- `SQL` 执行与结果解析:`Executor` 负责维护缓存和事务管理,并将数据库相关操作委托给 `StatementHandler`,`ParmeterHadler` 负责完成 `SQL` 语句的实参绑定并通过 `Statement` 对象执行 `SQL`,通过 `ResultSet` 返回结果,交由 `ResultSetHandler` 处理。 + +- 插件:支持开发者通过插件接口对 `MyBatis` 进行扩展。 + +### 接口层 + +`SqlSession` 接口定义了暴露给应用程序调用的 `API`,接口层在收到请求时会调用核心处理层的相应模块完成具体的数据库操作。 \ No newline at end of file diff --git a/docs/source/mybatis/2-reflect.md b/docs/source/mybatis/2-reflect.md new file mode 100644 index 0000000..06e4ea7 --- /dev/null +++ b/docs/source/mybatis/2-reflect.md @@ -0,0 +1,634 @@ +--- +sidebar: heading +title: MyBatis源码分析 +category: 源码分析 +tag: + - MyBatis +head: + - - meta + - name: keywords + content: MyBatis面试题,MyBatis源码分析,MyBatis整体架构,MyBatis反射,MyBatis源码,Hibernate,Executor,MyBatis分页,MyBatis插件运行原理,MyBatis延迟加载,MyBatis预编译,一级缓存和二级缓存 + - - meta + - name: description + content: 高质量的MyBatis源码分析总结 +--- + +大家好,我是大彬。今天分享Mybatis源码的反射模块。 + +`MyBatis` 在进行参数处理、结果映射时等操作时,会涉及大量的反射操作。为了简化这些反射相关操作,`MyBatis` 在 `org.apache.ibatis.reflection` 包下提供了专门的反射模块,对反射操作做了近一步封装,提供了更为简洁的 `API`。 + +## 缓存类的元信息 + +`MyBatis` 提供 `Reflector` 类来缓存类的字段名和 `getter/setter` 方法的元信息,使得反射时有更好的性能。使用方式是将原始类对象传入其构造方法,生成 `Reflector` 对象。 + +```java + public Reflector(Class clazz) { + type = clazz; + // 如果存在,记录无参构造方法 + addDefaultConstructor(clazz); + // 记录字段名与get方法、get方法返回值的映射关系 + addGetMethods(clazz); + // 记录字段名与set方法、set方法参数的映射关系 + addSetMethods(clazz); + // 针对没有getter/setter方法的字段,通过Filed对象的反射来设置和读取字段值 + addFields(clazz); + // 可读的字段名 + readablePropertyNames = getMethods.keySet().toArray(new String[getMethods.keySet().size()]); + // 可写的字段名 + writablePropertyNames = setMethods.keySet().toArray(new String[setMethods.keySet().size()]); + // 保存一份所有字段名大写与原始字段名的隐射 + for (String propName : readablePropertyNames) { + caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); + } + for (String propName : writablePropertyNames) { + caseInsensitivePropertyMap.put(propName.toUpperCase(Locale.ENGLISH), propName); + } + } +``` + +`addGetMethods` 和 `addSetMethods` 分别获取类的所有方法,从符合 `getter/setter` 规范的方法中解析出字段名,并记录方法的参数类型、返回值类型等信息: + +```java + private void addGetMethods(Class cls) { + // 字段名-get方法 + Map> conflictingGetters = new HashMap<>(); + // 获取类的所有方法,及其实现接口的方法,并根据方法签名去重 + Method[] methods = getClassMethods(cls); + for (Method method : methods) { + if (method.getParameterTypes().length > 0) { + // 过滤有参方法 + continue; + } + String name = method.getName(); + if ((name.startsWith("get") && name.length() > 3) + || (name.startsWith("is") && name.length() > 2)) { + // 由get属性获取对应的字段名(去除前缀,首字母转小写) + name = PropertyNamer.methodToProperty(name); + addMethodConflict(conflictingGetters, name, method); + } + } + // 保证每个字段只对应一个get方法 + resolveGetterConflicts(conflictingGetters); + } +``` + +对 `getter/setter` 方法进行去重是通过类似 `java.lang.String#getSignature:java.lang.reflect.Method` 的方法签名来实现的,如果子类在实现过程中,参数、返回值使用了不同的类型(使用原类型的子类),则会导致方法签名不一致,同一字段就会对应不同的 `getter/setter` 方法,因此需要进行去重。 + +> 分享一份大彬精心整理的大厂面试手册,包含计**算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> 链接:https://pan.xunlei.com/s/VNgU60NQQNSDaEy9z955oufbA1?pwd=y9fy# +> +> 备用链接:https://pan.quark.cn/s/cbbb681e7c19 + +```java + private void resolveGetterConflicts(Map> conflictingGetters) { + for (Entry> entry : conflictingGetters.entrySet()) { + Method winner = null; + // 属性名 + String propName = entry.getKey(); + for (Method candidate : entry.getValue()) { + if (winner == null) { + winner = candidate; + continue; + } + // 字段对应了多个get方法 + Class winnerType = winner.getReturnType(); + Class candidateType = candidate.getReturnType(); + if (candidateType.equals(winnerType)) { + // 返回值类型相同 + if (!boolean.class.equals(candidateType)) { + throw new ReflectionException( + "Illegal overloaded getter method with ambiguous type for property " + + propName + " in class " + winner.getDeclaringClass() + + ". This breaks the JavaBeans specification and can cause unpredictable results."); + } else if (candidate.getName().startsWith("is")) { + // 返回值为boolean的get方法可能有多个,如getIsSave和isSave,优先取is开头的 + winner = candidate; + } + } else if (candidateType.isAssignableFrom(winnerType)) { + // OK getter type is descendant + // 可能会出现接口中的方法返回值是List,子类实现方法返回值是ArrayList,使用子类返回值方法 + } else if (winnerType.isAssignableFrom(candidateType)) { + winner = candidate; + } else { + throw new ReflectionException( + "Illegal overloaded getter method with ambiguous type for property " + + propName + " in class " + winner.getDeclaringClass() + + ". This breaks the JavaBeans specification and can cause unpredictable results."); + } + } + // 记录字段名对应的get方法对象和返回值类型 + addGetMethod(propName, winner); + } + } +``` + +去重的方式是使用更规范的方法以及使用子类的方法。在确认字段名对应的唯一 `getter/setter` 方法后,记录方法名对应的方法、参数、返回值等信息。`MethodInvoker` 可用于调用 `Method` 类的 `invoke` 方法来执行 `getter/setter` 方法(`addSetMethods` 记录映射关系的方式与 `addGetMethods` 大致相同)。 + +```java +private void addGetMethod(String name, Method method) { + // 过滤$开头、serialVersionUID的get方法和getClass()方法 + if (isValidPropertyName(name)) { + // 字段名-对应get方法的MethodInvoker对象 + getMethods.put(name, new MethodInvoker(method)); + Type returnType = TypeParameterResolver.resolveReturnType(method, type); + // 字段名-运行时方法的真正返回类型 + getTypes.put(name, typeToClass(returnType)); + } +} +``` + +接下来会执行 `addFields` 方法,此方法针对没有 `getter/setter` 方法的字段,通过包装为 `SetFieldInvoker` 在需要时通过 `Field` 对象的反射来设置和读取字段值。 + +```java +private void addFields(Class clazz) { + Field[] fields = clazz.getDeclaredFields(); + for (Field field : fields) { + if (!setMethods.containsKey(field.getName())) { + // issue #379 - removed the check for final because JDK 1.5 allows + // modification of final fields through reflection (JSR-133). (JGB) + // pr #16 - final static can only be set by the classloader + int modifiers = field.getModifiers(); + if (!(Modifier.isFinal(modifiers) && Modifier.isStatic(modifiers))) { + // 非final的static变量,没有set方法,可以通过File对象做赋值操作 + addSetField(field); + } + } + if (!getMethods.containsKey(field.getName())) { + addGetField(field); + } + } + if (clazz.getSuperclass() != null) { + // 递归查找父类 + addFields(clazz.getSuperclass()); + } +} +``` + +## 抽象字段赋值与读取 + +`Invoker` 接口用于抽象设置和读取字段值的操作。对于有 `getter/setter` 方法的字段,通过 `MethodInvoker` 反射执行;对应其它字段,通过 `GetFieldInvoker` 和 `SetFieldInvoker` 操作 `Field` 对象的 `getter/setter` 方法反射执行。 + +```java +/** + * 用于抽象设置和读取字段值的操作 + * + * {@link MethodInvoker} 反射执行getter/setter方法 + * {@link GetFieldInvoker} {@link SetFieldInvoker} 反射执行Field对象的get/set方法 + * + * @author Clinton Begin + */ +public interface Invoker { + + /** + * 通过反射设置或读取字段值 + * + * @param target + * @param args + * @return + * @throws IllegalAccessException + * @throws InvocationTargetException + */ + Object invoke(Object target, Object[] args) throws IllegalAccessException, InvocationTargetException; + + /** + * 字段类型 + * + * @return + */ + Class getType(); +} +``` + +## 解析参数类型 + +针对 `Java-Type` 体系的多种实现,`TypeParameterResolver` 提供一系列方法来解析指定类中的字段、方法返回值或方法参数的类型。 + +`Type` 接口包含 4 个子接口和 1 个实现类: + +![Type接口](https://wch853.github.io/img/mybatis/Type%E6%8E%A5%E5%8F%A3.png) + +- `Class`:原始类型 +- `ParameterizedType`:泛型类型,如:`List` +- `TypeVariable`:泛型类型变量,如: `List` 中的 `T` +- `GenericArrayType`:组成元素是 `ParameterizedType` 或 `TypeVariable` 的数组类型,如:`List[]`、`T[]` +- `WildcardType`:通配符泛型类型变量,如:`List` 中的 `?` + +`TypeParameterResolver` 分别提供 `resolveFieldType`、`resolveReturnType`、`resolveParamTypes` 方法用于解析字段类型、方法返回值类型和方法入参类型,这些方法均调用 `resolveType` 来获取类型信息: + +```java +/** + * 获取类型信息 + * + * @param type 根据是否有泛型信息签名选择传入泛型类型或简单类型 + * @param srcType 引用字段/方法的类(可能是子类,字段和方法在父类声明) + * @param declaringClass 字段/方法声明的类 + * @return + */ +private static Type resolveType(Type type, Type srcType, Class declaringClass) { + if (type instanceof TypeVariable) { + // 泛型类型变量,如:List 中的 T + return resolveTypeVar((TypeVariable) type, srcType, declaringClass); + } else if (type instanceof ParameterizedType) { + // 泛型类型,如:List + return resolveParameterizedType((ParameterizedType) type, srcType, declaringClass); + } else if (type instanceof GenericArrayType) { + // TypeVariable/ParameterizedType 数组类型 + return resolveGenericArrayType((GenericArrayType) type, srcType, declaringClass); + } else { + // 原始类型,直接返回 + return type; + } +} +``` + +`resolveTypeVar` 用于解析泛型类型变量参数类型,如果字段或方法在当前类中声明,则返回泛型类型的上界或 `Object` 类型;如果在父类中声明,则递归解析父类;父类也无法解析,则递归解析实现的接口。 + +```java +private static Type resolveTypeVar(TypeVariable typeVar, Type srcType, Class declaringClass) { + Type result; + Class clazz; + if (srcType instanceof Class) { + // 原始类型 + clazz = (Class) srcType; + } else if (srcType instanceof ParameterizedType) { + // 泛型类型,如 TestObj + ParameterizedType parameterizedType = (ParameterizedType) srcType; + // 取原始类型TestObj + clazz = (Class) parameterizedType.getRawType(); + } else { + throw new IllegalArgumentException("The 2nd arg must be Class or ParameterizedType, but was: " + srcType.getClass()); + } + + if (clazz == declaringClass) { + // 字段就是在当前引用类中声明的 + Type[] bounds = typeVar.getBounds(); + if (bounds.length > 0) { + // 返回泛型类型变量上界,如:T extends String,则返回String + return bounds[0]; + } + // 没有上界返回Object + return Object.class; + } + + // 字段/方法在父类中声明,递归查找父类泛型 + Type superclass = clazz.getGenericSuperclass(); + result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superclass); + if (result != null) { + return result; + } + + // 递归泛型接口 + Type[] superInterfaces = clazz.getGenericInterfaces(); + for (Type superInterface : superInterfaces) { + result = scanSuperTypes(typeVar, srcType, declaringClass, clazz, superInterface); + if (result != null) { + return result; + } + } + return Object.class; +} +``` + +通过调用 `scanSuperTypes` 实现递归解析: + +```java +private static Type scanSuperTypes(TypeVariable typeVar, Type srcType, Class declaringClass, Class clazz, Type superclass) { + if (superclass instanceof ParameterizedType) { + // 父类是泛型类型 + ParameterizedType parentAsType = (ParameterizedType) superclass; + Class parentAsClass = (Class) parentAsType.getRawType(); + // 父类中的泛型类型变量集合 + TypeVariable[] parentTypeVars = parentAsClass.getTypeParameters(); + if (srcType instanceof ParameterizedType) { + // 子类可能对父类泛型变量做过替换,使用替换后的类型 + parentAsType = translateParentTypeVars((ParameterizedType) srcType, clazz, parentAsType); + } + if (declaringClass == parentAsClass) { + // 字段/方法在当前父类中声明 + for (int i = 0; i < parentTypeVars.length; i++) { + if (typeVar == parentTypeVars[i]) { + // 使用变量对应位置的真正类型(可能已经被替换),如父类 A,子类 B extends A,则返回String + return parentAsType.getActualTypeArguments()[i]; + } + } + } + // 字段/方法声明的类是当前父类的父类,继续递归 + if (declaringClass.isAssignableFrom(parentAsClass)) { + return resolveTypeVar(typeVar, parentAsType, declaringClass); + } + } else if (superclass instanceof Class && declaringClass.isAssignableFrom((Class) superclass)) { + // 父类是原始类型,继续递归父类 + return resolveTypeVar(typeVar, superclass, declaringClass); + } + return null; +} +``` + +解析方法返回值和方法参数的逻辑大致与解析字段类型相同,`MyBatis` 源码的`TypeParameterResolverTest` 类提供了相关的测试用例。 + +## 元信息工厂 + +`MyBatis` 还提供 `ReflectorFactory` 接口用于实现 `Reflector` 容器,其默认实现为 `DefaultReflectorFactory`,其中可以使用 `classCacheEnabled` 属性来配置是否使用缓存。 + +```java +public class DefaultReflectorFactory implements ReflectorFactory { + + /** + * 是否缓存Reflector类信息 + */ + private boolean classCacheEnabled = true; + + /** + * Reflector缓存容器 + */ + private final ConcurrentMap, Reflector> reflectorMap = new ConcurrentHashMap<>(); + + public DefaultReflectorFactory() { + } + + @Override + public boolean isClassCacheEnabled() { + return classCacheEnabled; + } + + @Override + public void setClassCacheEnabled(boolean classCacheEnabled) { + this.classCacheEnabled = classCacheEnabled; + } + + /** + * 获取类的Reflector信息 + * + * @param type + * @return + */ + @Override + public Reflector findForClass(Class type) { + if (classCacheEnabled) { + // synchronized (type) removed see issue #461 + // 如果缓存Reflector信息,放入缓存容器 + return reflectorMap.computeIfAbsent(type, Reflector::new); + } else { + return new Reflector(type); + } + } + +} +``` + +## 对象创建工厂 + +`ObjectFactory` 接口是 `MyBatis` 对象创建工厂,其默认实现 `DefaultObjectFactory` 通过构造器反射创建对象,支持使用无参构造器和有参构造器。 + +## 属性工具集 + +`MyBatis` 在映射文件定义 `resultMap` 支持如下形式: + +```xml + + + + ... + +``` + +`orders[0].items[0].name` 这样的表达式是由 `PropertyTokenizer` 解析的,其构造方法能够对表达式进行解析;同时还实现了 `Iterator` 接口,能够迭代解析表达式。 + +```java +public PropertyTokenizer(String fullname) { + // orders[0].items[0].name + int delim = fullname.indexOf('.'); + if (delim > -1) { + // name = orders[0] + name = fullname.substring(0, delim); + // children = items[0].name + children = fullname.substring(delim + 1); + } else { + name = fullname; + children = null; + } + // orders[0] + indexedName = name; + delim = name.indexOf('['); + if (delim > -1) { + // 0 + index = name.substring(delim + 1, name.length() - 1); + // order + name = name.substring(0, delim); + } +} + + /** + * 是否有children表达式继续迭代 + * + * @return + */ + @Override + public boolean hasNext() { + return children != null; + } + + /** + * 分解出的 . 分隔符的 children 表达式可以继续迭代 + * @return + */ + @Override + public PropertyTokenizer next() { + return new PropertyTokenizer(children); + } +``` + +`PropertyNamer` 可以根据 `getter/setter` 规范解析字段名称;`PropertyCopier` 则支持对有相同父类的对象,通过反射拷贝字段值。 + +## 封装类信息 + +`MetaClass` 类依赖 `PropertyTokenizer` 和 `Reflector` 查找表达式是否可以匹配 `Java` 对象中的字段,以及对应字段是否有 `getter/setter` 方法。 + +```java +/** + * 验证传入的表达式,是否存在指定的字段 + * + * @param name + * @param builder + * @return + */ +private StringBuilder buildProperty(String name, StringBuilder builder) { + // 映射文件表达式迭代器 + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + // 复杂表达式,如name = items[0].name,则prop.getName() = items + String propertyName = reflector.findPropertyName(prop.getName()); + if (propertyName != null) { + builder.append(propertyName); + // items. + builder.append("."); + // 加载内嵌字段类型对应的MetaClass + MetaClass metaProp = metaClassForProperty(propertyName); + // 迭代子字段 + metaProp.buildProperty(prop.getChildren(), builder); + } + } else { + // 非复杂表达式,获取字段名,如:userid->userId + String propertyName = reflector.findPropertyName(name); + if (propertyName != null) { + builder.append(propertyName); + } + } + return builder; +} +``` + +## 包装字段对象 + +相对于 `MetaClass` 关注类信息,`MetalObject` 关注的是对象的信息,除了保存传入的对象本身,还会为对象指定一个 `ObjectWrapper` 将对象包装起来。`ObejctWrapper` 体系如下: + +![ObjectWrapper体系](https://wch853.github.io/img/mybatis/ObjectWrapper%E4%BD%93%E7%B3%BB.png) + +`ObjectWrapper` 的默认实现包括了对 `Map`、`Collection` 和普通 `JavaBean` 的包装。`MyBatis` 还支持通过 `ObjectWrapperFactory` 接口对 `ObejctWrapper` 进行扩展,生成自定义的包装类。`MetaObject` 对对象的具体操作,就委托给真正的 `ObjectWrapper` 处理。 + +```java +private MetaObject(Object object, ObjectFactory objectFactory, ObjectWrapperFactory objectWrapperFactory, ReflectorFactory reflectorFactory) { + this.originalObject = object; + this.objectFactory = objectFactory; + this.objectWrapperFactory = objectWrapperFactory; + this.reflectorFactory = reflectorFactory; + + // 根据传入object类型不同,指定不同的wrapper + if (object instanceof ObjectWrapper) { + this.objectWrapper = (ObjectWrapper) object; + } else if (objectWrapperFactory.hasWrapperFor(object)) { + this.objectWrapper = objectWrapperFactory.getWrapperFor(this, object); + } else if (object instanceof Map) { + this.objectWrapper = new MapWrapper(this, (Map) object); + } else if (object instanceof Collection) { + this.objectWrapper = new CollectionWrapper(this, (Collection) object); + } else { + this.objectWrapper = new BeanWrapper(this, object); + } +} +``` + +例如赋值操作,`BeanWrapper` 的实现如下: + +```java + @Override + public void set(PropertyTokenizer prop, Object value) { + if (prop.getIndex() != null) { + // 当前表达式是集合,如:items[0],就需要获取items集合对象 + Object collection = resolveCollection(prop, object); + // 在集合的指定索引上赋值 + setCollectionValue(prop, collection, value); + } else { + // 解析完成,通过Invoker接口做赋值操作 + setBeanProperty(prop, object, value); + } + } + + protected Object resolveCollection(PropertyTokenizer prop, Object object) { + if ("".equals(prop.getName())) { + return object; + } else { + // 在对象信息中查到此字段对应的集合对象 + return metaObject.getValue(prop.getName()); + } + } +``` + +根据 `PropertyTokenizer` 对象解析出的当前字段是否存在 `index` 索引来判断字段是否为集合。如果当前字段对应集合,则需要在对象信息中查到此字段对应的集合对象: + +```javascript +public Object getValue(String name) { + PropertyTokenizer prop = new PropertyTokenizer(name); + if (prop.hasNext()) { + // 如果表达式仍可迭代,递归寻找字段对应的对象 + MetaObject metaValue = metaObjectForProperty(prop.getIndexedName()); + if (metaValue == SystemMetaObject.NULL_META_OBJECT) { + return null; + } else { + return metaValue.getValue(prop.getChildren()); + } + } else { + // 字段解析完成 + return objectWrapper.get(prop); + } +} +``` + +如果字段是简单类型,`BeanWrapper` 获取字段对应的对象逻辑如下: + +```java +@Override +public Object get(PropertyTokenizer prop) { + if (prop.getIndex() != null) { + // 集合类型,递归获取 + Object collection = resolveCollection(prop, object); + return getCollectionValue(prop, collection); + } else { + // 解析完成,反射读取 + return getBeanProperty(prop, object); + } +} +``` + +可以看到,仍然是会判断表达式是否迭代完成,如果未解析完字段会不断递归,直至找到对应的类型。前面说到 `Reflector` 创建过程中将对字段的读取和赋值操作通过 `Invoke` 接口抽象出来,针对最终获取的字段,此时就会调用 `Invoke` 接口对字段反射读取对象值: + +```java +/** + * 通过Invoker接口反射执行读取操作 + * + * @param prop + * @param object + */ +private Object getBeanProperty(PropertyTokenizer prop, Object object) { + try { + Invoker method = metaClass.getGetInvoker(prop.getName()); + try { + return method.invoke(object, NO_ARGUMENTS); + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } catch (RuntimeException e) { + throw e; + } catch (Throwable t) { + throw new ReflectionException("Could not get property '" + prop.getName() + "' from " + object.getClass() + ". Cause: " + t.toString(), t); + } +} +``` + +对象读取完毕再通过 `setCollectionValue` 方法对集合指定索引进行赋值或通过 `setBeanProperty` 方法对简单类型反射赋值。`MapWrapper` 的操作与 `BeanWrapper` 大致相同,`CollectionWrapper` 相对更会简单,只支持对原始集合对象进行添加操作。 + +## 小结 + +`MyBatis` 根据自身需求,对反射 `API` 做了近一步封装。其目的是简化反射操作,为对象字段的读取和赋值提供更好的性能。 + +- `org.apache.ibatis.reflection.Reflector`:缓存类的字段名和 getter/setter 方法的元信息,使得反射时有更好的性能。 +- `org.apache.ibatis.reflection.invoker.Invoker:`:用于抽象设置和读取字段值的操作。 +- `org.apache.ibatis.reflection.TypeParameterResolver`:针对 Java-Type 体系的多种实现,解析指定类中的字段、方法返回值或方法参数的类型。 +- `org.apache.ibatis.reflection.ReflectorFactory`:反射信息创建工厂抽象接口。 +- `org.apache.ibatis.reflection.DefaultReflectorFactory`:默认的反射信息创建工厂。 +- `org.apache.ibatis.reflection.factory.ObjectFactory`:MyBatis 对象创建工厂,其默认实现 DefaultObjectFactory 通过构造器反射创建对象。 +- `org.apache.ibatis.reflection.property`:property 工具包,针对映射文件表达式进行解析和 Java 对象的反射赋值。 +- `org.apache.ibatis.reflection.MetaClass`:依赖 PropertyTokenizer 和 Reflector 查找表达式是否可以匹配 Java 对象中的字段,以及对应字段是否有 getter/setter 方法。 +- `org.apache.ibatis.reflection.MetaObject`:对原始对象进行封装,将对象操作委托给 ObjectWrapper 处理。 +- `org.apache.ibatis.reflection.wrapper.ObjectWrapper`:对象包装类,封装对象的读取和赋值等操作。 + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# + +备用链接:https://pan.quark.cn/s/3f1321952a16 \ No newline at end of file diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2203--\345\237\272\347\241\200\346\224\257\346\214\201\346\250\241\345\235\227.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2203--\345\237\272\347\241\200\346\224\257\346\214\201\346\250\241\345\235\227.md" new file mode 100644 index 0000000..234ffcf --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2203--\345\237\272\347\241\200\346\224\257\346\214\201\346\250\241\345\235\227.md" @@ -0,0 +1,1563 @@ +## 类型转换 + +`JDBC` 规范定义的数据类型与 `Java` 数据类型并不是完全对应的,所以在 `PrepareStatement` 为 `SQL` 语句绑定参数时,需要从 `Java` 类型转为 `JDBC` 类型;而从结果集中获取数据时,则需要将 `JDBC` 类型转为 `Java` 类型。 + +### 类型转换操作 + +`MyBatis` 中的所有类型转换器都继承自 `BaseTypeHandler` 抽象类,此类实现了 `TypeHandler` 接口。接口中定义了 1 个向 `PreparedStatement` 对象中设置参数的方法和 3 个从结果集中取值的方法: + +```java + public interface TypeHandler { + + /** + * 为PreparedStatement对象设置参数 + * + * @param ps SQL 预编译对象 + * @param i 参数索引 + * @param parameter 参数值 + * @param jdbcType 参数 JDBC类型 + * @throws SQLException + */ + void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException; + + /** + * 根据列名从结果集中取值 + * + * @param rs 结果集 + * @param columnName 列名 + * @return + * @throws SQLException + */ + T getResult(ResultSet rs, String columnName) throws SQLException; + + /** + * 根据索引从结果集中取值 + * @param rs 结果集 + * @param columnIndex 索引 + * @return + * @throws SQLException + */ + T getResult(ResultSet rs, int columnIndex) throws SQLException; + + /** + * 根据索引从存储过程函数中取值 + * + * @param cs 存储过程对象 + * @param columnIndex 索引 + * @return + * @throws SQLException + */ + T getResult(CallableStatement cs, int columnIndex) throws SQLException; + + } +``` + +### BaseTypeHandler 及其实现 + +`BaseTypeHandler` 实现了 `TypeHandler` 接口,针对 `null` 和异常处理做了封装,但是具体逻辑封装成 4 个抽象方法仍交由相应的类型转换器子类实现,以 `IntegerTypeHandler` 为例,其实现如下: + +```java + public class IntegerTypeHandler extends BaseTypeHandler { + + @Override + public void setNonNullParameter(PreparedStatement ps, int i, Integer parameter, JdbcType jdbcType) + throws SQLException { + ps.setInt(i, parameter); + } + + @Override + public Integer getNullableResult(ResultSet rs, String columnName) + throws SQLException { + int result = rs.getInt(columnName); + // 如果列值为空值返回控制否则返回原值 + return result == 0 && rs.wasNull() ? null : result; + } + + @Override + public Integer getNullableResult(ResultSet rs, int columnIndex) + throws SQLException { + int result = rs.getInt(columnIndex); + return result == 0 && rs.wasNull() ? null : result; + } + + @Override + public Integer getNullableResult(CallableStatement cs, int columnIndex) + throws SQLException { + int result = cs.getInt(columnIndex); + return result == 0 && cs.wasNull() ? null : result; + } + } +``` + +其实现主要是调用 `JDBC API` 设置查询参数或取值,并对 `null` 等特定情况做特殊处理。 + +### 类型转换器注册 + +`TypeHandlerRegistry` 是 `TypeHandler` 的注册类,在其无参构造方法中维护了 `JavaType`、`JdbcType` 和 `TypeHandler` 的关系。其主要使用的容器如下: + +```java + /** + * JdbcType - TypeHandler对象 + * 用于将Jdbc类型转为Java类型 + */ + private final Map> jdbcTypeHandlerMap = new EnumMap<>(JdbcType.class); + + /** + * JavaType - JdbcType - TypeHandler对象 + * 用于将Java类型转为指定的Jdbc类型 + */ + private final Map>> typeHandlerMap = new ConcurrentHashMap<>(); + + /** + * TypeHandler类型 - TypeHandler对象 + * 注册所有的TypeHandler类型 + */ + private final Map, TypeHandler> allTypeHandlersMap = new HashMap<>(); +``` + +## 别名注册 + +### 别名转换器注册 + +`TypeAliasRegistry` 提供了多种方式用于为 `Java` 类型注册别名。包括直接指定别名、注解指定别名、为指定包下类型注册别名: + +```java + /** + * 注册指定包下所有类型别名 + * + * @param packageName + */ + public void registerAliases(String packageName) { + registerAliases(packageName, Object.class); + } + + /** + * 注册指定包下指定类型的别名 + * + * @param packageName + * @param superType + */ + public void registerAliases(String packageName, Class superType) { + ResolverUtil> resolverUtil = new ResolverUtil<>(); + // 找出该包下superType所有的子类 + resolverUtil.find(new ResolverUtil.IsA(superType), packageName); + Set>> typeSet = resolverUtil.getClasses(); + for (Class type : typeSet) { + // Ignore inner classes and interfaces (including package-info.java) + // Skip also inner classes. See issue #6 + if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { + registerAlias(type); + } + } + } + + /** + * 注册类型别名,默认为简单类名,优先从Alias注解获取 + * + * @param type + */ + public void registerAlias(Class type) { + String alias = type.getSimpleName(); + // 从Alias注解读取别名 + Alias aliasAnnotation = type.getAnnotation(Alias.class); + if (aliasAnnotation != null) { + alias = aliasAnnotation.value(); + } + registerAlias(alias, type); + } + + /** + * 注册类型别名 + * + * @param alias 别名 + * @param value 类型 + */ + public void registerAlias(String alias, Class value) { + if (alias == null) { + throw new TypeException("The parameter alias cannot be null"); + } + // issue #748 + String key = alias.toLowerCase(Locale.ENGLISH); + if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) { + throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'."); + } + typeAliases.put(key, value); + } + + /** + * 注册类型别名 + * @param alias 别名 + * @param value 指定类型全名 + */ + public void registerAlias(String alias, String value) { + try { + registerAlias(alias, Resources.classForName(value)); + } catch (ClassNotFoundException e) { + throw new TypeException("Error registering type alias " + alias + " for " + value + ". Cause: " + e, e); + } + } +``` + +所有别名均注册到名为 `typeAliases` 的容器中。`TypeAliasRegistry` 的无参构造方法默认为一些常用类型注册了别名,如 `Integer`、`String`、`byte[]` 等。 + +## 日志配置 + +`MyBatis` 支持与多种日志工具集成,包括 `Slf4j`、`log4j`、`log4j2`、`commons-logging` 等。这些第三方工具类对应日志的实现各有不同,`MyBatis` 通过适配器模式抽象了这些第三方工具的集成过程,按照一定的优先级选择具体的日志工具,并将真正的日志实现委托给选择的日志工具。 + +### 日志适配接口 + +`Log` 接口是 `MyBatis` 的日志适配接口,支持 `trace`、`debug`、`warn`、`error` 四种级别。 + +### 日志工厂 + +`LogFactory` 负责对第三方日志工具进行适配,在类加载时会通过静态代码块按顺序选择合适的日志实现。 + +```java + static { + // 按顺序加载日志实现,如果有某个第三方日志实现可以成功加载,则不继续加载其它实现 + tryImplementation(LogFactory::useSlf4jLogging); + tryImplementation(LogFactory::useCommonsLogging); + tryImplementation(LogFactory::useLog4J2Logging); + tryImplementation(LogFactory::useLog4JLogging); + tryImplementation(LogFactory::useJdkLogging); + tryImplementation(LogFactory::useNoLogging); + } + + /** + * 初始化 logConstructor + * + * @param runnable + */ + private static void tryImplementation(Runnable runnable) { + if (logConstructor == null) { + try { + // 同步执行 + runnable.run(); + } catch (Throwable t) { + // ignore + } + } + } + + /** + * 配置第三方日志实现适配器 + * + * @param implClass + */ + private static void setImplementation(Class implClass) { + try { + Constructor candidate = implClass.getConstructor(String.class); + Log log = candidate.newInstance(LogFactory.class.getName()); + if (log.isDebugEnabled()) { + log.debug("Logging initialized using '" + implClass + "' adapter."); + } + logConstructor = candidate; + } catch (Throwable t) { + throw new LogException("Error setting Log implementation. Cause: " + t, t); + } + } +``` + +`tryImplementation` 按顺序加载第三方日志工具的适配实现,如 `Slf4j` 的适配器 `Slf4jImpl`: + +```java +public Slf4jImpl(String clazz) { + Logger logger = LoggerFactory.getLogger(clazz); + + if (logger instanceof LocationAwareLogger) { + try { + // check for slf4j >= 1.6 method signature + logger.getClass().getMethod("log", Marker.class, String.class, int.class, String.class, Object[].class, Throwable.class); + log = new Slf4jLocationAwareLoggerImpl((LocationAwareLogger) logger); + return; + } catch (SecurityException | NoSuchMethodException e) { + // fail-back to Slf4jLoggerImpl + } + } + + // Logger is not LocationAwareLogger or slf4j version < 1.6 + log = new Slf4jLoggerImpl(logger); +} +``` + +如果 `Slf4jImpl` 能成功执行构造方法,则 `LogFactory` 的 `logConstructor` 被成功赋值,`MyBatis` 就找到了合适的日志实现,可以通过 `getLog` 方法获取 `Log` 对象。 + +### JDBC 日志代理 + +`org.apache.ibatis.logging.jdbc` 包提供了 `Connection`、`PrepareStatement`、`Statement`、`ResultSet` 类中的相关方法执行的日志记录代理。`BaseJdbcLogger` 在创建时整理了 `PreparedStatement` 执行的相关方法名,并提供容器保存列值: + +```java + /** + * PreparedStatement 接口中的 set* 方法名称集合 + */ + protected static final Set SET_METHODS; + + /** + * PreparedStatement 接口中的 部分执行方法 + */ + protected static final Set EXECUTE_METHODS = new HashSet<>(); + + /** + * 列名-列值 + */ + private final Map columnMap = new HashMap<>(); + + /** + * 列名集合 + */ + private final List columnNames = new ArrayList<>(); + + /** + * 列值集合 + */ + private final List columnValues = new ArrayList<>(); + + static { + SET_METHODS = Arrays.stream(PreparedStatement.class.getDeclaredMethods()) + .filter(method -> method.getName().startsWith("set")) + .filter(method -> method.getParameterCount() > 1) + .map(Method::getName) + .collect(Collectors.toSet()); + + EXECUTE_METHODS.add("execute"); + EXECUTE_METHODS.add("executeUpdate"); + EXECUTE_METHODS.add("executeQuery"); + EXECUTE_METHODS.add("addBatch"); + } + + protected void setColumn(Object key, Object value) { + columnMap.put(key, value); + columnNames.add(key); + columnValues.add(value); + } +``` + +`ConnectionLogger`、`PreparedStatementLogger`、`StatementLogger`、`ResultSetLogger` 都继承自 `BaseJdbcLogger`,并实现了 `InvocationHandler` 接口,在运行时通过 `JDK` 动态代理实现代理类,针对相关方法执行打印日志。如下是 `ConnectionLogger` 对 `InvocationHandler` 接口的实现: + +```java + @Override + public Object invoke(Object proxy, Method method, Object[] params) + throws Throwable { + try { + if (Object.class.equals(method.getDeclaringClass())) { + return method.invoke(this, params); + } + if ("prepareStatement".equals(method.getName())) { + if (isDebugEnabled()) { + debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); + } + PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); + // 执行创建PreparedStatement方法,使用PreparedStatementLogger代理 + stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); + return stmt; + } else if ("prepareCall".equals(method.getName())) { + if (isDebugEnabled()) { + debug(" Preparing: " + removeBreakingWhitespace((String) params[0]), true); + } + PreparedStatement stmt = (PreparedStatement) method.invoke(connection, params); + stmt = PreparedStatementLogger.newInstance(stmt, statementLog, queryStack); + return stmt; + } else if ("createStatement".equals(method.getName())) { + Statement stmt = (Statement) method.invoke(connection, params); + stmt = StatementLogger.newInstance(stmt, statementLog, queryStack); + return stmt; + } else { + return method.invoke(connection, params); + } + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } +``` + +如果执行 `prepareStatement` 方法创建 `PrepareStatement` 对象,则会使用动态代理创建 `PreparedStatementLogger` 对象增强原有对象。在 `PreparedStatementLogger` 的代理逻辑中,如果执行的是 `executeQuery` 或 `getResultSet` 方法,其返回值 `ResultSet` 也会包装为 `ResultSetLogger` 作为代理,其代理逻辑为如果执行 `ResultSet` 的 `next` 方法,会打印结果集的行。 + +## 资源加载 + +### resources 与 ClassLoaderWrapper + +`MyBatis` 提供工具类 `Resources` 用于工具加载,其底层是通过 `ClassLoaderWrapper` 实现的。`ClassLoaderWrapper` 组合了一系列的 `ClassLoader`: + +```java + ClassLoader[] getClassLoaders(ClassLoader classLoader) { + return new ClassLoader[]{ + // 指定 ClassLoader + classLoader, + // 默认 ClassLoader,默认为 null + defaultClassLoader, + // 当前线程对应的 ClassLoader + Thread.currentThread().getContextClassLoader(), + // 当前类对应的 ClassLoader + getClass().getClassLoader(), + // 默认为 SystemClassLoader + systemClassLoader}; + } +``` + +当加载资源时会按组合的 `ClassLoader` 顺序依次尝试加载资源,例如 `classForName` 方法的实现: + +```java + Class classForName(String name, ClassLoader[] classLoader) throws ClassNotFoundException { + // 按组合顺序依次加载 + for (ClassLoader cl : classLoader) { + if (null != cl) { + try { + // 类加载 + Class c = Class.forName(name, true, cl); + if (null != c) { + return c; + } + } catch (ClassNotFoundException e) { + // we'll ignore this until all classloaders fail to locate the class + } + } + } + throw new ClassNotFoundException("Cannot find class: " + name); + } +``` + +### 加载指定包下的类 + +`ResolverUtil` 的 `find` 方法用于按条件加载指定包下的类。 + +```java +public ResolverUtil find(Test test, String packageName) { + // 包名.替换为/ + String path = getPackagePath(packageName); + + try { + // 虚拟文件系统加载文件路径 + List children = VFS.getInstance().list(path); + for (String child : children) { + if (child.endsWith(".class")) { + // 如果指定class文件符合条件,加入容器 + addIfMatching(test, child); + } + } + } catch (IOException ioe) { + log.error("Could not read package: " + packageName, ioe); + } + + return this; +} +``` + +`ResolverUtil` 还提供了一个内部接口 `Test` 用于判断指定类型是否满足条件,在 `ResolverUtil` 有两个默认实现:`IsA` 用于判断是否为指定类型的子类;`AnnotatedWith` 用于判断类上是否有指定注解。 + +```java + /** + * A Test that checks to see if each class is assignable to the provided class. Note + * that this test will match the parent type itself if it is presented for matching. + * + * 判断是否为子类 + */ + public static class IsA implements Test { + private Class parent; + + /** Constructs an IsA test using the supplied Class as the parent class/interface. */ + public IsA(Class parentType) { + this.parent = parentType; + } + + /** Returns true if type is assignable to the parent type supplied in the constructor. */ + @Override + public boolean matches(Class type) { + return type != null && parent.isAssignableFrom(type); + } + } + + /** + * A Test that checks to see if each class is annotated with a specific annotation. If it + * is, then the test returns true, otherwise false. + * + * 判断类上是否有指定注解 + */ + public static class AnnotatedWith implements Test { + private Class annotation; + + /** Constructs an AnnotatedWith test for the specified annotation type. */ + public AnnotatedWith(Class annotation) { + this.annotation = annotation; + } + + /** Returns true if the type is annotated with the class provided to the constructor. */ + @Override + public boolean matches(Class type) { + return type != null && type.isAnnotationPresent(annotation); + } + } +``` + +如果要加载的类符合条件,则将加载的类对象加入容器。 + +```java +protected void addIfMatching(Test test, String fqn) { + try { + String externalName = fqn.substring(0, fqn.indexOf('.')).replace('/', '.'); + ClassLoader loader = getClassLoader(); + if (log.isDebugEnabled()) { + log.debug("Checking to see if class " + externalName + " matches criteria [" + test + "]"); + } + // 加载类 + Class type = loader.loadClass(externalName); + if (test.matches(type)) { + // 符合条件,加入容器 + matches.add((Class) type); + } + } catch (Throwable t) { + log.warn("Could not examine class '" + fqn + "'" + " due to a " + + t.getClass().getName() + " with message: " + t.getMessage()); + } +} +``` + +## 数据源实现 + +`MyBatis` 提供了自己的数据源实现,分别为非池化数据源 `UnpooledDataSource` 和池化数据源 `PooledDataSource`。两个数据源都实现了 `javax.sql.DataSource` 接口并分别由 `UnpooledDataSourceFactory` 和 `PooledDataSourceFactory` 工厂类创建。两个工厂类又都实现了 `DataSourceFactory` 接口。 + +![MyBatis DataSource 体系](https://wch853.github.io/img/mybatis/DataSource%E4%BD%93%E7%B3%BB.png) + +### DataSourceFactory 实现 + +`UnpooledDataSourceFactory` 实现了 `DataSourceFactory` 接口的 `setProperties` 和 `getDataSource` 方法,分别用于在创建数据源工厂后配置数据源属性和获取数据源,`PooledDataSourceFactory` 继承了其实现: + +```java + public UnpooledDataSourceFactory() { + this.dataSource = new UnpooledDataSource(); + } + + /** + * 创建数据源工厂后配置数据源属性 + * + * @param props + */ + @Override + public void setProperties(Properties properties) { + Properties driverProperties = new Properties(); + // 数据源对象信息 + MetaObject metaDataSource = SystemMetaObject.forObject(dataSource); + for (Object key : properties.keySet()) { + String propertyName = (String) key; + if (propertyName.startsWith(DRIVER_PROPERTY_PREFIX)) { + // 数据源驱动相关属性 + String value = properties.getProperty(propertyName); + driverProperties.setProperty(propertyName.substring(DRIVER_PROPERTY_PREFIX_LENGTH), value); + } else if (metaDataSource.hasSetter(propertyName)) { + // 属性在数据源类中有相应的set方法 + String value = (String) properties.get(propertyName); + // 转换类型 + Object convertedValue = convertValue(metaDataSource, propertyName, value); + // 反射赋值 + metaDataSource.setValue(propertyName, convertedValue); + } else { + throw new DataSourceException("Unknown DataSource property: " + propertyName); + } + } + if (driverProperties.size() > 0) { + metaDataSource.setValue("driverProperties", driverProperties); + } + } + + /** + * 获取数据源 + * + * @return + */ + @Override + public DataSource getDataSource() { + return dataSource; + } +``` + +### 非池化数据源 + +`UnpooledDataSource` 在静态语句块中从数据源驱动管理器 `DriverManager` 获取所有已注册驱动并放入本地容器: + +```java + static { + // 从数据库驱动类中获取所有驱动 + Enumeration drivers = DriverManager.getDrivers(); + while (drivers.hasMoreElements()) { + Driver driver = drivers.nextElement(); + registeredDrivers.put(driver.getClass().getName(), driver); + } + } +``` + +此数据源获取连接的实现为调用 `doGetConnection` 方法,每次获取连接时先校验当前驱动是否注册,如果已注册则直接创建新连接,并配置自动提交属性和默认事务隔离级别: + +```java + /** + * 获取连接 + * + * @param properties + * @return + * @throws SQLException + */ + private Connection doGetConnection(Properties properties) throws SQLException { + // 校验当前驱动是否注册,如果未注册,加载驱动并注册 + initializeDriver(); + // 获取数据库连接 + Connection connection = DriverManager.getConnection(url, properties); + // 配置自动提交和默认事务隔离级别属性 + configureConnection(connection); + return connection; + } + + /** + * 校验当前驱动是否注册 + * + * @throws SQLException + */ + private synchronized void initializeDriver() throws SQLException { + if (!registeredDrivers.containsKey(driver)) { + // 当前驱动还未注册 + Class driverType; + try { + // 加载驱动类 + if (driverClassLoader != null) { + driverType = Class.forName(driver, true, driverClassLoader); + } else { + driverType = Resources.classForName(driver); + } + // DriverManager requires the driver to be loaded via the system ClassLoader. + // http://www.kfu.com/~nsayer/Java/dyn-jdbc.html + Driver driverInstance = (Driver)driverType.newInstance(); + // 注册驱动 + DriverManager.registerDriver(new DriverProxy(driverInstance)); + registeredDrivers.put(driver, driverInstance); + } catch (Exception e) { + throw new SQLException("Error setting driver on UnpooledDataSource. Cause: " + e); + } + } + } + + /** + * 配置自动提交和默认事务隔离级别属性 + * + * @param conn + * @throws SQLException + */ + private void configureConnection(Connection conn) throws SQLException { + if (autoCommit != null && autoCommit != conn.getAutoCommit()) { + conn.setAutoCommit(autoCommit); + } + if (defaultTransactionIsolationLevel != null) { + conn.setTransactionIsolation(defaultTransactionIsolationLevel); + } + } +``` + +### 池化数据源 + +数据库连接的创建是十分耗时的,在高并发环境下,频繁地创建和关闭连接会为系统带来很大的开销。而使用连接池实现对数据库连接的重用可以显著提高性能,避免反复创建连接。`MyBatis` 实现的连接池包含了维护连接队列、创建和保存连接、归还连接等功能。 + +#### 池化连接 + +`PooledConnection` 是 `MyBatis` 的池化连接实现。其构造方法中传入了驱动管理器创建的真正连接,并通过 `JDK` 动态代理创建了连接的代理对象: + +```java + public PooledConnection(Connection connection, PooledDataSource dataSource) { + this.hashCode = connection.hashCode(); + // 真正的数据库连接 + this.realConnection = connection; + // 数据源对象 + this.dataSource = dataSource; + // 连接创建时间 + this.createdTimestamp = System.currentTimeMillis(); + // 连接上次使用时间 + this.lastUsedTimestamp = System.currentTimeMillis(); + // 数据源有效标志 + this.valid = true; + // 创建连接代理 + this.proxyConnection = (Connection) Proxy.newProxyInstance(Connection.class.getClassLoader(), IFACES, this); + } +``` + +连接代理的逻辑如下,如果执行 `close` 方法,并不会真正的关闭连接,而是当作空闲连接交给数据源处理,根据连接池的状态选择将连接放入空闲队列或关闭连接;如果执行其它方法,则会判断当前连接是否有效,如果是无效连接会抛出异常: + +```java + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + String methodName = method.getName(); + if (CLOSE.hashCode() == methodName.hashCode() && CLOSE.equals(methodName)) { + // 调用连接关闭方法代理逻辑:处理空闲连接,放入空闲队列或关闭 + dataSource.pushConnection(this); + return null; + } + try { + if (!Object.class.equals(method.getDeclaringClass())) { + // issue #579 toString() should never fail + // throw an SQLException instead of a Runtime + // 执行其它方法,验证连接是否有效,如果是无效连接,抛出异常 + checkConnection(); + } + return method.invoke(realConnection, args); + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + } +``` + +#### 连接池状态 + +`PoolState` 维护了数据库连接池状态。其内部维护了两个容器,分别为空闲连接集合和活跃连接集合。 + +```java + /** + * 空闲连接集合 + */ + protected final List idleConnections = new ArrayList<>(); + + /** + * 活跃连接集合 + */ + protected final List activeConnections = new ArrayList<>(); +``` + +#### 获取连接 + +池化数据源 `PooledDataSource` 是依赖 `UnpooledDataSource` 实现的。其获取连接的方式是调用 `popConnection` 方法。在获取连接池同步锁后按照以下顺序尝试获取可用连接: + +- 从空闲队列获取连接 +- 活跃连接池未满,创建新连接 +- 检查最早的活跃连接是否超时 +- 等待释放连接 + +```java +private PooledConnection popConnection(String username, String password) throws SQLException { + // 等待连接标志 + boolean countedWait = false; + // 待获取的池化连接 + PooledConnection conn = null; + long t = System.currentTimeMillis(); + int localBadConnectionCount = 0; + + while (conn == null) { + // 循环获取连接 + synchronized (state) { + // 获取连接池的同步锁 + if (!state.idleConnections.isEmpty()) { + // Pool has available connection 连接池中有空闲连接 + conn = state.idleConnections.remove(0); + if (log.isDebugEnabled()) { + log.debug("Checked out connection " + conn.getRealHashCode() + " from pool."); + } + } else { + // Pool does not have available connection 连接池无可用连接 + if (state.activeConnections.size() < poolMaximumActiveConnections) { + // Can create new connection 活跃连接数小于设定的最大连接数,创建新的连接(从驱动管理器创建新的连接) + conn = new PooledConnection(dataSource.getConnection(), this); + if (log.isDebugEnabled()) { + log.debug("Created connection " + conn.getRealHashCode() + "."); + } + } else { + // Cannot create new connection 活跃连接数到达最大连接数 + PooledConnection oldestActiveConnection = state.activeConnections.get(0); + // 查询最早入队的活跃连接使用时间(即使用时间最长的活跃连接使用时间) + long longestCheckoutTime = oldestActiveConnection.getCheckoutTime(); + if (longestCheckoutTime > poolMaximumCheckoutTime) { + // Can claim overdue connection 超出活跃连接最大使用时间 + state.claimedOverdueConnectionCount++; + // 超时连接累计使用时长 + state.accumulatedCheckoutTimeOfOverdueConnections += longestCheckoutTime; + state.accumulatedCheckoutTime += longestCheckoutTime; + // 活跃连接队列移除当前连接 + state.activeConnections.remove(oldestActiveConnection); + if (!oldestActiveConnection.getRealConnection().getAutoCommit()) { + try { + // 创建的连接未自动提交,执行回滚 + oldestActiveConnection.getRealConnection().rollback(); + } catch (SQLException e) { + /* + Just log a message for debug and continue to execute the following + statement like nothing happened. + Wrap the bad connection with a new PooledConnection, this will help + to not interrupt current executing thread and give current thread a + chance to join the next competition for another valid/good database + connection. At the end of this loop, bad {@link @conn} will be set as null. + */ + log.debug("Bad connection. Could not roll back"); + } + } + // 包装新的池化连接 + conn = new PooledConnection(oldestActiveConnection.getRealConnection(), this); + conn.setCreatedTimestamp(oldestActiveConnection.getCreatedTimestamp()); + conn.setLastUsedTimestamp(oldestActiveConnection.getLastUsedTimestamp()); + // 设置原连接无效 + oldestActiveConnection.invalidate(); + if (log.isDebugEnabled()) { + log.debug("Claimed overdue connection " + conn.getRealHashCode() + "."); + } + } else { + // Must wait + try { + // 存活连接有效 + if (!countedWait) { + state.hadToWaitCount++; + countedWait = true; + } + if (log.isDebugEnabled()) { + log.debug("Waiting as long as " + poolTimeToWait + " milliseconds for connection."); + } + long wt = System.currentTimeMillis(); + // 释放锁等待连接,{@link PooledDataSource#pushConnection} 如果有连接空闲,会唤醒等待 + state.wait(poolTimeToWait); + // 记录等待时长 + state.accumulatedWaitTime += System.currentTimeMillis() - wt; + } catch (InterruptedException e) { + break; + } + } + } + } + if (conn != null) { + // ping to server and check the connection is valid or not + if (conn.isValid()) { + // 连接有效 + if (!conn.getRealConnection().getAutoCommit()) { + // 非自动提交的连接,回滚上次任务 + conn.getRealConnection().rollback(); + } + conn.setConnectionTypeCode(assembleConnectionTypeCode(dataSource.getUrl(), username, password)); + // 设置连接新的使用时间 + conn.setCheckoutTimestamp(System.currentTimeMillis()); + conn.setLastUsedTimestamp(System.currentTimeMillis()); + // 添加到活跃连接集合队尾 + state.activeConnections.add(conn); + // 连接请求次数+1 + state.requestCount++; + // 请求连接花费的时间 + state.accumulatedRequestTime += System.currentTimeMillis() - t; + } else { + // 未获取到连接 + if (log.isDebugEnabled()) { + log.debug("A bad connection (" + conn.getRealHashCode() + ") was returned from the pool, getting another connection."); + } + // 因为没有空闲连接导致获取连接失败次数+1 + state.badConnectionCount++; + // 本次请求获取连接失败数+1 + localBadConnectionCount++; + conn = null; + if (localBadConnectionCount > (poolMaximumIdleConnections + poolMaximumLocalBadConnectionTolerance)) { + // 超出获取连接失败的可容忍次数,抛出异常 + if (log.isDebugEnabled()) { + log.debug("PooledDataSource: Could not get a good connection to the database."); + } + throw new SQLException("PooledDataSource: Could not get a good connection to the database."); + } + } + } + } + + } + + if (conn == null) { + if (log.isDebugEnabled()) { + log.debug("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); + } + throw new SQLException("PooledDataSource: Unknown severe error condition. The connection pool returned a null connection."); + } + + return conn; +} +``` + +如果暂时获取不到可用连接,则当前线程进入等待,等待新的空闲连接产生唤醒等待或等待超时后重新尝试获取连接。当尝试次数到达指定上限,会抛出异常跳出等待。 + +#### 判断连接有效性 + +如果可以从连接池中获取连接,会调用 `PooledConnection#isValid` 方法判断连接是否有效,其逻辑为 `PooledConnection` 对象自身维护的标志有效且连接存活。判断连接存活的实现如下: + +```java + protected boolean pingConnection(PooledConnection conn) { + boolean result = true; + + try { + // 连接是否关闭 + result = !conn.getRealConnection().isClosed(); + } catch (SQLException e) { + if (log.isDebugEnabled()) { + log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage()); + } + result = false; + } + + if (result) { + if (poolPingEnabled) { + // 使用语句检测连接是否可用开关开启 + if (poolPingConnectionsNotUsedFor >= 0 && conn.getTimeElapsedSinceLastUse() > poolPingConnectionsNotUsedFor) { + // 距上次连接使用经历时长超过设置的阈值 + try { + if (log.isDebugEnabled()) { + log.debug("Testing connection " + conn.getRealHashCode() + " ..."); + } + // 验证连接是否可用 + Connection realConn = conn.getRealConnection(); + try (Statement statement = realConn.createStatement()) { + statement.executeQuery(poolPingQuery).close(); + } + if (!realConn.getAutoCommit()) { + // 未自动提交执行回滚 + realConn.rollback(); + } + result = true; + if (log.isDebugEnabled()) { + log.debug("Connection " + conn.getRealHashCode() + " is GOOD!"); + } + } catch (Exception e) { + log.warn("Execution of ping query '" + poolPingQuery + "' failed: " + e.getMessage()); + try { + // 抛出异常,连接不可用,关闭连接 + conn.getRealConnection().close(); + } catch (Exception e2) { + //ignore + } + result = false; + if (log.isDebugEnabled()) { + log.debug("Connection " + conn.getRealHashCode() + " is BAD: " + e.getMessage()); + } + } + } + } + } + return result; + } +``` + +默认调用 `Connection#isClosed` 方法判断连接是否存活,如果连接存活,则可以选择执行 `SQL` 语句来进一步判断连接的有效性。是否进一步验证、验证使用的 `SQL` 语句、验证的时间条件,都是可配置的。 + +#### 处理空闲连接 + +在池化连接的代理连接执行关闭操作时,会转为对空闲连接的处理,其实现逻辑如下: + +```java + protected void pushConnection(PooledConnection conn) throws SQLException { + + synchronized (state) { + // 获取连接池状态同步锁,活跃连接队列移除当前连接 + state.activeConnections.remove(conn); + if (conn.isValid()) { + // 连接有效 + if (state.idleConnections.size() < poolMaximumIdleConnections && conn.getConnectionTypeCode() == expectedConnectionTypeCode) { + // 空闲连接数小于最大空闲连接数,累计连接使用时长 + state.accumulatedCheckoutTime += conn.getCheckoutTime(); + if (!conn.getRealConnection().getAutoCommit()) { + // 未自动提交连接回滚上次事务 + conn.getRealConnection().rollback(); + } + // 包装成新的代理连接 + PooledConnection newConn = new PooledConnection(conn.getRealConnection(), this); + // 将新连接放入空闲队列 + state.idleConnections.add(newConn); + // 设置相关统计时间戳 + newConn.setCreatedTimestamp(conn.getCreatedTimestamp()); + newConn.setLastUsedTimestamp(conn.getLastUsedTimestamp()); + // 老连接设置失效 + conn.invalidate(); + if (log.isDebugEnabled()) { + log.debug("Returned connection " + newConn.getRealHashCode() + " to pool."); + } + // 唤醒等待连接的线程,通知有新连接可以使用 + state.notifyAll(); + } else { + // 空闲连接数达到最大空闲连接数 + state.accumulatedCheckoutTime += conn.getCheckoutTime(); + if (!conn.getRealConnection().getAutoCommit()) { + // 未自动提交连接回滚上次事务 + conn.getRealConnection().rollback(); + } + // 关闭多余的连接 + conn.getRealConnection().close(); + if (log.isDebugEnabled()) { + log.debug("Closed connection " + conn.getRealHashCode() + "."); + } + // 连接设置失效 + conn.invalidate(); + } + } else { + if (log.isDebugEnabled()) { + log.debug("A bad connection (" + conn.getRealHashCode() + ") attempted to return to the pool, discarding connection."); + } + // 连接无效次数+1 + state.badConnectionCount++; + } + } + } +``` + +如果当前空闲连接数小于最大空闲连接数,则将空闲连接放入空闲队列,否则关闭连接。 + +#### 处理配置变更 + +在相关配置变更后,`MyBatis` 会调用 `forceCloseAll` 关闭连接池中所有存活的连接: + +```java + public void forceCloseAll() { + synchronized (state) { + // 获取连接池状态同步锁 + expectedConnectionTypeCode = assembleConnectionTypeCode(dataSource.getUrl(), dataSource.getUsername(), dataSource.getPassword()); + for (int i = state.activeConnections.size(); i > 0; i--) { + try { + // 移除活跃连接 + PooledConnection conn = state.activeConnections.remove(i - 1); + // 原连接置为无效 + conn.invalidate(); + + Connection realConn = conn.getRealConnection(); + if (!realConn.getAutoCommit()) { + // 未提交连接回滚当前事务 + realConn.rollback(); + } + // 关闭连接 + realConn.close(); + } catch (Exception e) { + // ignore + } + } + for (int i = state.idleConnections.size(); i > 0; i--) { + try { + // 移除空闲连接 + PooledConnection conn = state.idleConnections.remove(i - 1); + // 原连接置为无效 + conn.invalidate(); + + Connection realConn = conn.getRealConnection(); + if (!realConn.getAutoCommit()) { + // 未提交连接回滚当前事务 + realConn.rollback(); + } + // 关闭连接 + realConn.close(); + } catch (Exception e) { + // ignore + } + } + } + if (log.isDebugEnabled()) { + log.debug("PooledDataSource forcefully closed/removed all connections."); + } + } +``` + +## 缓存实现 + +`Cache` 是 `MyBatis` 的缓存抽象接口,其要求实现如下方法: + +```java + public interface Cache { + + /** + * 缓存对象 id + * + * @return The identifier of this cache + */ + String getId(); + + /** + * 设置缓存 + * + * @param key Can be any object but usually it is a {@link CacheKey} + * @param value The result of a select. + */ + void putObject(Object key, Object value); + + /** + * 获取缓存 + * + * @param key The key + * @return The object stored in the cache. + */ + Object getObject(Object key); + + /** + * 移除缓存 + * + * @param key The key + * @return Not used + */ + Object removeObject(Object key); + + /** + * 清空缓存 + * + * Clears this cache instance. + */ + void clear(); + + /** + * Optional. This method is not called by the core. + * 获取缓存项数量 + * + * @return The number of elements stored in the cache (not its capacity). + */ + int getSize(); + + /** + * 获取读写锁 + * + * @return A ReadWriteLock + */ + ReadWriteLock getReadWriteLock(); + } +``` + +### 基础实现 + +`PerpetualCache` 类是基础实现类,核心是基于 `HashMap` 作为缓存维护容器。在此基础上,`MyBatis` 实现了多种缓存装饰器,用于满足不同的需求。 + +### 缓存装饰器 + +#### 同步操作 + +`SynchronizedCache` 针对缓存操作方法加上了 `synchronized` 关键字用于进行同步操作。 + +#### 阻塞操作 + +`BlockingCache` 在执行获取缓存操作时对 `key` 加锁,直到写缓存后释放锁,保证了相同 `key` 同一时刻只有一个线程执行数据库操作,其它线程在缓存层阻塞。 + +```java + /** + * 写缓存完成后释放锁 + */ + @Override + public void putObject(Object key, Object value) { + try { + delegate.putObject(key, value); + } finally { + releaseLock(key); + } + } + + @Override + public Object getObject(Object key) { + // 获取锁 + acquireLock(key); + Object value = delegate.getObject(key); + if (value != null) { + // 缓存不为空则释放锁,否则继续持有锁,在进行数据库操作后写缓存释放锁 + releaseLock(key); + } + return value; + } + + /** + * 删除指定 key 对应的缓存,并释放锁 + * + * @param key The key + * @return + */ + @Override + public Object removeObject(Object key) { + // despite of its name, this method is called only to release locks + releaseLock(key); + return null; + } + + /** + * 获取已有的锁或创建新锁 + * + * @param key + * @return + */ + private ReentrantLock getLockForKey(Object key) { + return locks.computeIfAbsent(key, k -> new ReentrantLock()); + } + + /** + * 根据 key 获取锁 + * + * @param key + */ + private void acquireLock(Object key) { + Lock lock = getLockForKey(key); + if (timeout > 0) { + try { + boolean acquired = lock.tryLock(timeout, TimeUnit.MILLISECONDS); + if (!acquired) { + throw new CacheException("Couldn't get a lock in " + timeout + " for the key " + key + " at the cache " + delegate.getId()); + } + } catch (InterruptedException e) { + throw new CacheException("Got interrupted while trying to acquire lock for key " + key, e); + } + } else { + lock.lock(); + } + } + + /** + * 释放锁 + * + * @param key + */ + private void releaseLock(Object key) { + ReentrantLock lock = locks.get(key); + if (lock.isHeldByCurrentThread()) { + lock.unlock(); + } + } +``` + +#### 日志记录 + +`LoggingCache` 是缓存日志装饰器。查询缓存时会记录查询日志并统计命中率。 + +```java +/** + * 查询缓存时记录查询日志并统计命中率 + * + * @param key The key + * @return + */ +@Override +public Object getObject(Object key) { + // 查询数+1 + requests++; + final Object value = delegate.getObject(key); + if (value != null) { + // 命中数+1 + hits++; + } + if (log.isDebugEnabled()) { + log.debug("Cache Hit Ratio [" + getId() + "]: " + getHitRatio()); + } + return value; +} +``` + +#### 定时清理 + +`ScheduledCache` 是缓存定时清理装饰器。在执行缓存相关操作时会根据设置的时间间隔判断是否需要清除全部的缓存。 + +```java + /** + * 操作缓存时判断是否需要清除所有缓存。 + * + * @return + */ + private boolean clearWhenStale() { + if (System.currentTimeMillis() - lastClear > clearInterval) { + clear(); + return true; + } + return false; + } +``` + +#### 序列化与反序列化 + +`SerializedCache` 是缓存序列化装饰器,其在写入时会将值序列化成对象流,并在读取时进行反序列化。 + +#### 事务操作 + +`TransactionalCache` 是事务缓存装饰器。在事务提交后再将缓存写入,如果发生回滚则不写入。 + +#### 先进先出 + +`FifoCache` 是先进先出缓存装饰器。其按写缓存顺序维护了一个缓存 `key` 队列,如果缓存项超出指定大小,则删除最先入队的缓存。 + +```java +/** + * 按写缓存顺序维护缓存 key 队列,缓存项超出指定大小,删除最先入队的缓存 + * + * @param key + */ +private void cycleKeyList(Object key) { + keyList.addLast(key); + if (keyList.size() > size) { + Object oldestKey = keyList.removeFirst(); + delegate.removeObject(oldestKey); + } +} +``` + +#### 最近最久未使用 + +`LruCache` 是缓存最近最久未使用装饰器。其基于 `LinkedHashMap` 维护了 `key` 的 `LRU` 顺序。 + +```java + public void setSize(final int size) { + // LinkedHashMap 在执行 get 方法后会将对应的 entry 移到队尾来维护使用顺序 + keyMap = new LinkedHashMap(size, .75F, true) { + private static final long serialVersionUID = 4267176411845948333L; + + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + boolean tooBig = size() > size; + if (tooBig) { + // 超出缓存项数量限制,获取最近最久未使用的key + eldestKey = eldest.getKey(); + } + return tooBig; + } + }; + } + + /** + * 更新缓存后检查是否需要删除最近最久未使用的缓存项 + */ + @Override + public void putObject(Object key, Object value) { + delegate.putObject(key, value); + cycleKeyList(key); + } + + private void cycleKeyList(Object key) { + keyMap.put(key, key); + if (eldestKey != null) { + delegate.removeObject(eldestKey); + eldestKey = null; + } + } +``` + +#### 软引用缓存 + +`SoftCache` 是缓存软引用装饰器,其使用了软引用 + 强引用队列的方式维护缓存。在写缓存操作中,写入的数据其实时缓存项的软引用包装对象,在 `Full GC` 时,如果没有一个强引用指向被包装的缓存项或缓存值,并且系统内存不足,缓存项就会被 `GC`,被回收对象进入指定的引用队列。 + +```java + /** + * 引用队列,用于记录已经被 GC 的 SoftEntry 对象 + */ + private final ReferenceQueue queueOfGarbageCollectedEntries; + + /** + * 写入缓存。 + * 不直接写缓存的值,而是写入缓存项对应的软引用 + */ + @Override + public void putObject(Object key, Object value) { + removeGarbageCollectedItems(); + // 在 Full GC 时,如果没有一个强引用指向被包装的缓存项或缓存值,并且系统内存不足,缓存项就会被回收,被回收对象进入指定的引用队列 + delegate.putObject(key, new SoftEntry(key, value, queueOfGarbageCollectedEntries)); + } + + /** + * 查询已被 GC 的软引用,删除对应的缓存项 + */ + private void removeGarbageCollectedItems() { + SoftEntry sv; + while ((sv = (SoftEntry) queueOfGarbageCollectedEntries.poll()) != null) { + delegate.removeObject(sv.key); + } + } + + /** + * 封装软引用对象 + */ + private static class SoftEntry extends SoftReference { + private final Object key; + + SoftEntry(Object key, Object value, ReferenceQueue garbageCollectionQueue) { + // 声明 value 为软引用对象 + super(value, garbageCollectionQueue); + // key 为强引用 + this.key = key; + } + } +``` + +在读取缓存时,如果软引用被回收,则删除对应的缓存项;否则将缓存项放入一个强引用队列中,该队列会将最新读取的缓存项放入队首,使得真正的缓存项有了强引用指向,其软引用包装就不会被垃圾回收。队列有数量限制,当超出限制时会删除队尾的缓存项。 + +```java + /** + * 获取缓存。 + * 如果软引用被回收则删除对应的缓存项,如果未回收则加入到有数量限制的 LRU 队列中 + * + * @param key The key + * @return + */ + @Override + public Object getObject(Object key) { + Object result = null; + @SuppressWarnings("unchecked") // assumed delegate cache is totally managed by this cache + SoftReference softReference = (SoftReference) delegate.getObject(key); + if (softReference != null) { + result = softReference.get(); + if (result == null) { + // 软引用已经被回收,删除对应的缓存项 + delegate.removeObject(key); + } else { + // 如果未被回收,增将软引用加入到 LRU 队列 + // See #586 (and #335) modifications need more than a read lock + synchronized (hardLinksToAvoidGarbageCollection) { + // 将对应的软引用 + hardLinksToAvoidGarbageCollection.addFirst(result); + if (hardLinksToAvoidGarbageCollection.size() > numberOfHardLinks) { + // 超出数量限制,删除最近最久未使用的软引用对象 + hardLinksToAvoidGarbageCollection.removeLast(); + } + } + } + } + return result; + } +``` + +#### 弱引用缓存 + +`WeakCache` 是缓存弱引用装饰器,使用弱引用 + 强引用队列的方式维护缓存,其实现方式与 `SoftCache` 是一致的。 + +## Binding 模块 + +为了避免因拼写等错误导致在运行期才发现执行方法找不到对应的 `SQL` 语句,`MyBatis` 使用 `Binding` 模块在启动时对执行方法校验,如果找不到对应的语句,则会抛出 `BindingException`。 + +`MyBatis` 一般将执行数据库操作的方法所在的接口称为 `Mapper`,`MapperRegistry` 用来注册 `Mapper` 接口类型与其代理创建工厂的映射,其提供 `addMapper` 和 `addMappers` 接口用于注册。`Mapper` 接口代理工厂是通过 `MapperProxyFactory` 创建,创建过程依赖 `MapperProxy` 提供的 `JDK` 动态代理: + +```java + protected T newInstance(MapperProxy mapperProxy) { + return (T) Proxy.newProxyInstance(mapperInterface.getClassLoader(), new Class[] { mapperInterface }, mapperProxy); + } + + /** + * 使用 Mapper 代理封装 SqlSession 相关操作 + * + * @param sqlSession + * @return + */ + public T newInstance(SqlSession sqlSession) { + final MapperProxy mapperProxy = new MapperProxy<>(sqlSession, mapperInterface, methodCache); + return newInstance(mapperProxy); + } +``` + +`MapperProxy` 的代理逻辑如下,在 `Mapper` 接口中的方法真正执行时,会为指定的非 `default` 方法创建方法信息和 `SQL` 执行信息缓存: + +```java + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + try { + if (Object.class.equals(method.getDeclaringClass())) { + // Object中的方法,直接执行 + return method.invoke(this, args); + } else if (isDefaultMethod(method)) { + // 当前方法是接口中的非abstract、非static的public方法,即高版本JDK中的default方法 + return invokeDefaultMethod(proxy, method, args); + } + } catch (Throwable t) { + throw ExceptionUtil.unwrapThrowable(t); + } + // 缓存 Mapper接口 对应的方法和 SQL 执行信息 + final MapperMethod mapperMethod = cachedMapperMethod(method); + // 执行 SQL + return mapperMethod.execute(sqlSession, args); + } + + /** + * 缓存 Mapper接口 对应的方法和 SQL 执行信息 + * + * @param method + * @return + */ + private MapperMethod cachedMapperMethod(Method method) { + return methodCache.computeIfAbsent(method, k -> new MapperMethod(mapperInterface, method, sqlSession.getConfiguration())); + } +``` + +缓存通过 `MapperMethod` 类来保存,其构造方法创建了 `SqlCommand` 和 `MethodSignature` 对象。 + +```java + public MapperMethod(Class mapperInterface, Method method, Configuration config) { + // SQL 执行信息 + this.command = new SqlCommand(config, mapperInterface, method); + // 获取方法参数和返回值相关信息 + this.method = new MethodSignature(config, mapperInterface, method); + } +``` + +`SqlCommand` 会根据接口和方法名找到对应的 `SQL statement` 对象: + +```java + private MappedStatement resolveMappedStatement(Class mapperInterface, String methodName, + Class declaringClass, Configuration configuration) { + // statementId 为接口名与方法名组合 + String statementId = mapperInterface.getName() + "." + methodName; + if (configuration.hasStatement(statementId)) { + // 配置中存在此 statementId,返回对应的 statement + return configuration.getMappedStatement(statementId); + } else if (mapperInterface.equals(declaringClass)) { + // 此方法就是在对应接口中声明的 + return null; + } + // 递归查找父类 + for (Class superInterface : mapperInterface.getInterfaces()) { + if (declaringClass.isAssignableFrom(superInterface)) { + MappedStatement ms = resolveMappedStatement(superInterface, methodName, + declaringClass, configuration); + if (ms != null) { + return ms; + } + } + } + return null; + } +``` + +而 `MethodSignature` 会获取方法相关信息,如返回值类型、是否返回 `void`、是否返回多值等。对于 `Param` 注解的解析也会保存下来(`MyBatis` 使用 `Param` 注解重置参数名)。 + +## 小结 + +`MyBatis` 提供了一系列工具和实现,用于为整个框架提供基础支持。 + +> 类型转换 + +- `org.apache.ibatis.type.TypeHandler`:类型转换器接口,抽象 `JDBC` 类型和 `Java` 类型互转逻辑。 +- `org.apache.ibatis.type.BaseTypeHandler`:`TypeHandler` 的抽象实现,针对 null 和异常处理做了封装,具体逻辑仍由相应的类型转换器实现。 +- `org.apache.ibatis.type.TypeHandlerRegistry`:`TypeHandler` 注册类,维护 `JavaType`、`JdbcType` 和 `TypeHandler` 关系。 + +> 别名注册 + +- `org.apache.ibatis.type.TypeAliasRegistry`:别名注册类。注册常用类型的别名,并提供多种注册别名的方式。 + +> 日志配置 + +- `org.apache.ibatis.logging.Log`:`MyBatis` 日志适配接口,支持 `trace`、`debug`、`warn`、`error` 四种级别。 +- `org.apache.ibatis.logging.LogFactory`:`MyBatis` 日志工厂,负责适配第三方日志实现。 +- `org.apache.ibatis.logging.jdbc`:`SQL` 执行日志工具包,针对执行 `Connection`、`PrepareStatement`、`Statement`、`ResultSet` 类中的相关方法,提供日志记录工具。 + +> 资源加载 + +- `org.apache.ibatis.io.Resources`:`MyBatis` 封装的资源加载工具类。 +- `org.apache.ibatis.io.ClassLoaderWrapper`:资源加载底层实现。组合多种 `ClassLoader` 按顺序尝试加载资源。 +- `org.apache.ibatis.io.ResolverUtil`:按条件加载指定包下的类。 + +> 数据源实现 + +- `org.apache.ibatis.datasource.DataSourceFactory`:数据源创建工厂接口。 +- `org.apache.ibatis.datasource.unpooled.UnpooledDataSourceFactory`:非池化数据源工厂。 +- `org.apache.ibatis.datasource.pooled.PooledDataSourceFactory`:池化数据源工厂。 +- `org.apache.ibatis.datasource.unpooled.UnpooledDataSource`:非池化数据源。 +- `org.apache.ibatis.datasource.pooled.PooledDataSource`:池化数据源。 +- `org.apache.ibatis.datasource.pooled.PooledConnection`:池化连接。 +- `org.apache.ibatis.datasource.pooled.PoolState`:连接池状态。 + +> 事务实现 + +- `org.apache.ibatis.transaction.Transaction`:事务抽象接口 +- `org.apache.ibatis.session.TransactionIsolationLevel`:事务隔离级别。 +- `org.apache.ibatis.transaction.TransactionFactory`:事务创建工厂抽象接口。 +- `org.apache.ibatis.transaction.jdbc.JdbcTransaction`:封装 `JDBC` 数据库事务操作。 +- `org.apache.ibatis.transaction.managed.ManagedTransaction`:数据库事务操作依赖外部管理。 + +> 缓存实现 + +- `org.apache.ibatis.cache.Cache`:缓存抽象接口。 +- `org.apache.ibatis.cache.impl.PerpetualCache`:使用 `HashMap` 作为缓存实现容器的 `Cache` 基本实现。 +- `org.apache.ibatis.cache.decorators.BlockingCache`:缓存阻塞装饰器。保证相同 `key` 同一时刻只有一个线程执行数据库操作,其它线程在缓存层阻塞。 +- `org.apache.ibatis.cache.decorators.FifoCache`:缓存先进先出装饰器。按写缓存顺序维护缓存 `key` 队列,缓存项超出指定大小,删除最先入队的缓存。 +- `org.apache.ibatis.cache.decorators.LruCache`:缓存最近最久未使用装饰器。基于 `LinkedHashMap` 维护了 `key` 的 `LRU` 顺序。 +- `org.apache.ibatis.cache.decorators.LoggingCache`:缓存日志装饰器。查询缓存时记录查询日志并统计命中率。 +- `org.apache.ibatis.cache.decorators.ScheduledCache`:缓存定时清理装饰器。 +- `org.apache.ibatis.cache.decorators.SerializedCache`:缓存序列化装饰器。 +- `org.apache.ibatis.cache.decorators.SynchronizedCache`:缓存同步装饰器。在缓存操作方法上使用 `synchronized` 关键字同步。 +- `org.apache.ibatis.cache.decorators.TransactionalCache`:事务缓存装饰器。在事务提交后再将缓存写入,如果发生回滚则不写入。 +- `org.apache.ibatis.cache.decorators.SoftCache`:缓存软引用装饰器。使用软引用 + 强引用队列的方式维护缓存。 +- `org.apache.ibatis.cache.decorators.WeakCache`:缓存弱引用装饰器。使用弱引用 + 强引用队列的方式维护缓存。 + +### Binding 模块 + +- `org.apache.ibatis.binding.MapperRegistry`: `Mapper` 接口注册类,管理 `Mapper` 接口类型和其代理创建工厂的映射。 +- `org.apache.ibatis.binding.MapperProxyFactory`:`Mapper` 接口代理创建工厂。 +- `org.apache.ibatis.binding.MapperProxy`:`Mapper` 接口方法代理逻辑,封装 `SqlSession` 相关操作。 +- `org.apache.ibatis.binding.MapperMethod`:封装 `Mapper` 接口对应的方法和 `SQL` 执行信息。 + diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2204--\350\277\220\350\241\214\346\227\266\351\205\215\347\275\256\350\247\243\346\236\220.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2204--\350\277\220\350\241\214\346\227\266\351\205\215\347\275\256\350\247\243\346\236\220.md" new file mode 100644 index 0000000..fe60300 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2204--\350\277\220\350\241\214\346\227\266\351\205\215\347\275\256\350\247\243\346\236\220.md" @@ -0,0 +1,609 @@ +在 `Spring` 与 `MyBatis` 的集成中,通常需要声明一个 `sqlSessionFactory` 用于初始化 `MyBatis`: + +```xml + + + + + + + +``` + +在 `bean` 初始化的时候会调用 `SqlSessionFactoryBean` 的 `afterPropertiesSet` 方法,在此方法中 `MyBatis` 使用 `XMLConfigBuilder` 对配置进行解析。 + +## BaseBuilder 体系 + +`XMLConfigBuilder` 是 `XML` 配置解析的入口,继承自 `BaseBuilder`,其为 `MyBatis` 初始化提供了一系列工具方法,如别名转换、类型转换、类加载等。 + +![](http://img.topjavaer.cn/img/202401061526966.png) + +## 全局配置对象 + +`XMLConfigBuilder` 在构造方法中创建了 `Configuration` 对象,这个对象中用于保存 `MyBatis` 相关的全部配置,包括运行行为、类型容器、别名容器、注册 `Mapper`、注册 `statement` 等。通过 `XMLConfigBuilder` 的 `parse` 方法可以看出,配置解析的目的就是为了获取 `Configuration` 对象。 + +```java + private XMLConfigBuilder(XPathParser parser, String environment, Properties props) { + // 创建全局配置 + super(new Configuration()); + ErrorContext.instance().resource("SQL Mapper Configuration"); + // 设置自定义配置 + this.configuration.setVariables(props); + // 解析标志 + this.parsed = false; + // 指定环境 + this.environment = environment; + // 包装配置 InputStream 的 XPathParser + this.parser = parser; + } + + public Configuration parse() { + if (parsed) { + throw new BuilderException("Each XMLConfigBuilder can only be used once."); + } + parsed = true; + // 读取 configuration 元素并解析 + parseConfiguration(parser.evalNode("/configuration")); + return configuration; + } +``` + +## 解析配置文件 + +配置解析分为多步。`MyBatis` 源码内置 `mybatis-config.xsd` 文件用于定义配置文件书写规则。 + +```java + private void parseConfiguration(XNode root) { + try { + //issue #117 read properties first + // 解析 properties 元素 + propertiesElement(root.evalNode("properties")); + // 加载 settings 配置并验证是否有效 + Properties settings = settingsAsProperties(root.evalNode("settings")); + // 配置自定义虚拟文件系统实现 + loadCustomVfs(settings); + // 配置自定义日志实现 + loadCustomLogImpl(settings); + // 解析 typeAliases 元素 + typeAliasesElement(root.evalNode("typeAliases")); + // 解析 plugins 元素 + pluginElement(root.evalNode("plugins")); + // 解析 objectFactory 元素 + objectFactoryElement(root.evalNode("objectFactory")); + // 解析 objectWrapperFactory 元素 + objectWrapperFactoryElement(root.evalNode("objectWrapperFactory")); + // 解析 reflectorFactory 元素 + reflectorFactoryElement(root.evalNode("reflectorFactory")); + // 将 settings 配置设置到全局配置中 + settingsElement(settings); + // read it after objectFactory and objectWrapperFactory issue #631 + // 解析 environments 元素 + environmentsElement(root.evalNode("environments")); + // 解析 databaseIdProvider 元素 + databaseIdProviderElement(root.evalNode("databaseIdProvider")); + // 解析 typeHandlers 元素 + typeHandlerElement(root.evalNode("typeHandlers")); + // 解析 mappers 元素 + mapperElement(root.evalNode("mappers")); + } catch (Exception e) { + throw new BuilderException("Error parsing SQL Mapper Configuration. Cause: " + e, e); + } + } +``` + +### 解析 properties 元素 + +`properties` 元素用于将自定义配置传递给 `MyBatis`,例如: + +```xml + + + + +``` + +其加载逻辑为将不同配置转为 `Properties` 对象,并设置到全局配置中: + +```java + private void propertiesElement(XNode context) throws Exception { + if (context != null) { + // 获取子元素属性 + Properties defaults = context.getChildrenAsProperties(); + // 读取 resource 属性 + String resource = context.getStringAttribute("resource"); + // 读取 url 属性 + String url = context.getStringAttribute("url"); + if (resource != null && url != null) { + // 不可均为空 + throw new BuilderException("The properties element cannot specify both a URL and a resource based property file reference. Please specify one or the other."); + } + // 加载指定路径文件,转为 properties + if (resource != null) { + defaults.putAll(Resources.getResourceAsProperties(resource)); + } else if (url != null) { + defaults.putAll(Resources.getUrlAsProperties(url)); + } + // 添加创建配置的附加属性 + Properties vars = configuration.getVariables(); + if (vars != null) { + defaults.putAll(vars); + } + parser.setVariables(defaults); + // 设置到全局配置中 + configuration.setVariables(defaults); + } + } +``` + +### 解析 settings 元素 + +`setteings` 元素中的各子元素定义了 `MyBatis` 的运行时行为,例如: + +```xml + + + + + + + + + + ... + +``` + +这些配置在 `Configuration` 类中都有对应的 `setter` 方法。`settings` 元素的解析方法对配置进行了验证: + +```java + private Properties settingsAsProperties(XNode context) { + if (context == null) { + return new Properties(); + } + // 获取子元素配置 + Properties props = context.getChildrenAsProperties(); + // Check that all settings are known to the configuration class + // 获取 Configuration 类的相关信息 + MetaClass metaConfig = MetaClass.forClass(Configuration.class, localReflectorFactory); + for (Object key : props.keySet()) { + if (!metaConfig.hasSetter(String.valueOf(key))) { + // 验证对应的 setter 方法存在,保证配置是有效的 + throw new BuilderException("The setting " + key + " is not known. Make sure you spelled it correctly (case sensitive)."); + } + } + return props; + } +``` + +如果不存在对应的配置,会抛出 `BuilderException` 异常,如果自定义配置都是生效的,随后会调用 `settingsElement` 方法将这些运行时行为设置到全局配置中。 + +### 解析 typeAliases 元素 + +`typeAliases` 元素用于定义类别名: + +```xml + + + + + +``` + +如果使用 `package` 元素注册别名,则对应包下的所有类都会注册到 `TypeAliasRegistry` 别名注册容器中;如果使用 `typeAlias` 元素,则会注册指定类到别名容器中。注册逻辑如下,如果没有指定别名,则优先从类的 `Alias` 注解获取别名,如果未在类上定义,则默认使用简单类名: + +```java + /** + * 注册指定包下所有类型别名 + * + * @param packageName + */ + public void registerAliases(String packageName) { + registerAliases(packageName, Object.class); + } + + /** + * 注册指定包下指定类型的别名 + * + * @param packageName + * @param superType + */ + public void registerAliases(String packageName, Class superType) { + ResolverUtil> resolverUtil = new ResolverUtil<>(); + // 找出该包下superType所有的子类 + resolverUtil.find(new ResolverUtil.IsA(superType), packageName); + Set>> typeSet = resolverUtil.getClasses(); + for (Class type : typeSet) { + // Ignore inner classes and interfaces (including package-info.java) + // Skip also inner classes. See issue #6 + if (!type.isAnonymousClass() && !type.isInterface() && !type.isMemberClass()) { + registerAlias(type); + } + } + } + + /** + * 注册类型别名,默认为简单类名,优先从 Alias 注解获取 + * + * @param type + */ + public void registerAlias(Class type) { + String alias = type.getSimpleName(); + // 从Alias注解读取别名 + Alias aliasAnnotation = type.getAnnotation(Alias.class); + if (aliasAnnotation != null) { + alias = aliasAnnotation.value(); + } + registerAlias(alias, type); + } + + /** + * 注册类型别名 + * + * @param alias 别名 + * @param value 类型 + */ + public void registerAlias(String alias, Class value) { + if (alias == null) { + throw new TypeException("The parameter alias cannot be null"); + } + // issue #748 + String key = alias.toLowerCase(Locale.ENGLISH); + if (typeAliases.containsKey(key) && typeAliases.get(key) != null && !typeAliases.get(key).equals(value)) { + throw new TypeException("The alias '" + alias + "' is already mapped to the value '" + typeAliases.get(key).getName() + "'."); + } + typeAliases.put(key, value); + } +``` + +### 解析 plugins 元素 + +插件是 `MyBatis` 提供的扩展机制之一,通过添加自定义插件可以实现在 `SQL` 执行过程中的某个时机进行拦截。 `plugins` 元素用于定义调用拦截器: + +```xml + + + + + +``` + +指定的 `interceptor` 需要实现 `org.apache.ibatis.plugin.Interceptor` 接口,在创建对象后被加到全局配置过滤器链中: + +```java + private void pluginElement(XNode parent) throws Exception { + if (parent != null) { + for (XNode child : parent.getChildren()) { + // 获取 interceptor 属性 + String interceptor = child.getStringAttribute("interceptor"); + // 从子元素中读取属性配置 + Properties properties = child.getChildrenAsProperties(); + // 加载指定拦截器并创建实例 + Interceptor interceptorInstance = (Interceptor) resolveClass(interceptor).newInstance(); + interceptorInstance.setProperties(properties); + // 加入全局配置拦截器链 + configuration.addInterceptor(interceptorInstance); + } + } + } +``` + +`objectFactory`、 `objectWrapperFactory`、`reflectorFactory` 元素的解析方式与 `plugins` 元素类似 ,指定的子类对象创建后被设置到全局对象中。 + +### 解析 environments 元素 + +在实际生产中,一个项目可能会分为多个不同的环境,通过配置`enviroments` 元素可以定义不同的数据环境,并在运行时使用指定的环境: + +```xml + + + + + + + + + + + + + + ... + + +``` + +在解析过程中,只有被 `default` 属性指定的数据环境才会被加载: + +```java + private void environmentsElement(XNode context) throws Exception { + if (context != null) { + if (environment == null) { + // 获取指定的数据源名 + environment = context.getStringAttribute("default"); + } + for (XNode child : context.getChildren()) { + // 环境配置 id + String id = child.getStringAttribute("id"); + if (isSpecifiedEnvironment(id)) { + // 加载指定环境配置 + // 解析 transactionManager 元素并创建事务工厂实例 + TransactionFactory txFactory = transactionManagerElement(child.evalNode("transactionManager")); + // 解析 dataSource 元素并创建数据源工厂实例 + DataSourceFactory dsFactory = dataSourceElement(child.evalNode("dataSource")); + // 创建数据源 + DataSource dataSource = dsFactory.getDataSource(); + // 创建环境 + Environment.Builder environmentBuilder = new Environment.Builder(id) + .transactionFactory(txFactory) + .dataSource(dataSource); + // 将环境配置信息设置到全局配置中 + configuration.setEnvironment(environmentBuilder.build()); + } + } + } + } + + /** + * 解析 transactionManager 元素并创建事务工厂实例 + * + * @param context + * @return + * @throws Exception + */ + private TransactionFactory transactionManagerElement(XNode context) throws Exception { + if (context != null) { + // 指定事务工厂类型 + String type = context.getStringAttribute("type"); + // 从子元素读取属性配置 + Properties props = context.getChildrenAsProperties(); + // 加载事务工厂并创建实例 + TransactionFactory factory = (TransactionFactory) resolveClass(type).newInstance(); + factory.setProperties(props); + return factory; + } + throw new BuilderException("Environment declaration requires a TransactionFactory."); + } + + /** + * 解析 dataSource 元素并创建数据源工厂实例 + * + * @param context + * @return + * @throws Exception + */ + private DataSourceFactory dataSourceElement(XNode context) throws Exception { + if (context != null) { + // 指定数据源工厂类型 + String type = context.getStringAttribute("type"); + // 从子元素读取属性配置 + Properties props = context.getChildrenAsProperties(); + // 加载数据源工厂并创建实例 + DataSourceFactory factory = (DataSourceFactory) resolveClass(type).newInstance(); + factory.setProperties(props); + return factory; + } + throw new BuilderException("Environment declaration requires a DataSourceFactory."); + } +``` + +### 解析 databaseIdProvider 元素 + +`MyBatis` 支持通过 `databaseIdProvider` 元素来指定支持的数据库的 `databaseId`,这样在映射配置文件中指定 `databaseId` 就能够与对应的数据源进行匹配: + +```xml + + + + + +``` + +在根据指定类型解析出对应的 `DatabaseIdProvider` 后,`MyBatis` 会根据数据源获取对应的厂商信息: + +```java + private void databaseIdProviderElement(XNode context) throws Exception { + DatabaseIdProvider databaseIdProvider = null; + if (context != null) { + String type = context.getStringAttribute("type"); + // awful patch to keep backward compatibility + if ("VENDOR".equals(type)) { + type = "DB_VENDOR"; + } + // 从子元素读取属性配置 + Properties properties = context.getChildrenAsProperties(); + // 加载数据库厂商信息配置类并创建实例 + databaseIdProvider = (DatabaseIdProvider) resolveClass(type).newInstance(); + databaseIdProvider.setProperties(properties); + } + Environment environment = configuration.getEnvironment(); + if (environment != null && databaseIdProvider != null) { + // 获取数据库厂商标识 + String databaseId = databaseIdProvider.getDatabaseId(environment.getDataSource()); + configuration.setDatabaseId(databaseId); + } + } +``` + +因为 `DB_VENDOR` 被指定为 `VendorDatabaseIdProvider` 的别名,所以默认的获取厂商信息的逻辑如下,当通过 `property` 属性指定了数据库产品名则使用指定的名称,否则使用数据库元信息对应的产品名。 + +```java + /** + * 根据数据源获取对应的厂商信息 + * + * @param dataSource + * @return + */ + @Override + public String getDatabaseId(DataSource dataSource) { + if (dataSource == null) { + throw new NullPointerException("dataSource cannot be null"); + } + try { + return getDatabaseName(dataSource); + } catch (Exception e) { + LogHolder.log.error("Could not get a databaseId from dataSource", e); + } + return null; + } + + @Override + public void setProperties(Properties p) { + this.properties = p; + } + + /** + * 如果传入的属性配置包含当前数据库产品名,返回指定的值,否则返回数据库产品名 + * + * @param dataSource + * @return + * @throws SQLException + */ + private String getDatabaseName(DataSource dataSource) throws SQLException { + String productName = getDatabaseProductName(dataSource); + if (this.properties != null) { + for (Map.Entry property : properties.entrySet()) { + if (productName.contains((String) property.getKey())) { + return (String) property.getValue(); + } + } + // no match, return null + return null; + } + return productName; + } + + /** + * 获取数据库产品名 + * + * @param dataSource + * @return + * @throws SQLException + */ + private String getDatabaseProductName(DataSource dataSource) throws SQLException { + Connection con = null; + try { + con = dataSource.getConnection(); + DatabaseMetaData metaData = con.getMetaData(); + return metaData.getDatabaseProductName(); + } finally { + if (con != null) { + try { + con.close(); + } catch (SQLException e) { + // ignored + } + } + } + } +``` + +### 解析 typeHandlers 元素 + +`typeHandlers` 元素用于配置自定义类型转换器: + +```xml + + + +``` + +如果配置的是 `package` 元素,则会将包下的所有类注册为类型转换器;如果配置的是 `typeHandler` 元素,则会根据 `javaType`、`jdbcType`、`handler` 属性注册类型转换器。 + +```java + private void typeHandlerElement(XNode parent) { + if (parent != null) { + for (XNode child : parent.getChildren()) { + if ("package".equals(child.getName())) { + // 注册指定包下的类作为类型转换器,如果声明了 MappedTypes 注解则注册为指定 java 类型的转换器 + String typeHandlerPackage = child.getStringAttribute("name"); + typeHandlerRegistry.register(typeHandlerPackage); + } else { + // 获取相关属性 + String javaTypeName = child.getStringAttribute("javaType"); + String jdbcTypeName = child.getStringAttribute("jdbcType"); + String handlerTypeName = child.getStringAttribute("handler"); + // 加载指定 java 类型类对象 + Class javaTypeClass = resolveClass(javaTypeName); + // 加载指定 JDBC 类型并创建实例 + JdbcType jdbcType = resolveJdbcType(jdbcTypeName); + // 加载指定类型转换器类对象 + Class typeHandlerClass = resolveClass(handlerTypeName); + if (javaTypeClass != null) { + // 注册类型转换器 + if (jdbcType == null) { + typeHandlerRegistry.register(javaTypeClass, typeHandlerClass); + } else { + typeHandlerRegistry.register(javaTypeClass, jdbcType, typeHandlerClass); + } + } else { + typeHandlerRegistry.register(typeHandlerClass); + } + } + } + } + } +``` + +### 解析 mappers 元素 + +`mappers` 元素用于定义 `Mapper` 映射文件和 `Mapper` 调用接口: + +```xml + + + + + + +``` + +如果定义的是 `mapper` 元素并指定了 `class` 属性,或定义了 `package` 元素,则会将指定类型在 `MapperRegistry` 中注册为 `Mapper` 接口,并使用 `MapperAnnotationBuilder` 对接口方法进行解析;如果定义的是 `mapper` 元素并指定了 `resource`、或 `url` 属性,则会使用 `XMLMapperBuilder` 解析。对于 `Mapper` 接口和映射文件将在下一章进行分析。 + +```java + private void mapperElement(XNode parent) throws Exception { + if (parent != null) { + for (XNode child : parent.getChildren()) { + if ("package".equals(child.getName())) { + // 注册指定包名下的类为 Mapper 接口 + String mapperPackage = child.getStringAttribute("name"); + configuration.addMappers(mapperPackage); + } else { + String resource = child.getStringAttribute("resource"); + String url = child.getStringAttribute("url"); + String mapperClass = child.getStringAttribute("class"); + if (resource != null && url == null && mapperClass == null) { + // 加载指定资源 + ErrorContext.instance().resource(resource); + InputStream inputStream = Resources.getResourceAsStream(resource); + // 加载指定 Mapper 文件并解析 + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, resource, configuration.getSqlFragments()); + mapperParser.parse(); + } else if (resource == null && url != null && mapperClass == null) { + // 加载指定 URL + ErrorContext.instance().resource(url); + InputStream inputStream = Resources.getUrlAsStream(url); + // 加载指定 Mapper 文件并解析 + XMLMapperBuilder mapperParser = new XMLMapperBuilder(inputStream, configuration, url, configuration.getSqlFragments()); + mapperParser.parse(); + } else if (resource == null && url == null && mapperClass != null) { + // 注册指定类为 Mapper 接口 + Class mapperInterface = Resources.classForName(mapperClass); + configuration.addMapper(mapperInterface); + } else { + throw new BuilderException("A mapper element may only specify a url, resource or class, but not more than one."); + } + } + } + } + } +``` + +## 小结 + +`XMLConfigBuilder` 是 `XML` 配置解析的入口,通常 `MyBatis` 启动时会使用此类解析配置文件获取运行时行为。 + +- `org.apache.ibatis.builder.BaseBuilder`:为 `MyBatis` 初始化过程提供一系列工具方法。如别名转换、类型转换、类加载等。 +- `org.apache.ibatis.builder.xml.XMLConfigBuilder`:`XML` 配置解析入口。 +- `org.apache.ibatis.session.Configuration`:`MyBatis` 全局配置,包括运行行为、类型容器、别名容器、注册 `Mapper`、注册 `statement` 等。 +- `org.apache.ibatis.mapping.VendorDatabaseIdProvider`:根据数据源获取对应的厂商信息。 + diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2205--Mapper \351\200\232\347\224\250\351\205\215\347\275\256\350\247\243\346\236\220.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2205--Mapper \351\200\232\347\224\250\351\205\215\347\275\256\350\247\243\346\236\220.md" new file mode 100644 index 0000000..1e51141 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2205--Mapper \351\200\232\347\224\250\351\205\215\347\275\256\350\247\243\346\236\220.md" @@ -0,0 +1,641 @@ +在上章的配置解析中可以看到 `MyBatis` 在解析完运行时行为相关配置后会继续解析 `Mapper` 映射文件和接口,其中参数映射的解析入口为 `XMLMapperBuilder` 。 + +## 映射文件解析 + +`XMLMapperBuilder` 调用 `parse` 方法解析 `Mapper` 映射文件。 + +```java + public void parse() { + if (!configuration.isResourceLoaded(resource)) { + // 解析 mapper 元素 + configurationElement(parser.evalNode("/mapper")); + // 加入已解析队列 + configuration.addLoadedResource(resource); + // Mapper 映射文件与对应 namespace 的接口进行绑定 + bindMapperForNamespace(); + } + + // 重新引用配置 + parsePendingResultMaps(); + parsePendingCacheRefs(); + parsePendingStatements(); + } +``` + +## 解析 mapper 元素 + +`mapper` 元素通常需要指定 `namespace` 用于唯一区别映射文件,不同映射文件支持通过其它映射文件的 `namespace` 来引用其配置。`mapper` 元素下可以配置二级缓存(`cache`、`cache-ref`)、返回值映射(`resultMap`)、`sql fragments`(`sql`)、`statement`(`select`、`insert`、`update`、`delete`)等,`MyBatis` 源码提供 `mybatis-mapper.xsd` 文件用于规范映射文件书写规则。 + +```java + private void configurationElement(XNode context) { + try { + // 获取元素对应的 namespace 名称 + String namespace = context.getStringAttribute("namespace"); + if (namespace == null || namespace.equals("")) { + throw new BuilderException("Mapper's namespace cannot be empty"); + } + // 设置 Mapper 文件对应的 namespace 名称 + builderAssistant.setCurrentNamespace(namespace); + // 解析 cache-ref 元素 + cacheRefElement(context.evalNode("cache-ref")); + // 解析 cache 元素,会覆盖 cache-ref 配置 + cacheElement(context.evalNode("cache")); + // 解析 parameterMap 元素(废弃) + parameterMapElement(context.evalNodes("/mapper/parameterMap")); + // 解析 resultMap 元素 + resultMapElements(context.evalNodes("/mapper/resultMap")); + // 解析 sql 元素 + sqlElement(context.evalNodes("/mapper/sql")); + // 解析 select|insert|update|delete 元素 + buildStatementFromContext(context.evalNodes("select|insert|update|delete")); + } catch (Exception e) { + throw new BuilderException("Error parsing Mapper XML. The XML location is '" + resource + "'. Cause: " + e, e); + } + } +``` + +## 解析 cache 元素 + +如果要为某命名空间开启二级缓存功能,可以通过配置 `cache` 元素,示例配置如下: + +```xml + + + +``` + +`cache` 元素的解析逻辑如下: + +```java + private void cacheElement(XNode context) { + if (context != null) { + // 获取缓存类型,默认为 PERPETUAL + String type = context.getStringAttribute("type", "PERPETUAL"); + // Configuration 构造方法中已为默认的缓存实现注册别名,从别名转换器中获取类对象 + Class typeClass = typeAliasRegistry.resolveAlias(type); + // 获取失效类型,默认为 LRU + String eviction = context.getStringAttribute("eviction", "LRU"); + Class evictionClass = typeAliasRegistry.resolveAlias(eviction); + // 缓存刷新时间间隔 + Long flushInterval = context.getLongAttribute("flushInterval"); + // 缓存项大小 + Integer size = context.getIntAttribute("size"); + // 是否将序列化成二级制数据 + boolean readWrite = !context.getBooleanAttribute("readOnly", false); + // 缓存不命中进入数据库查询时是否加锁(保证同一时刻相同缓存key只有一个线程执行数据库查询任务) + boolean blocking = context.getBooleanAttribute("blocking", false); + // 从子元素中加载属性 + Properties props = context.getChildrenAsProperties(); + // 创建缓存配置 + builderAssistant.useNewCache(typeClass, evictionClass, flushInterval, size, readWrite, blocking, props); + } + } +``` + +在第三章基础支持模块中已经详细介绍了 `MyBatis` 实现的各种缓存。从 `cache` 元素中获取缓存参数配置后会交由 `MapperBuilderAssistant#useNewCache` 方法处理。`MapperBuilderAssistant` 方法是一个映射文件解析工具,它负责将映射文件各个元素解析的参数生成配置对象,最终设置到全局配置类 `Configuration` 中。 + +```java + public Cache useNewCache(Class typeClass, + Class evictionClass, + Long flushInterval, + Integer size, + boolean readWrite, + boolean blocking, + Properties props) { + Cache cache = new CacheBuilder(currentNamespace) + // 基础缓存配置 + .implementation(valueOrDefault(typeClass, PerpetualCache.class)) + // 失效类型,默认 LRU + .addDecorator(valueOrDefault(evictionClass, LruCache.class)) + // 定时清理缓存时间间隔 + .clearInterval(flushInterval) + // 缓存项大小 + .size(size) + // 是否将缓存系列化成二级制数据 + .readWrite(readWrite) + // 缓存不命中进入数据库查询时是否加锁(保证同一时刻相同缓存key只有一个线程执行数据库查询任务) + .blocking(blocking) + .properties(props) + .build(); + // 设置到全局配置中 + configuration.addCache(cache); + currentCache = cache; + return cache; + } +``` + +## 解析 cache-ref 元素 + +如果希望引用其它 `namespace` 的缓存配置,可以通过 `cache-ref` 元素配置: + +```xml + +``` + +其解析逻辑是将当前 `namespace` 与引用缓存配置的 `namespace` 在全局配置中进行绑定。 + +```java + private void cacheRefElement(XNode context) { + if (context != null) { + // 当前 namespace - 引用缓存配置的 namespace,在全局配置中进行绑定 + configuration.addCacheRef(builderAssistant.getCurrentNamespace(), context.getStringAttribute("namespace")); + // 获取缓存配置解析器 + CacheRefResolver cacheRefResolver = new CacheRefResolver(builderAssistant, context.getStringAttribute("namespace")); + try { + // 解析获得引用的缓存配置 + cacheRefResolver.resolveCacheRef(); + } catch (IncompleteElementException e) { + // 指定引用的 namespace 缓存还未加载,暂时放入集合,等待全部 namespace 都加载完成后重新引用 + configuration.addIncompleteCacheRef(cacheRefResolver); + } + } + } +``` + +由于存在被引用配置还未被加载,因而无法从全局配置中获取的情况,`MyBatis` 定义了 `IncompleteElementException` 在此时抛出,未解析完成的缓存解析对象会被加入到全局配置中的 `incompleteCacheRefs` 集合中,用于后续处理。 + +```java + public Cache useCacheRef(String namespace) { + if (namespace == null) { + throw new BuilderException("cache-ref element requires a namespace attribute."); + } + try { + unresolvedCacheRef = true; + // 从全局配置中获取缓存配置 + Cache cache = configuration.getCache(namespace); + if (cache == null) { + throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found."); + } + currentCache = cache; + unresolvedCacheRef = false; + return cache; + } catch (IllegalArgumentException e) { + // 可能指定引用的 namespace 缓存还未加载,抛出异常 + throw new IncompleteElementException("No cache for namespace '" + namespace + "' could be found.", e); + } + } +``` + +`MyBatis` 允许 `resultMap`、`cache-ref`、`statement` 元素延迟加载,以 `cache-ref` 重新引用的方法 `parsePendingCacheRefs` 为例,其重新引用逻辑如下: + +```java + private void parsePendingCacheRefs() { + // 从全局配置中获取未解析的缓存引用配置 + Collection incompleteCacheRefs = configuration.getIncompleteCacheRefs(); + synchronized (incompleteCacheRefs) { + Iterator iter = incompleteCacheRefs.iterator(); + while (iter.hasNext()) { + try { + // 逐个重新引用缓存配置 + iter.next().resolveCacheRef(); + // 引用成功,删除集合元素 + iter.remove(); + } catch (IncompleteElementException e) { + // 引用的缓存配置不存在 + // Cache ref is still missing a resource... + } + } + } + } +``` + +## 解析 resultMap 元素 + +`resultMap` 元素用于定义结果集与结果对象(`JavaBean` 对象)之间的映射规则。`resultMap` 下除了 `discriminator` 的其它元素,都会被解析成 `ResultMapping` 对象,其解析过程如下: + +```java + private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings, Class enclosingType) throws Exception { + ErrorContext.instance().activity("processing " + resultMapNode.getValueBasedIdentifier()); + // 获取返回值类型 + String type = resultMapNode.getStringAttribute("type", + resultMapNode.getStringAttribute("ofType", + resultMapNode.getStringAttribute("resultType", + resultMapNode.getStringAttribute("javaType")))); + // 加载返回值类对象 + Class typeClass = resolveClass(type); + if (typeClass == null) { + // association 和 case 元素没有显式地指定返回值类型 + typeClass = inheritEnclosingType(resultMapNode, enclosingType); + } + Discriminator discriminator = null; + List resultMappings = new ArrayList<>(); + resultMappings.addAll(additionalResultMappings); + // 加载子元素 + List resultChildren = resultMapNode.getChildren(); + for (XNode resultChild : resultChildren) { + if ("constructor".equals(resultChild.getName())) { + // 解析 constructor 元素 + processConstructorElement(resultChild, typeClass, resultMappings); + } else if ("discriminator".equals(resultChild.getName())) { + // 解析 discriminator 元素 + discriminator = processDiscriminatorElement(resultChild, typeClass, resultMappings); + } else { + // 解析 resultMap 元素下的其它元素 + List flags = new ArrayList<>(); + if ("id".equals(resultChild.getName())) { + // id 元素增加标志 + flags.add(ResultFlag.ID); + } + // 解析元素映射关系 + resultMappings.add(buildResultMappingFromContext(resultChild, typeClass, flags)); + } + } + String id = resultMapNode.getStringAttribute("id", + resultMapNode.getValueBasedIdentifier()); + // extend resultMap id + String extend = resultMapNode.getStringAttribute("extends"); + // 是否设置自动映射 + Boolean autoMapping = resultMapNode.getBooleanAttribute("autoMapping"); + // resultMap 解析器 + ResultMapResolver resultMapResolver = new ResultMapResolver(builderAssistant, id, typeClass, extend, discriminator, resultMappings, autoMapping); + try { + // 解析生成 ResultMap 对象并设置到全局配置中 + return resultMapResolver.resolve(); + } catch (IncompleteElementException e) { + // 异常稍后处理 + configuration.addIncompleteResultMap(resultMapResolver); + throw e; + } + } +``` + +以下从不同元素配置的角度分别分析 `MyBatis` 解析规则。 + +### id & result + +```xml + + +``` + +`id` 和 `result` 元素都会将一个列的值映射到一个简单数据类型字段,不同的是 `id` 元素对应对象的标识属性,在比较对象时会用到。此外还可以设置 `typeHandler` 属性用于自定义类型转换逻辑。 + +`buildResultMappingFromContext` 方法负责将 `resultMap` 子元素解析为 `ResultMapping` 对象: + +```java + /** + * 解析 resultMap 子元素映射关系 + */ + private ResultMapping buildResultMappingFromContext(XNode context, Class resultType, List flags) throws Exception { + String property; + if (flags.contains(ResultFlag.CONSTRUCTOR)) { + // constructor 子元素,通过 name 获取参数名 + property = context.getStringAttribute("name"); + } else { + property = context.getStringAttribute("property"); + } + // 列名 + String column = context.getStringAttribute("column"); + // java 类型 + String javaType = context.getStringAttribute("javaType"); + // jdbc 类型 + String jdbcType = context.getStringAttribute("jdbcType"); + // 嵌套的 select id + String nestedSelect = context.getStringAttribute("select"); + // 获取嵌套的 resultMap id + String nestedResultMap = context.getStringAttribute("resultMap", + processNestedResultMappings(context, Collections.emptyList(), resultType)); + // 获取指定的不为空才创建实例的列 + String notNullColumn = context.getStringAttribute("notNullColumn"); + // 列前缀 + String columnPrefix = context.getStringAttribute("columnPrefix"); + // 类型转换器 + String typeHandler = context.getStringAttribute("typeHandler"); + // 集合的多结果集 + String resultSet = context.getStringAttribute("resultSet"); + // 指定外键对应的列名 + String foreignColumn = context.getStringAttribute("foreignColumn"); + // 是否懒加载 + boolean lazy = "lazy".equals(context.getStringAttribute("fetchType", configuration.isLazyLoadingEnabled() ? "lazy" : "eager")); + // 加载返回值类型 + Class javaTypeClass = resolveClass(javaType); + // 加载类型转换器类型 + Class> typeHandlerClass = resolveClass(typeHandler); + // 加载 jdbc 类型对象 + JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); + return builderAssistant.buildResultMapping(resultType, property, column, javaTypeClass, jdbcTypeEnum, nestedSelect, nestedResultMap, notNullColumn, columnPrefix, typeHandlerClass, flags, resultSet, foreignColumn, lazy); + } +``` + +`ResultMapping` 对象保存了列名与 `JavaBean` 中字段名的对应关系,并明确了 `Java` 类型和 `JDBC` 类型,如果指定了类型转换器则使用指定的转化器将结果集字段映射为 `Java` 对象字段;否则根据 `Java` 类型和 `JDBC` 类型到类型转换器注册类中寻找适合的类型转换器。最终影响映射的一系列因素都被保存到 `ResultMapping` 对象中并加入到全局配置。 + +### constructor + +使用 `constructor` 元素允许返回值对象使用指定的构造方法创建而不是默认的构造方法。 + +```xml + + + + +``` + +在解析 `constructor` 元素时,`MyBatis` 特别指定了将 `constructor` 子元素解析为 `ResultMapping` 对象。 + +```java + ... + // 加载子元素 + List resultChildren = resultMapNode.getChildren(); + for (XNode resultChild : resultChildren) { + if ("constructor".equals(resultChild.getName())) { + // 解析 constructor 元素 + processConstructorElement(resultChild, typeClass, resultMappings); + } + ... + } + ... + + /** + * 解析 constructor 元素下的子元素 + */ + private void processConstructorElement(XNode resultChild, Class resultType, List resultMappings) throws Exception { + // 获取子元素 + List argChildren = resultChild.getChildren(); + for (XNode argChild : argChildren) { + List flags = new ArrayList<>(); + // 标明此元素在 constructor 元素中 + flags.add(ResultFlag.CONSTRUCTOR); + if ("idArg".equals(argChild.getName())) { + // 此元素映射 id + flags.add(ResultFlag.ID); + } + // 解析子元素映射关系 + resultMappings.add(buildResultMappingFromContext(argChild, resultType, flags)); + } + } +``` + +解析 `constructor` 子元素的逻辑与解析 `id`、`result` 元素的逻辑是一致的。 + +##### association + +`association` 用于配置非简单类型的映射关系。其不仅支持在当前查询中做嵌套映射: + +```xml + + + + +``` + +也支持通过 `select` 属性嵌套其它查询: + +```xml + + + + +``` + +在解析 `resultMap` 子元素方法 `buildResultMappingFromContext` 的逻辑中,`MyBatis` 会尝试获取每个子元素的 `resultMap` 属性,如果未指定,则会调用 `processNestedResultMappings` 方法,在此方法中对于 `asociation` 元素来说,如果指定了 `select` 属性,则映射时只需要获取对应 `select` 语句的 `resultMap`;如果未指定,则需要重新调用 `resultMapElement` 解析结果集映射关系。 + +```java + private ResultMapping buildResultMappingFromContext(XNode context, Class resultType, List flags) throws Exception { + ... + // 尝试获取嵌套的 resultMap id + String nestedResultMap = context.getStringAttribute("resultMap", processNestedResultMappings(context, Collections.emptyList(), resultType)); + ... + } + + /** + * 处理嵌套的 resultMap,获取 id + * + * @param context + * @param resultMappings + * @param enclosingType + * @return + * @throws Exception + */ + private String processNestedResultMappings(XNode context, List resultMappings, Class enclosingType) throws Exception { + if ("association".equals(context.getName()) + || "collection".equals(context.getName()) + || "case".equals(context.getName())) { + if (context.getStringAttribute("select") == null) { + // 如果是 association、collection 或 case 元素并且没有 select 属性 + // collection 元素没有指定 resultMap 或 javaType 属性,需要验证 resultMap 父元素对应的返回值类型是否有对当前集合的赋值入口 + validateCollection(context, enclosingType); + ResultMap resultMap = resultMapElement(context, resultMappings, enclosingType); + return resultMap.getId(); + } + } + return null; + } +``` + +在 `resultMapElement` 方法中调用了 `inheritEnclosingType` 针对未定义返回类型的元素的返回值类型解析: + +```java + private ResultMap resultMapElement(XNode resultMapNode, List additionalResultMappings, Class enclosingType) throws Exception { + ... + // 获取返回值类型 + String type = resultMapNode.getStringAttribute("type", + resultMapNode.getStringAttribute("ofType", + resultMapNode.getStringAttribute("resultType", + resultMapNode.getStringAttribute("javaType")))); + // 加载返回值类对象 + Class typeClass = resolveClass(type); + if (typeClass == null) { + // association 等元素没有显式地指定返回值类型 + typeClass = inheritEnclosingType(resultMapNode, enclosingType); + } + } + + protected Class inheritEnclosingType(XNode resultMapNode, Class enclosingType) { + if ("association".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) { + // association 元素没有指定 resultMap 属性 + String property = resultMapNode.getStringAttribute("property"); + if (property != null && enclosingType != null) { + // 根据反射信息确定字段的类型 + MetaClass metaResultType = MetaClass.forClass(enclosingType, configuration.getReflectorFactory()); + return metaResultType.getSetterType(property); + } + } else if ("case".equals(resultMapNode.getName()) && resultMapNode.getStringAttribute("resultMap") == null) { + // case 元素返回值属性与 resultMap 父元素相同 + return enclosingType; + } + return null; + } +``` + +在 `inheritEnclosingType` 方法中,如果未定义 `resultMap` 属性,则会通过反射工具 `MetaClass` 获取父元素 `resultMap` 返回类型的类信息,`association` 元素对应的字段名称的 `setter` 方法的参数就是其返回值类型。由此 `association` 元素必定可以关联到其结果集映射。 + +### collection + +`collection` 元素用于配置集合属性的映射关系,其解析过程与 `association` 元素大致相同,重要的区别是 `collection` 元素使用 `ofType` 属性指定集合元素类型,例如需要映射的 `Java` 集合为 `List users`,则配置示例如下: + +```xml + +``` + +`javaType`熟悉指定的是字段类型,而 `ofType` 属性指定的才是需要映射的集合存储的类型。 + +### discriminator + +`discriminator` 支持对一个查询可能出现的不同结果集做鉴别,根据具体的条件为 `resultMap` 动态选择返回值类型。 + +```xml + + + + +``` + +`discriminator` 元素的解析是通过 `processDiscriminatorElement` 方法完成的: + +```java + private Discriminator processDiscriminatorElement(XNode context, Class resultType, List resultMappings) throws Exception { + // 获取需要鉴别的字段的相关信息 + String column = context.getStringAttribute("column"); + String javaType = context.getStringAttribute("javaType"); + String jdbcType = context.getStringAttribute("jdbcType"); + String typeHandler = context.getStringAttribute("typeHandler"); + Class javaTypeClass = resolveClass(javaType); + Class> typeHandlerClass = resolveClass(typeHandler); + JdbcType jdbcTypeEnum = resolveJdbcType(jdbcType); + Map discriminatorMap = new HashMap<>(); + // 解析 discriminator 的 case 子元素 + for (XNode caseChild : context.getChildren()) { + // 解析不同列值对应的不同 resultMap + String value = caseChild.getStringAttribute("value"); + String resultMap = caseChild.getStringAttribute("resultMap", processNestedResultMappings(caseChild, resultMappings, resultType)); + discriminatorMap.put(value, resultMap); + } + return builderAssistant.buildDiscriminator(resultType, column, javaTypeClass, jdbcTypeEnum, typeHandlerClass, discriminatorMap); + } +``` + +### 创建 ResultMap + +在 `resultMap` 各子元素解析完成,`ResultMapResolver` 负责将生成的 `ResultMapping` 集合解析为 `ResultMap` 对象: + +```java + public ResultMap addResultMap(String id, Class type, String extend, Discriminator discriminator, List resultMappings, Boolean autoMapping) { + id = applyCurrentNamespace(id, false); + extend = applyCurrentNamespace(extend, true); + + if (extend != null) { + if (!configuration.hasResultMap(extend)) { + throw new IncompleteElementException("Could not find a parent resultmap with id '" + extend + "'"); + } + // 获取继承的 ResultMap 对象 + ResultMap resultMap = configuration.getResultMap(extend); + List extendedResultMappings = new ArrayList<>(resultMap.getResultMappings()); + extendedResultMappings.removeAll(resultMappings); + // Remove parent constructor if this resultMap declares a constructor. + boolean declaresConstructor = false; + for (ResultMapping resultMapping : resultMappings) { + if (resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)) { + // 当前 resultMap 指定了构造方法 + declaresConstructor = true; + break; + } + } + if (declaresConstructor) { + // 移除继承的 ResultMap 的构造器映射对象 + extendedResultMappings.removeIf(resultMapping -> resultMapping.getFlags().contains(ResultFlag.CONSTRUCTOR)); + } + resultMappings.addAll(extendedResultMappings); + } + ResultMap resultMap = new ResultMap.Builder(configuration, id, type, resultMappings, autoMapping).discriminator(discriminator).build(); + configuration.addResultMap(resultMap); + return resultMap; + } +``` + +如果当前 `ResultMap` 指定了 `extend` 属性,`MyBatis` 会从全局配置中获取被继承的 `ResultMap` 的相关映射关系,加入到当前映射关系中。但是如果被继承的 `ResultMap` 指定了构造器映射关系,当前 `ResultMap` 会选择移除。 + +## 解析 sql 元素 + +`sql` 元素用于定义可重用的 `SQL` 代码段,这些代码段可以通过 `include` 元素进行引用。 + +```xml + + id, name, value + + + + ${alias} + + + +``` + +对 `sql` 元素的解析逻辑如下,符合 `databaseId` 要求的 `sql` 元素才会被加载到全局配置中。 + +```java +private void sqlElement(List list) { + if (configuration.getDatabaseId() != null) { + sqlElement(list, configuration.getDatabaseId()); + } + sqlElement(list, null); +} + + +/** + * 解析 sql 元素,将对应的 sql 片段设置到全局配置中 + */ +private void sqlElement(List list, String requiredDatabaseId) { + for (XNode context : list) { + String databaseId = context.getStringAttribute("databaseId"); + String id = context.getStringAttribute("id"); + id = builderAssistant.applyCurrentNamespace(id, false); + if (databaseIdMatchesCurrent(id, databaseId, requiredDatabaseId)) { + // 符合当前 databaseId 的 sql fragment,加入到全局配置中 + sqlFragments.put(id, context); + } + } +} + +/** + * 判断 sql 元素是否满足加载条件 + * + * @param id + * @param databaseId + * @param requiredDatabaseId + * @return + */ +private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) { + if (requiredDatabaseId != null) { + // 如果指定了当前数据源的 databaseId + if (!requiredDatabaseId.equals(databaseId)) { + // 被解析 sql 元素的 databaseId 需要符合 + return false; + } + } else { + if (databaseId != null) { + // 全局未指定 databaseId,不会加载指定了 databaseId 的 sql 元素 + return false; + } + // skip this fragment if there is a previous one with a not null databaseId + if (this.sqlFragments.containsKey(id)) { + XNode context = this.sqlFragments.get(id); + if (context.getStringAttribute("databaseId") != null) { + return false; + } + } + } + return true; +} +``` + +## 小结 + +`MyBatis` 能够轻松实现列值转换为 Java 对象依靠的是其强大的参数映射功能,能够支持集合、关联类型、嵌套等复杂场景的映射。同时缓存配置、`sql` 片段配置,也为开发者方便的提供了配置入口。 + +- `org.apache.ibatis.builder.annotation.MapperAnnotationBuilder`:解析 `Mapper` 接口。 +- `org.apache.ibatis.builder.xml.XMLMapperBuilder`:解析 `Mapper` 文件。 +- `org.apache.ibatis.builder.MapperBuilderAssistant`:`Mapper` 文件解析工具。生成元素对象并设置到全局配置中。 +- `org.apache.ibatis.builder.CacheRefResolver`:缓存引用配置解析器,应用其它命名空间缓存配置到当前命名空间下。 +- `org.apache.ibatis.builder.IncompleteElementException`:当前映射文件引用了其它命名空间下的配置,而该配置还未加载到全局配置中时会抛出此异常。 +- `org.apache.ibatis.mapping.ResultMapping`:返回值字段映射关系对象。 +- `org.apache.ibatis.builder.ResultMapResolver`:`ResultMap` 解析器。 +- `org.apache.ibatis.mapping.ResultMap`:返回值映射对象 \ No newline at end of file diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2206--statement \350\247\243\346\236\220.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2206--statement \350\247\243\346\236\220.md" new file mode 100644 index 0000000..57c2231 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2206--statement \350\247\243\346\236\220.md" @@ -0,0 +1,1382 @@ +`Mapper` 映射文件解析的最后一步是解析所有 `statement` 元素,即 `select`、`insert`、`update`、`delete` 元素,这些元素中可能会包含动态 `SQL`,即使用 `${}` 占位符或 `if`、`choose`、`where` 等元素动态组成的 `SQL`。动态 `SQL` 功能正是 `MyBatis` 强大的所在,其解析过程也是十分复杂的。 + +## 解析工具 + +为了方便 `statement` 的解析,`MyBatis` 提供了一些解析工具。 + +### Token 解析 + +`MyBatis` 支持使用 `${}` 或 `#{}` 类型的 `token` 作为动态参数,不仅文本中可以使用 `token`,`xml` 元素中的属性等也可以使用。 + +#### GenericTokenParser + +`GenericTokenParser` 是 `MyBatis` 提供的通用 `token` 解析器,其解析逻辑是根据指定的 `token` 前缀和后缀搜索 `token`,并使用传入的 `TokenHandler` 对文本进行处理。 + +```java + public String parse(String text) { + if (text == null || text.isEmpty()) { + return ""; + } + // search open token 搜索 token 前缀 + int start = text.indexOf(openToken); + if (start == -1) { + // 没有 token 前缀,返回原文本 + return text; + } + char[] src = text.toCharArray(); + // 当前解析偏移量 + int offset = 0; + // 已解析文本 + final StringBuilder builder = new StringBuilder(); + // 当前占位符内的表达式 + StringBuilder expression = null; + while (start > -1) { + if (start > 0 && src[start - 1] == '\\') { + // 如果待解析属性前缀被转义,则去掉转义字符,加入已解析文本 + // this open token is escaped. remove the backslash and continue. + builder.append(src, offset, start - offset - 1).append(openToken); + // 更新解析偏移量 + offset = start + openToken.length(); + } else { + // found open token. let's search close token. + if (expression == null) { + expression = new StringBuilder(); + } else { + expression.setLength(0); + } + // 前缀前面的部分加入已解析文本 + builder.append(src, offset, start - offset); + // 更新解析偏移量 + offset = start + openToken.length(); + // 获取对应的后缀索引 + int end = text.indexOf(closeToken, offset); + while (end > -1) { + if (end > offset && src[end - 1] == '\\') { + // 后缀被转义,加入已解析文本 + // this close token is escaped. remove the backslash and continue. + expression.append(src, offset, end - offset - 1).append(closeToken); + offset = end + closeToken.length(); + // 寻找下一个后缀 + end = text.indexOf(closeToken, offset); + } else { + // 找到后缀,获取占位符内的表达式 + expression.append(src, offset, end - offset); + offset = end + closeToken.length(); + break; + } + } + if (end == -1) { + // 找不到后缀,前缀之后的部分全部加入已解析文本 + // close token was not found. + builder.append(src, start, src.length - start); + offset = src.length; + } else { + // 能够找到后缀,追加 token 处理器处理后的文本 + builder.append(handler.handleToken(expression.toString())); + // 更新解析偏移量 + offset = end + closeToken.length(); + } + } + // 寻找下一个前缀,重复解析表达式 + start = text.indexOf(openToken, offset); + } + if (offset < src.length) { + // 将最后的部分加入已解析文本 + builder.append(src, offset, src.length - offset); + } + // 返回解析后的文本 + return builder.toString(); + } +``` + +由于 `GenericTokenParser` 的 `token` 前后缀和具体解析逻辑都是可指定的,因此基于 `GenericTokenParser` 可以实现对不同 `token` 的定制化解析。 + +#### TokenHandler + +`TokenHandler` 是 `token` 处理器抽象接口。实现此接口可以定义 `token` 以何种方式被解析。 + +```java +public interface TokenHandler { + + /** + * 对 token 进行解析 + * + * @param content 待解析 token + * @return + */ + String handleToken(String content); +} +``` + +#### PropertyParser + +`PropertyParser` 是 `token` 解析的一种具体实现,其指定对 `${}` 类型 `token` 进行解析,具体解析逻辑由其内部类 `VariableTokenHandler` 实现: + +```java + /** + * 对 ${} 类型 token 进行解析 + * + * @param string + * @param variables + * @return + */ + public static String parse(String string, Properties variables) { + VariableTokenHandler handler = new VariableTokenHandler(variables); + GenericTokenParser parser = new GenericTokenParser("${", "}", handler); + return parser.parse(string); + } + + /** + * 根据配置属性对 ${} token 进行解析 + */ + private static class VariableTokenHandler implements TokenHandler { + + /** + * 预先设置的属性 + */ + private final Properties variables; + + /** + * 是否运行使用默认值,默认为 false + */ + private final boolean enableDefaultValue; + + /** + * 默认值分隔符号,即如待解析属性 ${key:default},key 的默认值为 default + */ + private final String defaultValueSeparator; + + private VariableTokenHandler(Properties variables) { + this.variables = variables; + this.enableDefaultValue = Boolean.parseBoolean(getPropertyValue(KEY_ENABLE_DEFAULT_VALUE, ENABLE_DEFAULT_VALUE)); + this.defaultValueSeparator = getPropertyValue(KEY_DEFAULT_VALUE_SEPARATOR, DEFAULT_VALUE_SEPARATOR); + } + + private String getPropertyValue(String key, String defaultValue) { + return (variables == null) ? defaultValue : variables.getProperty(key, defaultValue); + } + + @Override + public String handleToken(String content) { + if (variables != null) { + String key = content; + if (enableDefaultValue) { + // 如待解析属性 ${key:default},key 的默认值为 default + final int separatorIndex = content.indexOf(defaultValueSeparator); + String defaultValue = null; + if (separatorIndex >= 0) { + key = content.substring(0, separatorIndex); + defaultValue = content.substring(separatorIndex + defaultValueSeparator.length()); + } + if (defaultValue != null) { + // 使用默认值 + return variables.getProperty(key, defaultValue); + } + } + if (variables.containsKey(key)) { + // 不使用默认值 + return variables.getProperty(key); + } + } + // 返回原文本 + return "${" + content + "}"; + } + } +``` + +`VariableTokenHandler` 实现了 `TokenHandler` 接口,其构造方法允许传入一组 `Properties` 用于获取 `token` 表达式的值。如果开启了使用默认值,则表达式 `${key:default}` 会在 `key` 没有映射值的时候使用 `default` 作为默认值。 + +### 特殊容器 + +#### StrictMap + +`Configuration` 中的 `StrictMap` 继承了 `HashMap`,相对于 `HashMap`,其存取键值的要求更为严格。`put` 方法不允许添加相同的 `key`,并获取最后一个 `.` 后的部分作为 `shortKey`,如果 `shortKey` 也重复了,其会向容器中添加一个 `Ambiguity` 对象,当使用 `get` 方法获取这个 `shortKey` 对应的值时,就会抛出异常。`get` 方法对于不存在的 `key` 也会抛出异常。 + +```java + public V put(String key, V value) { + if (containsKey(key)) { + // 重复 key 异常 + throw new IllegalArgumentException(name + " already contains value for " + key + + (conflictMessageProducer == null ? "" : conflictMessageProducer.apply(super.get(key), value))); + } + if (key.contains(".")) { + // 获取最后一个 . 后的部分作为 shortKey + final String shortKey = getShortName(key); + // shortKey 不允许重复,否则在获取时异常 + if (super.get(shortKey) == null) { + super.put(shortKey, value); + } else { + super.put(shortKey, (V) new Ambiguity(shortKey)); + } + } + return super.put(key, value); + } + + public V get(Object key) { + V value = super.get(key); + if (value == null) { + // key 不存在抛异常 + throw new IllegalArgumentException(name + " does not contain value for " + key); + } + // 重复的 key 抛异常 + if (value instanceof Ambiguity) { + throw new IllegalArgumentException(((Ambiguity) value).getSubject() + " is ambiguous in " + name + + " (try using the full name including the namespace, or rename one of the entries)"); + } + return value; + } +``` + +#### ContextMap + +`ContextMap` 是 `DynamicContext` 的静态内部类,用于保存 `sql` 上下文中的绑定参数。 + +```java +static class ContextMap extends HashMap { + private static final long serialVersionUID = 2977601501966151582L; + + /** + * 参数对象 + */ + private MetaObject parameterMetaObject; + + public ContextMap(MetaObject parameterMetaObject) { + this.parameterMetaObject = parameterMetaObject; + } + + @Override + public Object get(Object key) { + // 先根据 key 查找原始容器 + String strKey = (String) key; + if (super.containsKey(strKey)) { + return super.get(strKey); + } + + // 再进入参数对象查找 + if (parameterMetaObject != null) { + // issue #61 do not modify the context when reading + return parameterMetaObject.getValue(strKey); + } + + return null; + } +} +``` + +### OGNL 工具 + +#### OgnlCache + +`OGNL` 工具支持通过字符串表达式调用 `Java` 方法,但是其实现需要对 `OGNL` 表达式进行编译,为了提高性能,`MyBatis` 提供 `OgnlCache` 工具类用于对 `OGNL` 表达式编译结果进行缓存。 + +```java + /** + * 根据 ognl 表达式和参数计算值 + * + * @param expression + * @param root + * @return + */ + public static Object getValue(String expression, Object root) { + try { + Map context = Ognl.createDefaultContext(root, MEMBER_ACCESS, CLASS_RESOLVER, null); + return Ognl.getValue(parseExpression(expression), context, root); + } catch (OgnlException e) { + throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e); + } + } + + /** + * 编译 ognl 表达式并放入缓存 + * + * @param expression + * @return + * @throws OgnlException + */ + private static Object parseExpression(String expression) throws OgnlException { + Object node = expressionCache.get(expression); + if (node == null) { + // 编译 ognl 表达式 + node = Ognl.parseExpression(expression); + // 放入缓存 + expressionCache.put(expression, node); + } + return node; + } +``` + +#### ExpressionEvaluator + +`ExpressionEvaluator` 是 `OGNL` 表达式计算工具,`evaluateBoolean` 和 `evaluateIterable` 方法分别根据传入的表达式和参数计算出一个 `boolean` 值或一个可迭代对象。 + +```java + /** + * 计算 ognl 表达式 true / false + * + * @param expression + * @param parameterObject + * @return + */ + public boolean evaluateBoolean(String expression, Object parameterObject) { + // 根据 ognl 表达式和参数计算值 + Object value = OgnlCache.getValue(expression, parameterObject); + // true / false + if (value instanceof Boolean) { + return (Boolean) value; + } + // 不为 0 + if (value instanceof Number) { + return new BigDecimal(String.valueOf(value)).compareTo(BigDecimal.ZERO) != 0; + } + // 不为 null + return value != null; + } + + /** + * 计算获得一个可迭代的对象 + * + * @param expression + * @param parameterObject + * @return + */ + public Iterable evaluateIterable(String expression, Object parameterObject) { + Object value = OgnlCache.getValue(expression, parameterObject); + if (value == null) { + throw new BuilderException("The expression '" + expression + "' evaluated to a null value."); + } + if (value instanceof Iterable) { + // 已实现 Iterable 接口 + return (Iterable) value; + } + if (value.getClass().isArray()) { + // 数组转集合 + // the array may be primitive, so Arrays.asList() may throw + // a ClassCastException (issue 209). Do the work manually + // Curse primitives! :) (JGB) + int size = Array.getLength(value); + List answer = new ArrayList<>(); + for (int i = 0; i < size; i++) { + Object o = Array.get(value, i); + answer.add(o); + } + return answer; + } + if (value instanceof Map) { + // Map 获取 entry + return ((Map) value).entrySet(); + } + throw new BuilderException("Error evaluating expression '" + expression + "'. Return value (" + value + ") was not iterable."); + } +``` + +## 解析逻辑 + +`MyBastis` 中调用 `XMLStatementBuilder#parseStatementNode` 方法解析单个 `statement` 元素。此方法中除了逐个获取元素属性,还对 `include` 元素、`selectKey` 元素进行解析,创建了 `sql` 生成对象 `SqlSource`,并将 `statement` 的全部信息聚合到 `MappedStatement` 对象中。 + +```java + public void parseStatementNode() { + // 获取 id + String id = context.getStringAttribute("id"); + // 自定义数据库厂商信息 + String databaseId = context.getStringAttribute("databaseId"); + + if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) { + // 不符合当前数据源对应的数据厂商信息的语句不加载 + return; + } + + // 获取元素名 + String nodeName = context.getNode().getNodeName(); + // 元素名转为对应的 SqlCommandType 枚举 + SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH)); + // 是否为查询 + boolean isSelect = sqlCommandType == SqlCommandType.SELECT; + // 获取 flushCache 属性,查询默认为 false,其它默认为 true + boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect); + // 获取 useCache 属性,查询默认为 true,其它默认为 false + boolean useCache = context.getBooleanAttribute("useCache", isSelect); + // 获取 resultOrdered 属性,默认为 false + boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false); + + // Include Fragments before parsing + XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant); + // 解析 include 属性 + includeParser.applyIncludes(context.getNode()); + + // 参数类型 + String parameterType = context.getStringAttribute("parameterType"); + Class parameterTypeClass = resolveClass(parameterType); + + // 获取 Mapper 语法类型 + String lang = context.getStringAttribute("lang"); + // 默认使用 XMLLanguageDriver + LanguageDriver langDriver = getLanguageDriver(lang); + + // Parse selectKey after includes and remove them. + // 解析 selectKey 元素 + processSelectKeyNodes(id, parameterTypeClass, langDriver); + + // 获取 KeyGenerator + // Parse the SQL (pre: and were parsed and removed) + KeyGenerator keyGenerator; + String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX; + keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true); + if (configuration.hasKeyGenerator(keyStatementId)) { + // 获取解析完成的 KeyGenerator 对象 + keyGenerator = configuration.getKeyGenerator(keyStatementId); + } else { + // 如果开启了 useGeneratedKeys 属性,并且为插入类型的 sql 语句、配置了 keyProperty 属性,则可以批量自动设置属性 + keyGenerator = context.getBooleanAttribute("useGeneratedKeys", + configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType)) + ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE; + } + + // 生成有效 sql 语句和参数绑定对象 + SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass); + // sql 类型 + StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString())); + // 分批获取数据的数量 + Integer fetchSize = context.getIntAttribute("fetchSize"); + // 执行超时时间 + Integer timeout = context.getIntAttribute("timeout"); + // 参数映射 + String parameterMap = context.getStringAttribute("parameterMap"); + // 返回值类型 + String resultType = context.getStringAttribute("resultType"); + Class resultTypeClass = resolveClass(resultType); + // 返回值映射 map + String resultMap = context.getStringAttribute("resultMap"); + // 结果集类型 + String resultSetType = context.getStringAttribute("resultSetType"); + ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType); + // 插入、更新生成键值的字段 + String keyProperty = context.getStringAttribute("keyProperty"); + // 插入、更新生成键值的列 + String keyColumn = context.getStringAttribute("keyColumn"); + // 指定多结果集名称 + String resultSets = context.getStringAttribute("resultSets"); + + // 新增 MappedStatement + builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, + fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, + resultSetTypeEnum, flushCache, useCache, resultOrdered, + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets); + } +``` + +### 语法驱动 + +`LanguageDriver` 是 `statement` 创建语法驱动,默认实现为 `XMLLanguageDriver`,其提供 `createSqlSource` 方法用于使用 `XMLScriptBuilder` 创建 `sql` 生成对象。 + +### 递归解析 include + +`include` 元素是 `statement` 元素的子元素,通过 `refid` 属性可以指向在别处定义的 `sql fragments`。 + +```java + public void applyIncludes(Node source) { + Properties variablesContext = new Properties(); + Properties configurationVariables = configuration.getVariables(); + // 拷贝全局配置中设置的额外配置属性 + Optional.ofNullable(configurationVariables).ifPresent(variablesContext::putAll); + applyIncludes(source, variablesContext, false); + } + + /** + * 递归解析 statement 元素中的 include 元素 + * + * Recursively apply includes through all SQL fragments. + * @param source Include node in DOM tree + * @param variablesContext Current context for static variables with values + */ + private void applyIncludes(Node source, final Properties variablesContext, boolean included) { + if (source.getNodeName().equals("include")) { + // include 元素,从全局配置中找对应的 sql 节点并 clone + Node toInclude = findSqlFragment(getStringAttribute(source, "refid"), variablesContext); + // 读取 include 子元素中的 property 元素,获取全部属性 + Properties toIncludeContext = getVariablesContext(source, variablesContext); + applyIncludes(toInclude, toIncludeContext, true); + if (toInclude.getOwnerDocument() != source.getOwnerDocument()) { + toInclude = source.getOwnerDocument().importNode(toInclude, true); + } + source.getParentNode().replaceChild(toInclude, source); + while (toInclude.hasChildNodes()) { + toInclude.getParentNode().insertBefore(toInclude.getFirstChild(), toInclude); + } + toInclude.getParentNode().removeChild(toInclude); + } else if (source.getNodeType() == Node.ELEMENT_NODE) { + if (included && !variablesContext.isEmpty()) { + // replace variables in attribute values + // include 指向的 sql clone 节点,逐个对属性进行解析 + NamedNodeMap attributes = source.getAttributes(); + for (int i = 0; i < attributes.getLength(); i++) { + Node attr = attributes.item(i); + attr.setNodeValue(PropertyParser.parse(attr.getNodeValue(), variablesContext)); + } + } + // statement 元素中可能包含 include 子元素 + NodeList children = source.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + applyIncludes(children.item(i), variablesContext, included); + } + } else if (included && source.getNodeType() == Node.TEXT_NODE + && !variablesContext.isEmpty()) { + // replace variables in text node + // 替换元素值,如果使用了 ${} 占位符,会对 token 进行解析 + source.setNodeValue(PropertyParser.parse(source.getNodeValue(), variablesContext)); + } + } + + /** + * 从全局配置中找对应的 sql fragment + * + * @param refid + * @param variables + * @return + */ + private Node findSqlFragment(String refid, Properties variables) { + // 解析 refid + refid = PropertyParser.parse(refid, variables); + // namespace.refid + refid = builderAssistant.applyCurrentNamespace(refid, true); + try { + // 从全局配置中找对应的 sql fragment + XNode nodeToInclude = configuration.getSqlFragments().get(refid); + return nodeToInclude.getNode().cloneNode(true); + } catch (IllegalArgumentException e) { + // sql fragments 定义在全局配置中的 StrictMap 中,获取不到会抛出异常 + throw new IncompleteElementException("Could not find SQL statement to include with refid '" + refid + "'", e); + } + } + + private String getStringAttribute(Node node, String name) { + return node.getAttributes().getNamedItem(name).getNodeValue(); + } + + /** + * Read placeholders and their values from include node definition. + * + * 读取 include 子元素中的 property 元素 + * @param node Include node instance + * @param inheritedVariablesContext Current context used for replace variables in new variables values + * @return variables context from include instance (no inherited values) + */ + private Properties getVariablesContext(Node node, Properties inheritedVariablesContext) { + Map declaredProperties = null; + // 解析 include 元素中的 property 子元素 + NodeList children = node.getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + Node n = children.item(i); + if (n.getNodeType() == Node.ELEMENT_NODE) { + // include 运行包含 property 元素 + String name = getStringAttribute(n, "name"); + // Replace variables inside + String value = PropertyParser.parse(getStringAttribute(n, "value"), inheritedVariablesContext); + if (declaredProperties == null) { + declaredProperties = new HashMap<>(); + } + if (declaredProperties.put(name, value) != null) { + // 不允许添加同名属性 + throw new BuilderException("Variable " + name + " defined twice in the same include definition"); + } + } + } + if (declaredProperties == null) { + return inheritedVariablesContext; + } else { + // 聚合属性配置 + Properties newProperties = new Properties(); + newProperties.putAll(inheritedVariablesContext); + newProperties.putAll(declaredProperties); + return newProperties; + } + } +``` + +在开始解析前,从全局配置中获取全部的属性配置,如果 `include` 元素中有 `property` 元素,解析并获取键值,放入 `variablesContext` 中,在后续处理中针对可能出现的 `${}` 类型 `token` 使用 `PropertyParser` 进行解析。 + +因为解析 `statement` 元素前已经加载过 `sql` 元素,因此会根据 `include` 元素的 `refid` 属性查找对应的 `sql fragments`,如果全局配置中无法找到就会抛出异常;如果能够找到则克隆 `sql` 元素并插入到当前 `xml` 文档中。 + +### 解析 selectKey + +`selectKey` 用于指定 `sql` 在 `insert` 或 `update` 语句执行前或执行后生成或获取列值,在 `MyBatis` 中 `selectKey` 也被当做 `statement` 语句进行解析并设置到全局配置中。单个 `selectKey` 元素会以`SelectKeyGenerator` 对象的形式进行保存用于后续调用。 + +```java + private void parseSelectKeyNode(String id, XNode nodeToHandle, Class parameterTypeClass, LanguageDriver langDriver, String databaseId) { + // 返回值类型 + String resultType = nodeToHandle.getStringAttribute("resultType"); + Class resultTypeClass = resolveClass(resultType); + StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString())); + // 对应字段名 + String keyProperty = nodeToHandle.getStringAttribute("keyProperty"); + // 对应列名 + String keyColumn = nodeToHandle.getStringAttribute("keyColumn"); + // 是否在父sql执行前执行 + boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER")); + + //defaults + boolean useCache = false; + boolean resultOrdered = false; + KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE; + Integer fetchSize = null; + Integer timeout = null; + boolean flushCache = false; + String parameterMap = null; + String resultMap = null; + ResultSetType resultSetTypeEnum = null; + + // 创建 sql 生成对象 + SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass); + SqlCommandType sqlCommandType = SqlCommandType.SELECT; + + // 将 KeyGenerator 生成 sql 作为 MappedStatement 加入全局对象 + builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType, + fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass, + resultSetTypeEnum, flushCache, useCache, resultOrdered, + keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null); + + id = builderAssistant.applyCurrentNamespace(id, false); + + MappedStatement keyStatement = configuration.getMappedStatement(id, false); + // 包装为 SelectKeyGenerator 对象 + configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore)); + } +``` + +在 `selectKey` 解析完成后,按指定的 `namespace` 规则从全局配置中获取 `SelectKeyGenerator` 对象,等待创建 `MappedStatement` 对象。如果未指定 `selectKey` 元素,但是全局配置中开启了 `useGeneratedKeys`,并且指定 `insert` 元素的 `useGeneratedKeys` 属性为 `true`,则 `MyBatis` 会指定 `Jdbc3KeyGenerator` 作为 `useGeneratedKeys` 的默认实现。 + +### 创建 sql 生成对象 + +#### SqlSource + +`SqlSource` 是 `sql` 生成抽象接口,其提供 `getBoundSql` 方法用于根据参数生成有效 `sql` 语句和参数绑定对象 `BoundSql`。在生成 `statement` 元素的解析结果 `MappedStatement` 对象前,需要先创建 `sql` 生成对象,即 `SqlSource` 对象。 + +```java +public interface SqlSource { + + /** + * 根据参数生成有效 sql 语句和参数绑定对象 + * + * @param parameterObject + * @return + */ + BoundSql getBoundSql(Object parameterObject); + +} +``` + +#### SqlNode + +`SqlNode` 是 `sql` 节点抽象接口。`sql` 节点指的是 `statement` 中的组成部分,如果简单文本、`if` 元素、`where` 元素等。`SqlNode` 提供 `apply` 方法用于判断当前 `sql` 节点是否可以加入到生效的 `sql` 语句中。 + +```java +public interface SqlNode { + + /** + * 根据条件判断当前 sql 节点是否可以加入到生效的 sql 语句中 + * + * @param context + * @return + */ + boolean apply(DynamicContext context); +} +``` + +![SqlNode 体系](https://wch853.github.io/img/mybatis/SqlNode%E4%BD%93%E7%B3%BB.png) + +#### DynamicContext + +`DynamicContext` 是动态 `sql` 上下文,用于保存绑定参数和生效 `sql` 节点。`DynamicContext` 使用 `ContextMap` 作为参数绑定容器。由于动态 `sql` 是根据参数条件组合生成 `sql`,`DynamicContext` 还提供了对 `sqlBuilder` 修改和访问方法,用于添加有效 `sql` 节点和生成 `sql` 文本。 + +```java + /** + * 生效的 sql 部分,以空格相连 + */ + private final StringJoiner sqlBuilder = new StringJoiner(" "); + + public void appendSql(String sql) { + sqlBuilder.add(sql); + } + + public String getSql() { + return sqlBuilder.toString().trim(); + } +``` + +#### 节点解析 + +将 `statement` 元素转为 `sql` 生成对象依赖于 `LanguageDriver` 的 `createSqlSource` 方法,此方法中创建 `XMLScriptBuilder` 对象,并调用 `parseScriptNode` 方法对 `sql` 组成节点逐个解析并进行组合。 + +```java + public SqlSource parseScriptNode() { + // 递归解析各 sql 节点 + MixedSqlNode rootSqlNode = parseDynamicTags(context); + SqlSource sqlSource; + if (isDynamic) { + // 动态 sql + sqlSource = new DynamicSqlSource(configuration, rootSqlNode); + } else { + // 原始文本 sql + sqlSource = new RawSqlSource(configuration, rootSqlNode, parameterType); + } + return sqlSource; + } + + /** + * 处理 statement 各 SQL 组成部分,并进行组合 + */ + protected MixedSqlNode parseDynamicTags(XNode node) { + // SQL 各组成部分 + List contents = new ArrayList<>(); + // 遍历子元素 + NodeList children = node.getNode().getChildNodes(); + for (int i = 0; i < children.getLength(); i++) { + XNode child = node.newXNode(children.item(i)); + if (child.getNode().getNodeType() == Node.CDATA_SECTION_NODE || child.getNode().getNodeType() == Node.TEXT_NODE) { + // 解析 sql 文本 + String data = child.getStringBody(""); + TextSqlNode textSqlNode = new TextSqlNode(data); + if (textSqlNode.isDynamic()) { + // 判断是否为动态 sql,包含 ${} 占位符即为动态 sql + contents.add(textSqlNode); + isDynamic = true; + } else { + // 静态 sql 元素 + contents.add(new StaticTextSqlNode(data)); + } + } else if (child.getNode().getNodeType() == Node.ELEMENT_NODE) { // issue #628 + // 如果是子元素 + String nodeName = child.getNode().getNodeName(); + // 获取支持的子元素语法处理器 + NodeHandler handler = nodeHandlerMap.get(nodeName); + if (handler == null) { + throw new BuilderException("Unknown element <" + nodeName + "> in SQL statement."); + } + // 根据子元素标签类型使用对应的处理器处理子元素 + handler.handleNode(child, contents); + // 包含标签元素,认定为动态 SQL + isDynamic = true; + } + } + return new MixedSqlNode(contents); + } +``` + +`parseDynamicTags` 方法会对 `sql` 各组成部分进行分解,如果 `statement` 元素包含 `${}` 类型 `token` 或含有标签子元素,则认为当前 `statement` 是动态 `sql`,随后 `isDynamic` 属性会被设置为 `true`。对于文本节点,如 `sql` 纯文本和仅含 `${}` 类型 `token` 的文本,会被包装为 `StaticTextSqlNode` 或 `TextSqlNode` 加入到 `sql` 节点容器中,而其它元素类型的 `sql` 节点会经过 `NodeHandler` 的 `handleNode` 方法处理过之后才能加入到节点容器中。`nodeHandlerMap` 定义了不同动态 `sql` 元素节点与 `NodeHandler` 的关系: + +```java + private void initNodeHandlerMap() { + nodeHandlerMap.put("trim", new TrimHandler()); + nodeHandlerMap.put("where", new WhereHandler()); + nodeHandlerMap.put("set", new SetHandler()); + nodeHandlerMap.put("foreach", new ForEachHandler()); + nodeHandlerMap.put("if", new IfHandler()); + nodeHandlerMap.put("choose", new ChooseHandler()); + nodeHandlerMap.put("when", new IfHandler()); + nodeHandlerMap.put("otherwise", new OtherwiseHandler()); + nodeHandlerMap.put("bind", new BindHandler()); + } +``` + +##### MixedSqlNode + +`MixedSqlNode` 中定义了一个 `SqlNode` 集合,用于保存 `statement` 中包含的全部 `sql` 节点。其生成有效 `sql` 的逻辑为逐个判断节点是否有效。 + +```java + /** + * 组合 SQL 各组成部分 + * + * @author Clinton Begin + */ + public class MixedSqlNode implements SqlNode { + + /** + * SQL 各组装成部分 + */ + private final List contents; + + public MixedSqlNode(List contents) { + this.contents = contents; + } + + @Override + public boolean apply(DynamicContext context) { + // 逐个判断各个 sql 节点是否能生效 + contents.forEach(node -> node.apply(context)); + return true; + } + } +``` + +##### StaticTextSqlNode + +`StaticTextSqlNode` 中仅包含静态 `sql` 文本,在组装时会直接追加到 `sql` 上下文的有效 `sql` 中: + +```java + @Override + public boolean apply(DynamicContext context) { + context.appendSql(text); + return true; + } +``` + +##### TextSqlNode + +`TextSqlNode` 中的 `sql` 文本包含 `${}` 类型 `token`,使用 `GenericTokenParser` 搜索到 `token` 后会使用 `BindingTokenParser` 对 `token` 进行解析,解析后的文本会被追加到生效 `sql` 中。 + +```java + @Override + public boolean apply(DynamicContext context) { + // 搜索 ${} 类型 token 节点 + GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter)); + // 解析 token 并追加解析后的文本到生效 sql 中 + context.appendSql(parser.parse(text)); + return true; + } + + private GenericTokenParser createParser(TokenHandler handler) { + return new GenericTokenParser("${", "}", handler); + } + + private static class BindingTokenParser implements TokenHandler { + + private DynamicContext context; + private Pattern injectionFilter; + + public BindingTokenParser(DynamicContext context, Pattern injectionFilter) { + this.context = context; + this.injectionFilter = injectionFilter; + } + + @Override + public String handleToken(String content) { + // 获取绑定参数 + Object parameter = context.getBindings().get("_parameter"); + if (parameter == null) { + context.getBindings().put("value", null); + } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) { + context.getBindings().put("value", parameter); + } + // 计算 ognl 表达式的值 + Object value = OgnlCache.getValue(content, context.getBindings()); + String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null" + checkInjection(srtValue); + return srtValue; + } + } +``` + +##### IfSqlNode + +`if` 标签用于在 `test` 条件生效时才追加标签内的文本。 + +```xml + ... + + AND user_id = #{userId} + +``` + +`IfSqlNode` 保存了 `if` 元素下的节点内容和 `test` 表达式,在生成有效 `sql` 时会根据 `OGNL` 工具计算 `test` 表达式是否生效。 + +```java + @Override + public boolean apply(DynamicContext context) { + // 根据 test 表达式判断当前节点是否生效 + if (evaluator.evaluateBoolean(test, context.getBindings())) { + contents.apply(context); + return true; + } + return false; + } +``` + +##### TrimSqlNode + +`trim` 标签用于解决动态 `sql` 中由于条件不同不能拼接正确语法的问题。 + +```xml + SELECT * FROM test + + + a = #{a} + + + OR b = #{b} + + + AND c = #{c} + + +``` + +如果没有 `trim` 标签,这个 `statement` 的有效 `sql` 最终可能会是这样的: + +```sql +SELECT * FROM test OR b = #{b} +``` + +但是加上 `trim` 标签,生成的 `sql` 语法是正确的: + +```sql +SELECT * FROM test WHERE b = #{b} +``` + +`prefix` 属性用于指定 `trim` 节点生成的 `sql` 语句的前缀,`prefixOverrides` 则会指定生成的 `sql` 语句的前缀需要去除的部分,多个需要去除的前缀可以使用 `|` 隔开。`suffix` 与 `suffixOverrides` 的功能类似,但是作用于后缀。 + +`TrimSqlNode` 首先调用 `parseOverrides` 对 `prefixOverrides` 和 `suffixOverrides` 进行解析,通过 `|` 分隔,分别加入字符串集合。 + +```java + private static List parseOverrides(String overrides) { + if (overrides != null) { + // 解析 token,按 | 分隔 + final StringTokenizer parser = new StringTokenizer(overrides, "|", false); + final List list = new ArrayList<>(parser.countTokens()); + while (parser.hasMoreTokens()) { + // 保存为字符串集合 + list.add(parser.nextToken().toUpperCase(Locale.ENGLISH)); + } + return list; + } + return Collections.emptyList(); + } +``` + +在调用包含的 `SqlNode` 的 `apply` 方法后还会调用 `FilteredDynamicContext` 的 `applyAll` 方法处理前缀和后缀。 + +```java + @Override + public boolean apply(DynamicContext context) { + FilteredDynamicContext filteredDynamicContext = new FilteredDynamicContext(context); + boolean result = contents.apply(filteredDynamicContext); + // 加上前缀和和后缀,并去除多余字段 + filteredDynamicContext.applyAll(); + return result; + } +``` + +对于已经生成的 `sql` 文本,分别根据规则加上和去除指定前缀和后缀。 + +```java + public void applyAll() { + sqlBuffer = new StringBuilder(sqlBuffer.toString().trim()); + String trimmedUppercaseSql = sqlBuffer.toString().toUpperCase(Locale.ENGLISH); + if (trimmedUppercaseSql.length() > 0) { + // 加上前缀和和后缀,并去除多余字段 + applyPrefix(sqlBuffer, trimmedUppercaseSql); + applySuffix(sqlBuffer, trimmedUppercaseSql); + } + delegate.appendSql(sqlBuffer.toString()); + } + + private void applyPrefix(StringBuilder sql, String trimmedUppercaseSql) { + if (!prefixApplied) { + prefixApplied = true; + if (prefixesToOverride != null) { + // 文本最前去除多余字段 + for (String toRemove : prefixesToOverride) { + if (trimmedUppercaseSql.startsWith(toRemove)) { + sql.delete(0, toRemove.trim().length()); + break; + } + } + } + // 在文本最前插入前缀和空格 + if (prefix != null) { + sql.insert(0, " "); + sql.insert(0, prefix); + } + } + } + + private void applySuffix(StringBuilder sql, String trimmedUppercaseSql) { + if (!suffixApplied) { + suffixApplied = true; + if (suffixesToOverride != null) { + // 文本最后去除多余字段 + for (String toRemove : suffixesToOverride) { + if (trimmedUppercaseSql.endsWith(toRemove) || trimmedUppercaseSql.endsWith(toRemove.trim())) { + int start = sql.length() - toRemove.trim().length(); + int end = sql.length(); + sql.delete(start, end); + break; + } + } + } + // 文本最后插入空格和后缀 + if (suffix != null) { + sql.append(" "); + sql.append(suffix); + } + } + } +``` + +##### WhereSqlNode + +`where` 元素与 `trim` 元素的功能类似,区别在于 `where` 元素不提供属性配置可以处理的前缀和后缀。 + +```xml + ... + + ... + +``` + +`WhereSqlNode` 继承了 `TrimSqlNode`,并指定了需要添加和删除的前缀。 + +```java +public class WhereSqlNode extends TrimSqlNode { + + private static List prefixList = Arrays.asList("AND ","OR ","AND\n", "OR\n", "AND\r", "OR\r", "AND\t", "OR\t"); + + public WhereSqlNode(Configuration configuration, SqlNode contents) { + // 默认添加 WHERE 前缀,去除 AND、OR 等前缀 + super(configuration, contents, "WHERE", prefixList, null, null); + } + +} +``` + +因此,生成的 `sql` 语句会自动在最前加上 `WHERE`,并去除前缀中包含的 `AND`、`OR` 等字符串。 + +##### SetSqlNode + +`set` 标签用于 `update` 语句中。 + +```xml + UPDATE test + + + a = #{a}, + + + b = #{b} + + +``` + +`SetSqlNode` 同样继承自 `TrimSqlNode`,并指定默认添加 `SET` 前缀,去除 `,` 前缀和后缀。 + +```java +public class SetSqlNode extends TrimSqlNode { + + private static final List COMMA = Collections.singletonList(","); + + public SetSqlNode(Configuration configuration,SqlNode contents) { + // 默认添加 SET 前缀,去除 , 前缀和后缀 + super(configuration, contents, "SET", COMMA, null, COMMA); + } + +} +``` + +##### ForEachSqlNode + +`foreach` 元素用于指定对集合循环添加 `sql` 语句。 + +```xml +... + + AND itm = #{item} AND idx #{index} + +``` + +`ForEachSqlNode` 解析生成有效 `sql` 的逻辑如下,除了计算 `collection` 表达式的值、添加前缀、后缀外,还将参数与索引进行了绑定。 + +```java + @Override + public boolean apply(DynamicContext context) { + // 获取绑定参数 + Map bindings = context.getBindings(); + // 计算 ognl 表达式获取可迭代对象 + final Iterable iterable = evaluator.evaluateIterable(collectionExpression, bindings); + if (!iterable.iterator().hasNext()) { + return true; + } + boolean first = true; + // 添加动态语句前缀 + applyOpen(context); + // 迭代索引 + int i = 0; + for (Object o : iterable) { + DynamicContext oldContext = context; + // 首个元素 + if (first || separator == null) { + context = new PrefixedContext(context, ""); + } else { + context = new PrefixedContext(context, separator); + } + int uniqueNumber = context.getUniqueNumber(); + // Issue #709 + if (o instanceof Map.Entry) { + // entry 集合项索引为 key,集合项为 value + @SuppressWarnings("unchecked") + Map.Entry mapEntry = (Map.Entry) o; + applyIndex(context, mapEntry.getKey(), uniqueNumber); + applyItem(context, mapEntry.getValue(), uniqueNumber); + } else { + // 绑定集合项索引关系 + applyIndex(context, i, uniqueNumber); + // 绑定集合项关系 + applyItem(context, o, uniqueNumber); + } + // 对解析的表达式进行替换,如 idx = #{index} AND itm = #{item} 替换为 idx = #{__frch_index_1} AND itm = #{__frch_item_1} + contents.apply(new FilteredDynamicContext(configuration, context, index, item, uniqueNumber)); + if (first) { + first = !((PrefixedContext) context).isPrefixApplied(); + } + context = oldContext; + i++; + } + // 添加动态语句后缀 + applyClose(context); + // 移除原始的表达式 + context.getBindings().remove(item); + context.getBindings().remove(index); + return true; + } + + /** + * 绑定集合项索引关系 + * + * @param context + * @param o + * @param i + */ + private void applyIndex(DynamicContext context, Object o, int i) { + if (index != null) { + context.bind(index, o); + context.bind(itemizeItem(index, i), o); + } + } + + /** + * 绑定集合项关系 + * + * @param context + * @param o + * @param i + */ + private void applyItem(DynamicContext context, Object o, int i) { + if (item != null) { + context.bind(item, o); + context.bind(itemizeItem(item, i), o); + } + } +``` + +对于循环中的 `#{}` 类型 `token`,`ForEachSqlNode` 在内部类 `FilteredDynamicContext` 中定义了解析规则: + +```java + @Override + public void appendSql(String sql) { + GenericTokenParser parser = new GenericTokenParser("#{", "}", content -> { + // 对解析的表达式进行替换,如 idx = #{index} AND itm = #{item} 替换为 idx = #{__frch_index_1} AND itm = #{__frch_item_1} + String newContent = content.replaceFirst("^\\s*" + item + "(?![^.,:\\s])", itemizeItem(item, index)); + if (itemIndex != null && newContent.equals(content)) { + newContent = content.replaceFirst("^\\s*" + itemIndex + "(?![^.,:\\s])", itemizeItem(itemIndex, index)); + } + return "#{" + newContent + "}"; + }); + + delegate.appendSql(parser.parse(sql)); + } +``` + +类似 `idx = #{index} AND itm = #{item}` 会被替换为 `idx = #{__frch_index_1} AND itm = #{__frch_item_1}`,而 `ForEachSqlNode` 也做了参数与索引的绑定,因此在替换时可以快速绑定参数。 + +##### ChooseSqlNode + +`choose` 元素用于生成带默认 `sql` 文本的语句,当 `when` 元素中的条件都不生效,就可以使用 `otherwise` 元素的默认文本。 + +```xml + ... + + + AND a = #{a} + + + AND b = #{b} + + +``` + +`ChooseSqlNode` 是由 `choose` 节点和 `otherwise` 节点组合而成的,在生成有效 `sql` 于语句时会逐个计算 `when` 节点的 `test` 表达式,如果返回 `true` 则生效当前 `when` 语句中的 `sql`。如果均不生效则使用 `otherwise` 语句对应的默认 `sql` 文本。 + +```java +@Override +public boolean apply(DynamicContext context) { + // when 节点根据 test 表达式判断是否生效 + for (SqlNode sqlNode : ifSqlNodes) { + if (sqlNode.apply(context)) { + return true; + } + } + + // when 节点如果都未生效,且存在 otherwise 节点,则使用 otherwise 节点 + if (defaultSqlNode != null) { + defaultSqlNode.apply(context); + return true; + } + return false; +} +``` + +##### VarDeclSqlNode + +`bind` 元素用于绑定一个 `OGNL` 表达式到一个动态 `sql` 变量中。 + +```xml + +``` + +`VarDeclSqlNode` 会计算表达式的值并将参数名和值绑定到参数容器中。 + +```java +@Override +public boolean apply(DynamicContext context) { + // 解析 ognl 表达式 + final Object value = OgnlCache.getValue(expression, context.getBindings()); + // 绑定参数 + context.bind(name, value); + return true; +} +``` + +### 创建解析对象与生成可执行 sql + +`statemen` 解析完毕后会创建 `MappedStatement` 对象,`statement` 的相关属性以及生成的 `sql` 创建对象都会被保存到该对象中。`MappedStatement` 还提供了 `getBoundSql` 方法用于获取可执行 sql 和参数绑定对象,即 `BoundSql` 对象。 + +```java +public BoundSql getBoundSql(Object parameterObject) { + // 生成可执行 sql 和参数绑定对象 + BoundSql boundSql = sqlSource.getBoundSql(parameterObject); + // 获取参数映射 + List parameterMappings = boundSql.getParameterMappings(); + if (parameterMappings == null || parameterMappings.isEmpty()) { + boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject); + } + + // check for nested result maps in parameter mappings (issue #30) + // 检查是否有嵌套的 resultMap + for (ParameterMapping pm : boundSql.getParameterMappings()) { + String rmId = pm.getResultMapId(); + if (rmId != null) { + ResultMap rm = configuration.getResultMap(rmId); + if (rm != null) { + hasNestedResultMaps |= rm.hasNestedResultMaps(); + } + } + } + + return boundSql; +} +``` + +`BoundSql` 对象由 `DynamicSqlSource` 的 `getBoundSql` 方法生成,在验证各个 `sql` 节点,生成了有效 `sql` 后会继续调用 `SqlSourceBuilder` 将 `sql` 解析为 `StaticSqlSource`,即可执行 `sql`。 + +```java + @Override + public BoundSql getBoundSql(Object parameterObject) { + DynamicContext context = new DynamicContext(configuration, parameterObject); + // 验证各 sql 节点,生成有效 sql + rootSqlNode.apply(context); + SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration); + Class parameterType = parameterObject == null ? Object.class : parameterObject.getClass(); + // 将生成的 sql 文本解析为 StaticSqlSource + SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings()); + BoundSql boundSql = sqlSource.getBoundSql(parameterObject); + context.getBindings().forEach(boundSql::setAdditionalParameter); + return boundSql; + } +``` + +此时的 `sql` 文本中仍包含 `#{}` 类型 `token`,需要通过 `ParameterMappingTokenHandler` 进行解析。 + +```java + public SqlSource parse(String originalSql, Class parameterType, Map additionalParameters) { + ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters); + // 创建 #{} 类型 token 搜索对象 + GenericTokenParser parser = new GenericTokenParser("#{", "}", handler); + // 解析 token + String sql = parser.parse(originalSql); + // 创建静态 sql 生成对象,并绑定参数 + return new StaticSqlSource(configuration, sql, handler.getParameterMappings()); + } +``` + +`token` 的具体解析逻辑为根据表达式的参数名生成对应的参数映射对象,并将表达式转为预编译 `sql` 的占位符 `?`。 + +```java + @Override + public String handleToken(String content) { + // 创建参数映射对象 + parameterMappings.add(buildParameterMapping(content)); + // 将表达式转为预编译 sql 占位符 + return "?"; + } +``` + +最终解析完成的 `sql` 与参数映射关系集合包装为 `StaticSqlSource` 对象,该对象在随后的逻辑中通过构造方法创建了 `BoundSql` 对象。 + +## 接口解析 + +除了使用 `xml` 方式配置 `statement`,`MyBatis` 同样支持使用 `Java` 注解配置。但是相对于 `xml` 的映射方式,将动态 `sql` 写在 `Java` 代码中是不合适的。如果在配置文件中指定了需要注册 `Mapper` 接口的类或包,`MyBatis` 会扫描相关类进行注册;在 `Mapper` 文件解析完成后也会尝试加载 `namespace` 的同名类,如果存在,则注册为 `Mapper` 接口。 + +无论是绑定还是直接注册 `Mapper` 接口,都是调用 `MapperAnnotationBuilder#parse` 方法来解析的。此方法中的解析方式与上述 `xml` 解析方式大致相同,区别只在于相关配置参数是从注解中获取而不是从 `xml` 元素属性中获取。 + +```java + public void addMapper(Class type) { + if (type.isInterface()) { + if (hasMapper(type)) { + // 不允许相同接口重复注册 + throw new BindingException("Type " + type + " is already known to the MapperRegistry."); + } + boolean loadCompleted = false; + try { + knownMappers.put(type, new MapperProxyFactory<>(type)); + // It's important that the type is added before the parser is run + // otherwise the binding may automatically be attempted by the + // mapper parser. If the type is already known, it won't try. + MapperAnnotationBuilder parser = new MapperAnnotationBuilder(config, type); + parser.parse(); + loadCompleted = true; + } finally { + if (!loadCompleted) { + knownMappers.remove(type); + } + } + } + } +``` + +## 小结 + +`statement` 解析的最终目的是为每个 `statement` 创建一个 `MappedStatement` 对象保存相关定义,在 `sql` 执行时根据传入参数动态获取可执行 `sql` 和参数绑定对象。 + +- `org.apache.ibatis.builder.xml.XMLStatementBuilder`:解析 `Mapper` 文件中的 `select|insert|update|delete` 元素。 +- `org.apache.ibatis.parsing.GenericTokenParser.GenericTokenParser`:搜索指定格式 `token` 并进行解析。 +- `org.apache.ibatis.parsing.TokenHandler`:`token` 处理器抽象接口。定义 `token` 以何种方式被解析。 +- `org.apache.ibatis.parsing.PropertyParser`:`${}` 类型 `token` 解析器。 +- `org.apache.ibatis.session.Configuration.StrictMap`:封装 `HashMap`,对键值存取有严格要求。 +- `org.apache.ibatis.builder.xml.XMLIncludeTransformer`:`include` 元素解析器。 +- `org.apache.ibatis.mapping.SqlSource`:`sql` 生成抽象接口。根据传入参数生成有效 `sql` 语句和参数绑定对象。 +- `org.apache.ibatis.scripting.xmltags.XMLScriptBuilder`:解析 `statement` 各个 `sql` 节点并进行组合。 +- `org.apache.ibatis.scripting.xmltags.SqlNode`:`sql` 节点抽象接口。用于判断当前 `sql` 节点是否可以加入到生效的 sql 语句中。 +- `org.apache.ibatis.scripting.xmltags.DynamicContext`:动态 `sql` 上下文。用于保存绑定参数和生效 `sql` 节点。 +- `org.apache.ibatis.scripting.xmltags.OgnlCache`:`ognl` 缓存工具,缓存表达式编译结果。 +- `org.apache.ibatis.scripting.xmltags.ExpressionEvaluator`:`ognl` 表达式计算工具。 +- `org.apache.ibatis.scripting.xmltags.MixedSqlNode`:`sql` 节点组合对象。 +- `org.apache.ibatis.scripting.xmltags.StaticTextSqlNode`:静态 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.TextSqlNode`:`${}` 类型 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.IfSqlNode`:`if` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.TrimSqlNode`:`trim` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.WhereSqlNode`:`where` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.SetSqlNode`:`set` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.ForEachSqlNode`:`foreach` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.ChooseSqlNode`:`choose` 元素 `sql` 节点对象。 +- `org.apache.ibatis.scripting.xmltags.VarDeclSqlNode`:`bind` 元素 `sql` 节点对象。 +- `org.apache.ibatis.mapping.MappedStatement`:`statement` 解析对象。 +- `org.apache.ibatis.mapping.BoundSql`:可执行 `sql` 和参数绑定对象。 +- `org.apache.ibatis.scripting.xmltags.DynamicSqlSource`:根据参数动态生成有效 `sql` 和绑定参数。 +- `org.apache.ibatis.builder.SqlSourceBuilder`:解析 `#{}` 类型 `token` 并绑定参数对象 \ No newline at end of file diff --git "a/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2208--\346\211\247\350\241\214\345\231\250.md" "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2208--\346\211\247\350\241\214\345\231\250.md" new file mode 100644 index 0000000..54b81e8 --- /dev/null +++ "b/docs/source/mybatis/MyBatis \346\272\220\347\240\201\345\210\206\346\236\2208--\346\211\247\350\241\214\345\231\250.md" @@ -0,0 +1,765 @@ +执行器 `Executor` 是 `MyBatis` 的核心接口之一,接口层提供的相关数据库操作,都是基于 `Executor` 的子类实现的。 + +![Executor 体系](https://wch853.github.io/img/mybatis/Executor%E4%BD%93%E7%B3%BB.png) + +## 创建执行器 + +在创建 `sql` 会话时,`MyBatis` 会调用 `Configuration#newExecutor` 方法创建执行器。枚举类 `ExecutorType` 定义了三种执行器类型,即 `SIMPLE`、`REUSE` 和 `Batch`,这些执行器的主要区别在于: + +- `SIMPLE` 在每次执行完成后都会关闭 `statement` 对象; +- `REUSE` 会在本地维护一个容器,当前 `statement` 创建完成后放入容器中,当下次执行相同的 `sql` 时会复用 `statement` 对象,执行完毕后也不会关闭; +- `BATCH` 会将修改操作记录在本地,等待程序触发或有下一次查询时才批量执行修改操作。 + +```java +public Executor newExecutor(Transaction transaction, ExecutorType executorType) { + // 默认类型为 simple + executorType = executorType == null ? defaultExecutorType : executorType; + executorType = executorType == null ? ExecutorType.SIMPLE : executorType; + Executor executor; + if (ExecutorType.BATCH == executorType) { + executor = new BatchExecutor(this, transaction); + } else if (ExecutorType.REUSE == executorType) { + executor = new ReuseExecutor(this, transaction); + } else { + executor = new SimpleExecutor(this, transaction); + } + if (cacheEnabled) { + // 如果全局缓存打开,使用 CachingExecutor 代理执行器 + executor = new CachingExecutor(executor); + } + // 应用插件 + executor = (Executor) interceptorChain.pluginAll(executor); + return executor; +} +``` + +执行器创建后,如果全局缓存配置是有效的,则会将执行器装饰为 `CachingExecutor`。 + +## 基础执行器 + +`SimpleExecutor`、`ReuseExecutor`、`BatchExecutor` 均继承自 `BaseExecutor`。`BaseExecutor` 实现了 `Executor` 的全部方法,对缓存、事务、连接处理等提供了一些模板方法,但是针对具体的数据库操作留下了四个抽象方法交由子类实现。 + +```java + /** + * 更新 + */ + protected abstract int doUpdate(MappedStatement ms, Object parameter) + throws SQLException; + + /** + * 刷新 statement + */ + protected abstract List doFlushStatements(boolean isRollback) + throws SQLException; + + /** + * 查询 + */ + protected abstract List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) + throws SQLException; + + /** + * 查询获取游标对象 + */ + protected abstract Cursor doQueryCursor(MappedStatement ms, Object parameter, RowBounds rowBounds, BoundSql boundSql) + throws SQLException; +``` + +基础执行器的查询逻辑如下: + +```java + @Override + public List query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { + ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId()); + if (closed) { + throw new ExecutorException("Executor was closed."); + } + if (queryStack == 0 && ms.isFlushCacheRequired()) { + // 非嵌套查询且设置强制刷新时清除缓存 + clearLocalCache(); + } + List list; + try { + queryStack++; + list = resultHandler == null ? (List) localCache.getObject(key) : null; + if (list != null) { + // 缓存不为空,组装存储过程出参 + handleLocallyCachedOutputParameters(ms, key, parameter, boundSql); + } else { + // 无本地缓存,执行数据库查询 + list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql); + } + } finally { + queryStack--; + } + if (queryStack == 0) { + for (DeferredLoad deferredLoad : deferredLoads) { + deferredLoad.load(); + } + // issue #601 + deferredLoads.clear(); + if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) { + // issue #482 + // 全局配置语句不共享缓存 + clearLocalCache(); + } + } + return list; + } + + /** + * 查询本地缓存,组装存储过程结果集 + */ + private void handleLocallyCachedOutputParameters(MappedStatement ms, CacheKey key, Object parameter, BoundSql boundSql) { + if (ms.getStatementType() == StatementType.CALLABLE) { + // 存储过程类型,查询缓存 + final Object cachedParameter = localOutputParameterCache.getObject(key); + if (cachedParameter != null && parameter != null) { + final MetaObject metaCachedParameter = configuration.newMetaObject(cachedParameter); + final MetaObject metaParameter = configuration.newMetaObject(parameter); + for (ParameterMapping parameterMapping : boundSql.getParameterMappings()) { + if (parameterMapping.getMode() != ParameterMode.IN) { + // 参数类型为 OUT 或 INOUT 的,组装结果集 + final String parameterName = parameterMapping.getProperty(); + final Object cachedValue = metaCachedParameter.getValue(parameterName); + metaParameter.setValue(parameterName, cachedValue); + } + } + } + } + } + + /** + * 查询数据库获取结果集 + */ + private List queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException { + List list; + // 放一个 placeHolder 标志 + localCache.putObject(key, EXECUTION_PLACEHOLDER); + try { + // 执行查询 + list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql); + } finally { + localCache.removeObject(key); + } + // 查询结果集放入本地缓存 + localCache.putObject(key, list); + if (ms.getStatementType() == StatementType.CALLABLE) { + // 如果是存储过程查询,将存储过程结果集放入本地缓存 + localOutputParameterCache.putObject(key, parameter); + } + return list; + } +``` + +执行查询时 `MyBatis` 首先会根据 `CacheKey` 查询本地缓存,`CacheKey` 由本次查询的参数生成,本地缓存由 `PerpetualCache` 实现,这就是 `MyBatis` 的一级缓存。一级缓存维护对象 `localCache` 是基础执行器的本地变量,因此只有相同 `sql` 会话的查询才能共享一级缓存。当一级缓存中没有对应的数据,基础执行器最终会调用 `doQuery` 方法交由子类去获取数据。 + +而执行 `update` 等其它操作时,则会首先清除本地的一级缓存再交由子类执行具体的操作: + +```java + @Override + public int update(MappedStatement ms, Object parameter) throws SQLException { + ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId()); + if (closed) { + throw new ExecutorException("Executor was closed."); + } + // 清空本地缓存 + clearLocalCache(); + // 调用子类执行器逻辑 + return doUpdate(ms, parameter); + } +``` + +## 简单执行器 + +简单执行器是 `MyBatis` 的默认执行器。其封装了对 `JDBC` 的操作,对于查询方法 `doQuery` 的实现如下,其主要包括创建 `statement` 处理器、创建 `statement`、执行查询、关闭 `statement`。 + +```java + @Override + public List doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException { + Statement stmt = null; + try { + Configuration configuration = ms.getConfiguration(); + // 创建 statement 处理器 + StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); + // 创建 statement + stmt = prepareStatement(handler, ms.getStatementLog()); + // 执行查询 + return handler.query(stmt, resultHandler); + } finally { + // 关闭 statement + closeStatement(stmt); + } + } +``` + +### 创建 statement 处理器 + +全局配置类 `Configuration` 提供了方法 `newStatementHandler` 用于创建 `statement` 处理器: + +```java + /** + * 创建 statement 处理器 + */ + public StatementHandler newStatementHandler(Executor executor, MappedStatement mappedStatement, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + // StatementHandler 包装对象,根据 statement 类型创建代理处理器,并将实际操作委托给代理处理器处理 + StatementHandler statementHandler = new RoutingStatementHandler(executor, mappedStatement, parameterObject, rowBounds, resultHandler, boundSql); + // 应用插件 + statementHandler = (StatementHandler) interceptorChain.pluginAll(statementHandler); + return statementHandler; + } +``` + +实际每次创建的 `statement` 处理器对象都是由 `RoutingStatementHandler` 创建的,`RoutingStatementHandler` 根据当前 `MappedStatement` 的类型创建具体的 `statement` 类型处理器。`StatementType` 定义了 `3` 个 `statement` 类型枚举,分别对应 `JDBC` 的普通语句、预编译语句和存储过程语句。 + +```java + public RoutingStatementHandler(Executor executor, MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { + // 根据 statement 类型选择对应的 statementHandler + switch (ms.getStatementType()) { + case STATEMENT: + delegate = new SimpleStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case PREPARED: + delegate = new PreparedStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + case CALLABLE: + delegate = new CallableStatementHandler(executor, ms, parameter, rowBounds, resultHandler, boundSql); + break; + default: + throw new ExecutorException("Unknown statement type: " + ms.getStatementType()); + } + } +``` + +### 创建 statement + +简单执行器中的 `Statement` 对象是根据上述步骤中生成的 `statement` 处理器获取的。 + +```java + private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { + Statement stmt; + // 获取代理连接对象 + Connection connection = getConnection(statementLog); + // 创建 statement 对象 + stmt = handler.prepare(connection, transaction.getTimeout()); + // 设置 statement 参数 + handler.parameterize(stmt); + return stmt; + } + + @Override + public Statement prepare(Connection connection, Integer transactionTimeout) throws SQLException { + ErrorContext.instance().sql(boundSql.getSql()); + Statement statement = null; + try { + // 从连接中创建 statement 对象 + statement = instantiateStatement(connection); + // 设置超时时间 + setStatementTimeout(statement, transactionTimeout); + // 设置分批获取数据数量 + setFetchSize(statement); + return statement; + } catch (SQLException e) { + closeStatement(statement); + throw e; + } catch (Exception e) { + closeStatement(statement); + throw new ExecutorException("Error preparing statement. Cause: " + e, e); + } + } +``` + +### 执行查询 + +创建 `statement` 对象完成即可通过 `JDBC` 的 `API` 执行数据库查询,并从 `statement` 对象中获取查询结果,根据配置进行转换。 + +```java + @Override + public List query(Statement statement, ResultHandler resultHandler) throws SQLException { + PreparedStatement ps = (PreparedStatement) statement; + // 执行查询 + ps.execute(); + // 处理结果集 + return resultSetHandler.handleResultSets(ps); + } + + /** + * 处理结果集 + */ + @Override + public List handleResultSets(Statement stmt) throws SQLException { + ErrorContext.instance().activity("handling results").object(mappedStatement.getId()); + // 多结果集 + final List multipleResults = new ArrayList<>(); + int resultSetCount = 0; + ResultSetWrapper rsw = getFirstResultSet(stmt); + // statement 对应的所有 ResultMap 对象 + List resultMaps = mappedStatement.getResultMaps(); + int resultMapCount = resultMaps.size(); + // 验证结果集不为空时,ResultMap 数量不能为 0 + validateResultMapsCount(rsw, resultMapCount); + while (rsw != null && resultMapCount > resultSetCount) { + // 逐个获取 ResultMap + ResultMap resultMap = resultMaps.get(resultSetCount); + // 转换结果集,放到 multipleResults 容器中 + handleResultSet(rsw, resultMap, multipleResults, null); + // 获取下一个待处理的结果集 + rsw = getNextResultSet(stmt); + cleanUpAfterHandlingResultSet(); + resultSetCount++; + } + + // statement 配置的多结果集类型 + String[] resultSets = mappedStatement.getResultSets(); + if (resultSets != null) { + while (rsw != null && resultSetCount < resultSets.length) { + ResultMapping parentMapping = nextResultMaps.get(resultSets[resultSetCount]); + if (parentMapping != null) { + String nestedResultMapId = parentMapping.getNestedResultMapId(); + ResultMap resultMap = configuration.getResultMap(nestedResultMapId); + handleResultSet(rsw, resultMap, null, parentMapping); + } + rsw = getNextResultSet(stmt); + cleanUpAfterHandlingResultSet(); + resultSetCount++; + } + } + + return collapseSingleResultList(multipleResults); + } +``` + +### 关闭连接 + +查询完成后 `statement` 对象会被关闭。 + +```java + /** + * 关闭 statement + * + * @param statement + */ + protected void closeStatement(Statement statement) { + if (statement != null) { + try { + statement.close(); + } catch (SQLException e) { + // ignore + } + } + } +``` + +简单执行器中的其它数据库执行方法与 `doQuery` 方法实现类似。 + +## 复用执行器 + +`ReuseExecutor` 相对于 `SimpleExecutor` 实现了对 `statment` 对象的复用,其在本地维护了 `statementMap` 用于保存 `sql` 语句和 `statement` 对象的关系。当调用 `prepareStatement` 方法获取 `statement` 对象时首先会查找本地是否有对应的 `statement` 对象,如果有则进行复用,负责重新创建并将 `statement` 对象放入本地缓存。 + +```java + /** + * 创建 statement 对象 + * + * @param handler + * @param statementLog + * @return + * @throws SQLException + */ + private Statement prepareStatement(StatementHandler handler, Log statementLog) throws SQLException { + Statement stmt; + BoundSql boundSql = handler.getBoundSql(); + String sql = boundSql.getSql(); + if (hasStatementFor(sql)) { + // 如果本地容器中包含当前 sql 对应的 statement 对象,进行复用 + stmt = getStatement(sql); + applyTransactionTimeout(stmt); + } else { + Connection connection = getConnection(statementLog); + stmt = handler.prepare(connection, transaction.getTimeout()); + putStatement(sql, stmt); + } + handler.parameterize(stmt); + return stmt; + } + + private boolean hasStatementFor(String sql) { + try { + return statementMap.keySet().contains(sql) && !statementMap.get(sql).getConnection().isClosed(); + } catch (SQLException e) { + return false; + } + } + + private Statement getStatement(String s) { + return statementMap.get(s); + } + + private void putStatement(String sql, Statement stmt) { + statementMap.put(sql, stmt); + } +``` + +提交或回滚会导致执行器调用 `doFlushStatements` 方法,复用执行器会因此批量关闭本地的 `statement` 对象。 + +```java + /** + * 批量关闭 statement 对象 + * + * @param isRollback + * @return + */ + @Override + public List doFlushStatements(boolean isRollback) { + for (Statement stmt : statementMap.values()) { + closeStatement(stmt); + } + statementMap.clear(); + return Collections.emptyList(); + } +``` + +## 批量执行器 + +`BatchExecutor` 相对于 `SimpleExecutor` ,其 `update` 操作是批量执行的。 + +```java + @Override + public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException { + final Configuration configuration = ms.getConfiguration(); + final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null); + final BoundSql boundSql = handler.getBoundSql(); + final String sql = boundSql.getSql(); + final Statement stmt; + if (sql.equals(currentSql) && ms.equals(currentStatement)) { + // 如果当前 sql 与上次传入 sql 相同且为相同的 MappedStatement,复用 statement 对象 + int last = statementList.size() - 1; + // 获取最后一个 statement 对象 + stmt = statementList.get(last); + // 设置超时时间 + applyTransactionTimeout(stmt); + // 设置参数 + handler.parameterize(stmt);//fix Issues 322 + // 获取批量执行结果对象 + BatchResult batchResult = batchResultList.get(last); + batchResult.addParameterObject(parameterObject); + } else { + // 创建新的 statement 对象 + Connection connection = getConnection(ms.getStatementLog()); + stmt = handler.prepare(connection, transaction.getTimeout()); + handler.parameterize(stmt); //fix Issues 322 + currentSql = sql; + currentStatement = ms; + statementList.add(stmt); + batchResultList.add(new BatchResult(ms, sql, parameterObject)); + } + // 执行 JDBC 批量添加 sql 语句操作 + handler.batch(stmt); + return BATCH_UPDATE_RETURN_VALUE; + } + + /** + * 批量执行 sql + * + * @param isRollback + * @return + * @throws SQLException + */ + @Override + public List doFlushStatements(boolean isRollback) throws SQLException { + try { + // 批量执行结果 + List results = new ArrayList<>(); + if (isRollback) { + return Collections.emptyList(); + } + for (int i = 0, n = statementList.size(); i < n; i++) { + Statement stmt = statementList.get(i); + applyTransactionTimeout(stmt); + BatchResult batchResult = batchResultList.get(i); + try { + // 设置执行影响行数 + batchResult.setUpdateCounts(stmt.executeBatch()); + MappedStatement ms = batchResult.getMappedStatement(); + List parameterObjects = batchResult.getParameterObjects(); + KeyGenerator keyGenerator = ms.getKeyGenerator(); + if (Jdbc3KeyGenerator.class.equals(keyGenerator.getClass())) { + Jdbc3KeyGenerator jdbc3KeyGenerator = (Jdbc3KeyGenerator) keyGenerator; + // 设置数据库生成的主键 + jdbc3KeyGenerator.processBatch(ms, stmt, parameterObjects); + } else if (!NoKeyGenerator.class.equals(keyGenerator.getClass())) { //issue #141 + for (Object parameter : parameterObjects) { + keyGenerator.processAfter(this, ms, stmt, parameter); + } + } + // Close statement to close cursor #1109 + closeStatement(stmt); + } catch (BatchUpdateException e) { + StringBuilder message = new StringBuilder(); + message.append(batchResult.getMappedStatement().getId()) + .append(" (batch index #") + .append(i + 1) + .append(")") + .append(" failed."); + if (i > 0) { + message.append(" ") + .append(i) + .append(" prior sub executor(s) completed successfully, but will be rolled back."); + } + throw new BatchExecutorException(message.toString(), e, results, batchResult); + } + results.add(batchResult); + } + return results; + } finally { + for (Statement stmt : statementList) { + closeStatement(stmt); + } + currentSql = null; + statementList.clear(); + batchResultList.clear(); + } + } +``` + +执行器提交或回滚事务时会调用 `doFlushStatements`,从而批量执行提交的 `sql` 语句并最终批量关闭 `statement` 对象。 + +## 缓存执行器与二级缓存 + +`CachingExecutor` 对基础执行器进行了装饰,其作用就是为查询提供二级缓存。所谓的二级缓存是由 `CachingExecutor` 维护的,相对默认内置的一级缓存而言的缓存。二者区别如下: + +- 一级缓存由基础执行器维护,且不可关闭。二级缓存的配置是开发者可干预的,在 `xml` 文件或注解中针对 `namespace` 的缓存配置就是二级缓存配置。 +- 一级缓存在执行器中维护,即不同 `sql` 会话不能共享一级缓存。二级缓存则是根据 `namespace` 维护,不同 `sql` 会话是可以共享二级缓存的。 + +`CachingExecutor` 中的方法大多是通过直接调用其代理的执行器来实现的,而查询操作则会先查询二级缓存。 + +```java + /** + * 缓存事务管理器 + */ + private final TransactionalCacheManager tcm = new TransactionalCacheManager(); + + @Override + public List query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) + throws SQLException { + // 查询二级缓存配置 + Cache cache = ms.getCache(); + if (cache != null) { + flushCacheIfRequired(ms); + if (ms.isUseCache() && resultHandler == null) { + // 当前 statement 配置使用二级缓存 + ensureNoOutParams(ms, boundSql); + @SuppressWarnings("unchecked") + List list = (List) tcm.getObject(cache, key); + if (list == null) { + // 二级缓存中没用数据,调用代理执行器 + list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); + // 将查询结果放入二级缓存 + tcm.putObject(cache, key, list); // issue #578 and #116 + } + return list; + } + } + // 无二级缓存配置,调用代理执行器获取结果 + return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql); + } + + private void flushCacheIfRequired(MappedStatement ms) { + Cache cache = ms.getCache(); + if (cache != null && ms.isFlushCacheRequired()) { + // 存在 namespace 对应的缓存配置,且当前 statement 配置了刷新缓存,执行清空缓存操作 + // 非 select 语句配置了默认刷新 + tcm.clear(cache); + } + } +``` + +如果对应的 `statement` 的二级缓存配置有效,则会先通过缓存事务管理器 `TransactionalCacheManager` 查询二级缓存,如果没有命中则查询一级缓存,仍没有命中才会执行数据库查询。 + +### 缓存事务管理器 + +缓存执行器对二级缓存的维护是基于缓存事务管理器 `TransactionalCacheManager` 的,其内部维护了一个 `Map` 容器,用于保存 `namespace` 缓存配置与事务缓存对象的映射关系。 + +```java +public class TransactionalCacheManager { + + /** + * 缓存配置 - 缓存事务对象 + */ + private final Map transactionalCaches = new HashMap<>(); + + /** + * 清除缓存 + * + * @param cache + */ + public void clear(Cache cache) { + getTransactionalCache(cache).clear(); + } + + /** + * 获取缓存 + */ + public Object getObject(Cache cache, CacheKey key) { + return getTransactionalCache(cache).getObject(key); + } + + /** + * 写缓存 + */ + public void putObject(Cache cache, CacheKey key, Object value) { + getTransactionalCache(cache).putObject(key, value); + } + + /** + * 缓存提交 + */ + public void commit() { + for (TransactionalCache txCache : transactionalCaches.values()) { + txCache.commit(); + } + } + + /** + * 缓存回滚 + */ + public void rollback() { + for (TransactionalCache txCache : transactionalCaches.values()) { + txCache.rollback(); + } + } + + /** + * 获取或新建事务缓存对象 + */ + private TransactionalCache getTransactionalCache(Cache cache) { + return transactionalCaches.computeIfAbsent(cache, TransactionalCache::new); + } + +} +``` + +缓存配置映射的事务缓存对象就是前文中提到过的事务缓存装饰器 `TransactionalCache`。`getTransactionalCache` 会从维护容器中查找对应的事务缓存对象,如果找不到就创建一个事务缓存对象,即通过事务缓存对象装饰当前缓存配置。 + +查询缓存时,如果缓存未命中,则将对应的 `key` 放入未命中队列,执行数据库查询完毕后写缓存时并不是立刻写到缓存配置的本地容器中,而是暂时放入待提交队列中,当触发事务提交时才将提交队列中的缓存数据写到缓存配置中。如果发生回滚,则提交队列中的数据会被清空,从而保证了数据的一致性。 + +```java + @Override + public Object getObject(Object key) { + // issue #116 + Object object = delegate.getObject(key); + if (object == null) { + // 放入未命中缓存的 key 的队列 + entriesMissedInCache.add(key); + } + // issue #146 + if (clearOnCommit) { + return null; + } else { + return object; + } + } + + @Override + public void putObject(Object key, Object object) { + // 缓存先写入待提交容器 + entriesToAddOnCommit.put(key, object); + } + + /** + * 事务提交 + */ + public void commit() { + if (clearOnCommit) { + delegate.clear(); + } + // 提交缓存 + flushPendingEntries(); + reset(); + } + + /** + * 事务回滚 + */ + public void rollback() { + unlockMissedEntries(); + reset(); + } + + /** + * 事务提交,提交待提交的缓存。 + */ + private void flushPendingEntries() { + for (Map.Entry entry : entriesToAddOnCommit.entrySet()) { + delegate.putObject(entry.getKey(), entry.getValue()); + } + for (Object entry : entriesMissedInCache) { + if (!entriesToAddOnCommit.containsKey(entry)) { + delegate.putObject(entry, null); + } + } + } + + /** + * 事务回滚,清理未命中缓存。 + */ + private void unlockMissedEntries() { + for (Object entry : entriesMissedInCache) { + try { + delegate.removeObject(entry); + } catch (Exception e) { + log.warn("Unexpected exception while notifiying a rollback to the cache adapter." + + "Consider upgrading your cache adapter to the latest version. Cause: " + e); + } + } + } +``` + +### 二级缓存与一级缓存的互斥性 + +使用二级缓存要求无论是否配置了事务自动提交,在执行完成后, `sql` 会话必须手动提交事务才能触发事务缓存管理器维护缓存到缓存配置中,否则二级缓存无法生效。而缓存执行器在触发事务提交时,不仅会调用事务缓存管理器提交,还会调用代理执行器提交事务: + +```java + @Override + public void commit(boolean required) throws SQLException { + // 代理执行器提交 + delegate.commit(required); + // 事务缓存管理器提交 + tcm.commit(); + } +``` + +代理执行器的事务提交方法继承自 `BaseExecutor`,其 `commit` 方法中调用了 `clearLocalCache` 方法清除本地一级缓存。因此二级缓存和一级缓存的使用是互斥的。 + +```java + @Override + public void commit(boolean required) throws SQLException { + if (closed) { + throw new ExecutorException("Cannot commit, transaction is already closed"); + } + // 清除本地一级缓存 + clearLocalCache(); + flushStatements(); + if (required) { + transaction.commit(); + } + } +``` + +## 小结 + +`MyBatis` 提供若干执行器封装底层 `JDBC` 操作和结果集转换,并嵌入 `sql` 会话维度的一级缓存和 `namespace` 维度的二级缓存。接口层可以通过调用不同类型的执行器来完成 `sql` 相关操作。 + +- `org.apache.ibatis.executor.Executor`:数据库操作执行器抽象接口。 +- `org.apache.ibatis.executor.BaseExecutor`:执行器基础抽象实现。 +- `org.apache.ibatis.executor.SimpleExecutor`:简单类型执行器。 +- `org.apache.ibatis.executor.ReuseExecutor`:`statement` 复用执行器。 +- `org.apache.ibatis.executor.BatchExecutor`:批量执行器。 +- `org.apache.ibatis.executor.CachingExecutor`:二级缓存执行器。 +- `org.apache.ibatis.executor.statement.StatementHandler`:`statement` 处理器抽象接口。 +- `org.apache.ibatis.executor.statement.BaseStatementHandler`:`statement` 处理器基础抽象实现。 +- `org.apache.ibatis.executor.statement.RoutingStatementHandler`:`statement` 处理器路由对象。 +- `org.apache.ibatis.executor.statement.SimpleStatementHandler`:简单 `statement` 处理器。 +- `org.apache.ibatis.executor.statement.PreparedStatementHandler`:预编译 `statement` 处理器。 +- `org.apache.ibatis.executor.statement.CallableStatementHandler`:存储过程 `statement` 处理器。 +- `org.apache.ibatis.cache.TransactionalCacheManager`:缓存事务管理器。 + diff --git a/docs/source/spring-mvc/1-overview.md b/docs/source/spring-mvc/1-overview.md new file mode 100644 index 0000000..5dc737d --- /dev/null +++ b/docs/source/spring-mvc/1-overview.md @@ -0,0 +1,1444 @@ +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,MVC模式,Spring MVC和Struts,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器,REST + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 1. 先从DispatcherServlet说起 + +有Servlet基础的同学应该都知道,前端的每一个请求都会由一个Servlet来处理。在最原始的Java Web开发中,我们需要写自己的Servlet,然后再标注上这个Servlet是处理什么URL路径的,这样当一个HTTP请求到来时,就可以根据请求的路径找到我们的Servlet,再调用这个Servlet来处理请求。 + +在SpringMVC的开发中,我们需要配一个特殊的Servlet叫`DispatcherServlet`(这个东西是Spring帮我们写好的),然后我们再为`DispatcherServlet`配置映射的URL路径为`/`。其中`/`是个通配符,代表它能处理所有URL的请求。这代表所有的HTTP请求都会打到了`DispatcherServlet`上,那么此时我们的开发就由: + +![](http://img.topjavaer.cn/img/202311230846421.png) + +转为了: + +![](http://img.topjavaer.cn/img/202311230846478.png) + + + +其中上图的**后端处理**就是我们自己写的Controller里的每个方法。 + +这样就有了一个问题,**为什么要那么做?为什么要将所有的请求都打到一个Servlet上再由这个Servlet分发?** + +原因很简单,**就是Spring想掌控一切**。Spring想拦截所有的HTTP请求并对每个请求做一定的处理,**让我们能够尽量少的关心HTTP请求和响应,尽可能多的关心业务**。 + +比如,我们往往会写这样一个创建用户的请求: + +```java +@RestController +public class UserController{ + @PostMapping("/user") + public User createUser(@RequestBody User user){ + //... + } +} +``` + +上述Controller中,我们的参数User对象是如何从HTTP请求中解析到的,又是如何将返回给前端的数据写进HTTP响应的,这些我们都不需要关心,这些其实都是`DispatcherServlet`这个类帮我们做了。 + +有了上面这些思想后,我们来看下`DispatcherServlet`类的继承结构: + +![](http://img.topjavaer.cn/img/202311230847898.png) + + + +可以看到`DispatcherServlet`是一个Servlet实现类。而我们又知道处理Http请求的Servlet必须是`HttpServlet`,`HttpServlet`内规定了`doGet()、doPost()、doPut()`等方法,其中`FrameworkServlet`类(上图的倒数第二个类)重写了这些方法,它对这些方法的重写都一模一样: + +```java +public abstract class FrameworkServlet extends HttpServletBean implements ApplicationContextAware { + //... + @Override + protected final void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + @Override + protected final void doPost(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + @Override + protected final void doPut(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + + processRequest(request, response); + } + //... +} +``` + +都是调用`processRequest()`方法,而`processRequest()`方法又会调用`doService()`方法。`doService()`是`FrameworkServlet`的抽象方法,这个方法由`DispatcherServlet`实现,`DispatcherServlet#doService()`又调用了`DispatcherServlet#doDispatch()`方法。其时序图如下: + +![image-20220807223638836](http://img.topjavaer.cn/img/202311230846720.png) + + + +其中,**`DispatcherServlet#doDispatch()`**是最核心的方法,**对于SpringMVC处理请求的源码分析往往也是对`DispatcherServlet#doDispatch()`的分析**。 + +## 2. doDispatch做了什么 + +简单来讲,`doDispatch()`做了四件事: + +1. 根据HTTP请求的信息找到我们的处理方法(包括方法的适配器,我们一会说) +2. 使用适配器,根据HTTP请求信息和我们的处理方法,解析出请求参数,并执行我们的处理方法。 +3. 将处理方法的返回结果进行处理 +4. 视图解析 + +其中我们的处理方法就是Controller层里面每一个标了`@RequestMapping`(`@GettingMapping`、`@PostMapping`等也属于`@RequestMapping`)注解的方法。 + +`doDispatch()`源码中这四步流程的具体执行为: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + //对应步骤1,找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //对应步骤1,找到处理方法的适配器(适配器我们下面会说,别着急) + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //对应步骤2和步骤3,解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //对应步骤4 视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +我们将`doDispatch()`中比较重要的源码拿了出来,这样看起来更加清晰明了,这些重要的源码就分别对应我们上面说的4个步骤。下面我们会一一说一下每个步骤SpringMVC都做了什么。 + +## 3. 第一步 找到处理方法 + +要想处理请求,必然要先找到能处理这个请求的方法。SpringMVC分为两步来寻找,**第一步是找到我们的Controller里自己写的那个方法,称作handler(处理器)。第二步是通过这个handler找到对应的适配器,称作handlerAdapter。** + +### 3.1 找到handler + +#### 3.1.1 HandlerMapping + +先介绍一个接口`HandlerMapping`,它翻译过来是请求映射处理器。我们可以这样理解: + +对于后端来讲,我们把HTTP请求分为几种:比如请求静态资源的,请求动态处理的,请求欢迎页的等。每一种类型的HTTP请求都需要一个专门的处理器来处理,这些处理器就是`HandlerMapping`。换句话说一个`HandlerMapping`实现类就是一个场景下的HTTP请求处理器。 + +`DispatcherServlet`类内部有一个属性`handlerMappings`,这个属性里面装了一些`HandlerMapping`的实现类: + +```java +public class DispatcherServlet extends FrameworkServlet{ + //... + private List handlerMappings; + //... +} +``` + +默认情况下`handlerMappings`里有5个实现类,分别是`RequestMappingHandlerMapping`、`BeanNameUrlHandlerMapping`、`RouterFunctionMapping`、`SimpleUrlHandlerMapping`和`WelcomePageHandlerMapping`。 + +![image-20220808000313545](http://img.topjavaer.cn/img/202311230846759.png) + + + +看名字大概也可以猜到这些`HandlerMapping`的应用场景,比如`SimpleUrlHandlerMapping`是用来处理静态页面请求的,`WelcomePageHandlerMapping`是用来处理欢迎页请求的。而**对于动态请求的处理,也即执行我们Controller里的方法(handler)的请求,是由`RequestMappingHandlerMapping`实现的**。 + +#### 3.1.2 DispatcherServlet#getHandler() + +了解了这些后,我们再来看SpringMVC是如何找到我们的hander的,根据上面的源码我们知道首先会执行`getHandler()`方法,`getHandler()`就是遍历`DispatcherServlet`类内`handlerMappings`的所有`HandlerMapping`,挨个调用它们的`getHandler()`方法,谁先有返回就代表找到了(这里返回的是`HandlerExecutionChain`,而非`HandlerMapping`,主要原因是`HandlerExecutionChain`封装了一些拦截器,我们讲到拦截器时再说,大家可以简单认为`HandlerExecutionChain`和`HandlerMapping`是一样的)。 + +```java + protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + if (this.handlerMappings != null) { + //遍历handlerMappings里的每个HandlerMapping实现类,判断哪个HandlerMapping能处理这个HTTP请求 + for (HandlerMapping mapping : this.handlerMappings) { + //一旦HandlerMapping的getHandler()有返回,就代表这个找到了能处理的handler + HandlerExecutionChain handler = mapping.getHandler(request); + if (handler != null) { + return handler; + } + } + } + return null; + } +``` + +我们之前说了对于HTTP的动态请求的是由`RequestMappingHandlerMapping`来处理的,那也就是`RequestMappingHandlerMapping#getHandler()`有返回,那么我们看下它是怎么处理的。 + +#### 3.1.3 RequestMappingHandlerMapping#getHandler() + +MappingRegistry与HandlerMethod + +这里又需要插入一个知识,我们要先讲一个叫做`MappingRegistry`的东西。`MappingRegistry`翻译过来叫做映射器注册中心。也即所有的映射器都需要注册到这里,它就像是一个大集合,装了所有的映射器。那什么是映射器呢?就是我们的handler,也即我们自己写的Controller里的方法。也就是说在项目启动的时候,Spring会扫描我们这个项目下所有标注了`@Controller`注解的类,然后再扫描这些类里面标了`@RequestMapping`(`@GettingMapping`、`@PostMapping`等也属于`@RequestMapping`)注解的方法,并将这些方法封装注册到`MappingRegistry`中。**Spring将我们这些方法统一封装成`HandlerMethod`对象。**`HandlerMethod`内的属性挺丰富的,比如: + +我们之前说了`MappingRegistry`是一个大的集合,这个集合装了所有Controller类内标了`@RequestMapping`的方法。因此我们必然能根据HTTP请求从`MappingRegistry`中找到我们的处理方法。`MappingRegistry`内部有两个非常重要的Map + +```java +class MappingRegistry { + + private final Map> registry = new HashMap<>(); + + private final MultiValueMap pathLookup = new LinkedMultiValueMap<>(); + //... +} +``` + +首先`MappingRegistration`是一个包装类,它包装了我们上面说的`HandlerMethod`,部分信息如下: + +```java +static class MappingRegistration { + + private final T mapping; + //可以看到包装了我们的HandlerMethod + private final HandlerMethod handlerMethod; + + //... +} +``` + +`pathLookup`这个Map,key是URL路径,而value是能处理这个URL路径的方法描述信息(注意是方法描述信息,不是方法本身)。这个Map比较特殊的是一个`MultiValueMap`,也即**可以根据一个key得到多个value**。这个很正常,比如: + +```java +@RestController +public class UserController { + @GetMapping("/user") + public Object getUser(){ + //... + } + + @PostMapping("/user") + public User postUser(@RequestBody User user){ + //... + } + + @DeleteMapping("/user") + public String deleteUser(){ + //... + } +} +``` + +我们一个URL路径`/user`可以对应上述Controller里的三个方法。 + +而`registry`这个Map的key是方法描述信息 (就是pathLookup的value),`registry`的value是`MappingRegistration`,也即封装了具体的执行方法。 + +AbstractHandlerMethodMapping#getHandlerInternal() + +了解了`MappingRegistry`后,我们再回来看`RequestMappingHandlerMapping`,`RequestMappingHandlerMapping`类的父类是`AbstractHandlerMethodMapping`,这个类内持有`MappingRegistry`实例: + +```java +public abstract class AbstractHandlerMethodMapping extends AbstractHandlerMapping implements InitializingBean { + //... + private final MappingRegistry mappingRegistry = new MappingRegistry(); + //... +} +``` + +也即我们刚才分析的`MappingRegistry`会被`AbstractHandlerMethodMapping`持有,那也就代表**我们的`RequestMappingHandlerMapping`这个类拥有项目所有的`HandlerMethod`**。 + +对于`RequestMappingHandlerMapping#getHandler()`的调用最终会调用到`AbstractHandlerMethodMapping#getHandlerInternal()`上,其关键源码如下: + +```java +protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception { + //先根据HTTP请求得到请求URL路径,比如/user + String lookupPath = initLookupPath(request); + //读写锁,获得读锁,暂时可以不必关心 + this.mappingRegistry.acquireReadLock(); + try { + //根据URL路径和HTTP请求信息从自己的属性MappingRegistry中拿到匹配的HandlerMethod + HandlerMethod handlerMethod = lookupHandlerMethod(lookupPath, request); + //找到后将匹配的HandlerMethod返回 + return (handlerMethod != null ? handlerMethod.createWithResolvedBean() : null); + } + finally { + //释放读锁 + this.mappingRegistry.releaseReadLock(); + } +} +``` + +可以看到上述信息核心的地方在`lookupHandlerMethod()`,其部分关键源码如下: + +```java +protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception { + //match是符合的,匹配到的HandlerMethod,一会我们找到的HandlerMethod会放到这个里面 + List matches = new ArrayList<>(); + //根据URL路径先找匹配的方法信息(注意是方法信息,不是方法本身) + //我们上面说过,根据URL路径寻找可能会找到多个符合的方法,比如GET/user POST/user DELETE/user + List directPathMatches = this.mappingRegistry.getMappingsByDirectPath(lookupPath); + if (directPathMatches != null) { + //再根据HTTP请求的其他信息(比如请求类型(GET,POST等)),进一步寻找符合的处理方法 + //并将找到的方法放入matches里面 + addMatchingMappings(directPathMatches, matches, request); + } + //下面就是一些异常判断了 + //比如如果matchers是空的话,也即没找到能处理的方法要怎么办啦 + //如果找到不只一个能处理的方法就抛出异常啦之类的 + //正常情况下我们只会找到一个能处理的方法,将这个方法返回即可 + //... +} +``` + +这里很容易想到`this.mappingRegistry.getMappingsByDirectPath(lookupPath)`其实就是从`MappingRegistry`中的`pathLookup`这个Map里面去取信息(还记得吗,我们刚说过`MappingRegistry`内部的两个重要map)。 + +根据URL路径匹配完以后,肯定还得结合HTTP请求,再详细的判断这个方法是否满足需求(比如我要的是GET,你的方法是POST,那肯定就不行)做一层筛选,这样筛选下来得到的方法描述信息,再作为key值,从`MappingRegistry`中的`registry`这个Map里面去取具体的方法。而这个就是`addMatchingMappings()`方法的实现细节。感兴趣的可以自己点进去这两个方法细节,实际很简单,就几行代码,这里不再贴出。 + +#### 3.1.4 总结 + +走完上面的流程,我们已经拿到了能处理请求的`HandlerMethod`,我们可以总结: + +首先`DispatcherServlet`内部有5个`HandlerMapping`,它会挨个询问哪个`HandlerMapping`能处理这个请求,结果是我们的`RequestMappingHandlerMapping`能处理。`RequestMappingHandlerMapping`这个对象的内部有一个叫`MappingRegistry`的大集合,这个集合里面装了我们项目所有的自己编写的Controller里处理请求的方法,它将我们的方法封装为`HandlerMethod`,当`RequestMappingHandlerMapping`去处理的时候实际上就是根据HTTP 信息(比如请求类型和URL路径)去`MappingRegistry`里面找符合的`HandlerMethod`,将找到的`HandlerMethod`返回。 + +流程大致如下图: + +![image-20220808171735041](http://img.topjavaer.cn/img/202311230846114.png) + + + +#### 3.1.5 一些补充 + +handlerMappings内的元素是有顺序的 + +我们上面看到`DispatcherServlet`里的`handlerMappings`有5个实现类,都放在一个List里面,它们其实是有顺序的(父类`AbstractHandlerMapping`继承接口`Ordered`),`RequestMappingHandlerMapping`在最前面,这有什么影响呢?举个例子: + +假设我的SpringBoot项目现在有一个静态文件叫hello.html + +![](http://img.topjavaer.cn/img/202311230846876.png) + +默认情况下,我通过访问`localhost:8080/hello.html`,SpringBoot就会将这个静态文件返回给浏览器。但如果这个时候我有个Controller,它是这样写的 + +```java +@RestController +public class HelloController { + @GetMapping("/hello.html") + public String hello(){ + return "hello world"; + } + +} +``` + +那么请问此时再访问`localhost:8080/hello.html`还会返回静态文件吗,还是会走到我们的Controller返回`"hello world"`字符串呢? + +答案是会走进Controller返回`"hello world"`字符串。原因也很简单,因为`RequestMappingHandlerMapping`在`SimpleUrlHandlerMapping`前面,`DispatcherServlet`会先问`RequestMappingHandlerMapping`能不能处理。在我们没写这个Controller时,`RequestMappingHandlerMapping`肯定是没法从`MappingRegistry`找到匹配的处理方法的,所以就处理不了,这时才会轮到后面的`SimpleUrlHandlerMapping`来处理,然后返回静态页面。但当我们写了这个Controller,自然`RequestMappingHandlerMapping`能找到处理方法,也就没后面`SimpleUrlHandlerMapping`什么事了。 + +MappingRegistry里的读写锁 + +刚才我们在源码里看到了`MappingRegistry`的读写锁,这个原因也挺简单的,首先`MappingRegistry`是一个注册中心,自然就允许有人往里注册或注销东西,比如MappingRegistry的部分功能如下: + +```java +class MappingRegistry { + public void register(T mapping, Object handler, Method method) { + //... + } + public void unregister(T mapping) { + //... + } +} +``` + +所谓注册和注销,本质上就是在操作`MappingRegistry`里面的那些装了我们方法的集合。同时`MappingRegistry`又会被大量的查询(每次HTTP请求来了以后都会向它查询能处理的方法),因此这是一个典型的**读多写少**的场景,为保证并发安全且尽可能的提高性能,读写锁就是很好的选择。读锁之间共享,写锁独占,同时写的时候不能读。 + +从`MappingRegistry`的API,我们也可以大胆猜测,在项目运行期间,我们也可以向其注册和删除`HandlerMethod`。 + +### 3.2 找到handlerAdapter + +#### 3.2.1 HandlerAdapter + +在说寻找handlerAdapter之前,我们先来说下为什么需要handlerAdapter。 + +走到这一步的时候,想想我们拿到了什么?拿到了能处理请求的`HandlerMethod`,以及拥有HTTP请求的所有信息和HTTP响应。那正常来说就是根据HTTP请求的信息,调用我们的`HandlerMethod`来处理请求,处理完后将处理结果写进HTTP响应。但是我们知道HTTP请求是五花八门的,比如参数放在请求头或请求体,以form形式提交,以JSON格式提交等等。同时我们自己的Controller层函数写法也是各种各样,加`@RequestParam`注解的,加Cookie、Session信息的,想返回页面的,想返回数据的等等。 + +我们将HTTP请求类比为插头,`HandlerMethod`类比为插座,为了让这些插头和插座能够结合,就需要一个适配器,插头插在适配器上,适配器插在插座上,这样任何类型的请求,任何不同的处理方法,都会有一个适配器来处理。 + +**Spring将这种适配器称作`HandlerAdapter`。** + +与`handlerMappings`相同,`DispatcherServlet`内部也有一个属性叫`handlerAdapters`,这个属性里面也装了一些`HandlerAdapter`的实现类: + +```java +public class DispatcherServlet extends FrameworkServlet{ + //... + private List handlerAdapters; + //... +} +``` + +默认情况下,这个List集合里有4个实现类,分别是`RequestMappingHandlerAdapter`、`HandlerFunctionAdapter`、`HttpRequestHandlerAdapter`和`SimpleControllerHandlerAdapter`。 + +![image-20220804185151491](http://img.topjavaer.cn/img/202311230846149.png) + + + +根据名字可以看出,这些适配器与`HandlerMapping`是有对应关系的。比如`RequestMappingHandlerMapping`与`RequestMappingHandlerAdapter`是对应的,说明`RequestMappingHandlerAdapter`是用于适配`RequestMappingHandlerMapping`映射器的。 + +#### 3.2.2 DispatcherServlet#getHandlerAdapter() + +根据之前的源码,我们知道`getHandlerAdapter()`函数就是用来获得适配器的,它的源码如下: + +```java +protected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { + if (this.handlerAdapters != null) { + for (HandlerAdapter adapter : this.handlerAdapters) { + if (adapter.supports(handler)) { + return adapter; + } + } + } + throw new ServletException("No adapter for handler [" + handler + + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler"); +} +``` + +可以看到,十分简单,就是挨个问`handlerAdapters`里的每一个`HandlerAdapter`,谁支持适配这个请求处理,谁支持就返回谁。 + +我们刚才知道`RequestMappingHandlerMapping`的适配器是`RequestMappingHandlerAdapter`,那也就代表,这里正常会返回`RequestMappingHandlerAdapter`,我们不妨走进`RequestMappingHandlerAdapter#support()`,看看它是如何判断的。 + +#### 3.2.3 RequestMappingHandlerAdapter#support() + +`RequestMappingHandlerAdapter#support()`会进入`AbstractHandlerMethodAdapter#supports()`,其中`AbstractHandlerMethodAdapter`是`RequestMappingHandlerAdapter`的父类,其源码如下: + +```java +public abstract class AbstractHandlerMethodAdapter extends WebContentGenerator implements HandlerAdapter, Ordered { + //... + public final boolean supports(Object handler) { + return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler)); + } + //... +} +``` + +`supportsInternal()`由子类`RequestMappingHandlerAdapter`实现: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + //... + @Override + protected boolean supportsInternal(HandlerMethod handlerMethod) { + return true; + } + //... +} +``` + +可以看到,`RequestMappingHandlerAdapter`的判断逻辑很简单,就是问找到的handler是不是`HandlerMethod`类型,通过前面的源码分析,我们很清楚handler就是`HandlerMethod`。 + +至此我们就找到了我们的`HandlerMethod`和`HandlerAdapter`,完成了`DispatcherServlet#doDispatch()`的第一步:根据HTTP请求的信息找到我们的处理方法(包括方法的适配器)。 + +#### 3.2.4 总结 + +我们在3.1节中已经找到了`HandlerMethod`,但考虑到HTTP请求信息比较多,如何将HTTP请求的信息与我们的`HandlerMethod`参数信息进行匹配,以及又如何将`HandlerMethod`处理完的结果返回给HTTP响应,这就需要适配器,我们需要找到合适的适配器来进行后续的工作。 + +与`handlerMappings`相同,`DispatcherServlet`内的`handlerAdapters`装了一些`HandlerAdapter`的实现类。处理动态请求会由`RequestMappingHandlerAdapter`适配器来处理,而`RequestMappingHandlerAdapter`判断能处理的逻辑也很简单,就是看上一步得到的处理器是不是`HandlerMethod`。拿到适配器后,我们就可以将HTTP请求信息与我们的`HandlerMethod`进行适配了。 + +## 4. 第二步 解析参数 + +在讲参数解析前,我们先回顾下目前已经走了哪些步骤,得到了哪些东西: + +首先我们根据HTTP请求,从`MappingRegistry`中得到了能处理请求的`HandlerMethod`,其次为将HTTP请求与我们的`HandlerMethod`适配,又根据`HandlerMethod`找到了适配器`RequestMappingHandlerAdapter`,现在我们拥有的就是 + +- 实际的处理器`HandlerMethod` +- 适配器`RequestMappingHandlerAdapter` +- HTTP请求和HTTP响应 + +我们之前说了由于HTTP请求和`HandlerMethod`五花八门,因此需要适配器来帮忙执行HTTP请求。那适配器到底帮了哪些忙呢? + +实际上就两点:**参数解析和返回结果的处理**。适配器会首先根据HTTP请求信息和我们的`HandlerMethod`,解析HTTP请求,将它转为我们`HandlerMethod`上的参数。举个例子,前端通过form表单发送HTTP请求: + +form表单内容如下: + +```json +name=tom&age=18&pet.name=myDog&pet.age=18 +``` + +后端Controller: + +```java +@PostMapping("/user") +public User postUser(User user){ + return user; +} +``` + +SpringMVC会自动将表单中的字符串信息转为我们的User参数对象。 + +得到所有参数后,适配器会直接调用`HandlerMethod`处理请求。这时我们又得到了处理的返回结果。这个结果的意义也是众多的,它可以是一个对象然后用JSON写出,也可以是一个页面,还可以是一个跳转或重定向,不同的返回情况都需要被特殊适配处理,这些东西也是适配器帮我们做的。 + +为了让大家回想起`DispatcherServlet#doDispatch()`的四个步骤,我们再将源码贴过来: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + //对应步骤1,找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //对应步骤1,找到处理方法的适配器(适配器我们下面会说,别着急) + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //对应步骤2和步骤3,解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //对应步骤4 视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +其中`mv = ha.handle(processedRequest, response, mappedHandler.getHandler());`就是适配器做的工作:**参数解析,执行方法并处理执行返回**。 + +在本章中我们先学习参数解析,下一章讲返回结果的处理。 + +### 4.1 先从一个设计模式说起 + +为便于理解参数解析的设计思想,我们先从一个设计模式讲起,理解这个设计模式对于SpringMVC的学习很关键。 + +我们假设现在你是一个包工头,你的手底下有很多技术工,有能刷墙的,能砌砖的,能设计图纸的还有能开挖掘机的。很多房地产开发商有活的时候会找你。比如今天恒太房地产跟你说他现在手里有一个刷墙的活问你能不能干。你二话不说接了这个活并派出了刷墙技术工来干活。 + +针对上面这个场景,我们可以抽象为代码,首先我们有个工人接口。刷墙工,砌砖工等都属于这个接口的实现类。 + +```java +public interface Worker{} +``` + +每个工人首先需要明确自己自己能干哪些活,比如刷墙工只能干刷墙的活,他干不了砌砖。怎么明确呢,只能问。比如现在来了一个活,我得问下刷墙工能不能干。因此就需要一个`support(Object work)`方法,表明当前工人能不能干这个活。同时如果能干,还得有个实际干活的功能,因此我们的`Worker`接口如下: + +```java +public interface Worker{ + /** + * 判断当前工人是否能干这个活 + */ + boolean support(Object work); + /** + * 如果能干就实际的干 + */ + void doWork(Object work); +} +``` + +由于你是个包工头,因此你手里应该有一批工人,我们可以使用类`Boss`描述包工头: + +```java +public class Boss{ + List workerList; +} +``` + +`workerList`就表示你手里拥有的众多工人。 + +现在,某客户有活过来了,你得判断谁能接这个活,然后交给他干。比较简单的办法就是挨个问手底下的工人,谁能干就让谁干。 + +```java +public class Boss{ + List workerList; + + //遍历询问谁能干,谁能干就让谁干 + public void doWork(Object work){ + for(Worker worker : workerList){ + if(worker.support(work)){ + worker.doWork(work); + return; + } + } + //都不能干就抛出异常 + throw new RuntimeException("抱歉,我们这接不了这种活"); + } +} +``` + +这样,其实我们就完成了一个设计模式,叫**策略模式**。 + +什么是策略模式?**我们可以将上面的工人都理解为某种策略,当有任务来的时候,就需要选择一个正确的策略来处理这个任务。我们将策略抽象为接口(或抽象类),然后自己持有这个接口(或抽象类)的集合**。类似于: + +```java +//A接口是策略 +public interface A{} +public class B{ + //B对象持有A的实现类集合 + List aList; +} +``` + +后面源码的分析中,**策略模式会使用的非常频繁**。其实如果你细心观察会发现之前的`DispatcherServlet`里的`handlerMappings`和`handlerAdapters`也是策略模式,每个`HandlerMapping`或`HandlerAdapter`的实现类都是一种策略,然后我们的`DispatcherServlet`持有这些策略,当有HTTP请求到来时,就遍历这些策略判断哪个能处理,找到那个能处理的策略来处理任务。 + +### 4.2 参数解析 + +了解了策略模式,我们来看下参数解析的源码。 + +``` +ha.handle()`的具体执行会走到`RequestMappingHandlerAdapter#handleInternal()`,而`handleInternal()`又会调用`RequestMappingHandlerAdapter#invokeHandlerMethod() +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + @Override + protected ModelAndView handleInternal(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + + ModelAndView mav; + //... + //一些校验和特殊情况的判断,比如加session锁 + //最终会执行invokeHandlerMethod + mav = invokeHandlerMethod(request, response, handlerMethod); + //... + return mav; + } +} +``` + +因此我们先来分析下`RequestMappingHandlerAdapter#invokeHandlerMethod()` + +#### 4.2.1 RequestMappingHandlerAdapter#invokeHandlerMethod() + +HandlerMethodArgumentResolver + +本着不大篇幅的贴源码,尽量深入浅出的思想,在讲源码前我们先来说点别的。首先从一个接口`HandlerMethodArgumentResolver`说起。 + +我们都知道SpringMVC会根据HTTP请求和我们写的`HandlerMethod`来解析参数。但是之前也说过,解析参数的情况太多了,比如用`@RequestParam`从请求头中解析,使用`@RequestBody`从请求体中解析等。面对如此多的情况,我们不妨使用刚刚学习的策略模式,**将每种情况的处理都认为是一个策略,然后使用一个List将这些策略汇总起来。那么当要解析一个参数的时候,就从集合中取出一个合适的策略来解析这个参数**。SpringMVC就是那么做的。其中对于解析参数的这一策略接口叫做**`HandlerMethodArgumentResolver`**,我们称为参数解析器。接口的源码为: + +```java +public interface HandlerMethodArgumentResolver { + + boolean supportsParameter(MethodParameter parameter); + + @Nullable + Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception; + +} +``` + +可以看到一个`supportsParameter()`一个`resolveArgument()`,和我们之前讲的包工头例子一模一样。那么自然而言我们需要一个集合来保存`HandlerMethodArgumentResolver`接口的实现类。`RequestMappingHandlerAdapter`内有一个`argumentResolvers`属性: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + //... + private HandlerMethodArgumentResolverComposite argumentResolvers; + //... +} +``` + +**其中`HandlerMethodArgumentResolverComposite`就是`HandlerMethodArgumentResolver`的集合**,其内部属性如下: + +```java +public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { + + private final List argumentResolvers = new ArrayList<>(); + //... +} +``` + +默认情况下`argumentResolvers`内包含27个实现类,内容如下: + +![image-20220819160536958](http://img.topjavaer.cn/img/202311230845819.png) + + + +ModelAndView + +我们再来说第二个重要的类`ModelAndViewContainer`,这个类的作用类似于上下文,它保存着整个请求处理过程中的**Model**和**View**。 + +首先根据MVC思想我们知道,对于一个MVC项目,我们可以给请求响应两种东西:数据和页面。除此以外,在转发或重定向时,往往需要将我们的数据也转发或重定向到下一级请求(转发是同一个HTTP请求,这里只是表述方便)。这时就需要一种数据结构,用来装我们返回的结果,这个结果可以是返回给请求的,也可以是转发到下一级请求处理的(转发和重定向),这就是`ModelAndView`。因此我们可以理解为`ModelAndView`就是用来装一次HTTP处理结果的容器,这个容器里主要装了Model和View两个信息。 + +其实我们在之前的源码中已经看到过`ModelAndView`了,它实际上贯穿于整个`doDispatch()`方法,我们不妨将之前`DispatcherServelt`的重要源码再拿过来看: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + //这里就是先声明了一个ModelAndView + ModelAndView mv = null; + + mappedHandler = getHandler(processedRequest); + + //... + + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //可以看到适配器的处理结果就是返回一个ModelAndView + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //视图解析,需要从ModelAndView中拿到视图信息 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +`ModelAndViewContainer`可以理解为`ModelAndView`的包装类,这个类在`RequestMappingHandlerAdapter#invokeHandlerMethod()`处理过程中被创建(我们一会儿会看到),主要是装`HandlerMethod`的处理结果(如果参数有Map或者Model还会装参数里的Map和Model信息)。 + +**也即`ModelAndViewContainer`就是适配器在处理一次HTTP请求的上下文信息,这个上下文里面装的是处理过程中的Model和View。** +`ModelAndViewContainer`源码中几个重要的属性信息如下: + +```java +public class ModelAndViewContainer { + //视图信息,可以是一个View,也可能只是一个视图名String + private Object view; + //数据,本质是个Map,我们在Controller内的函数上写的Map或Model其实都是它 + private final ModelMap defaultModel = new BindingAwareModelMap(); + //重定向的数据,也是个Map + private ModelMap redirectModel; +} +``` + +可以看到`ModelAndViewContainer`内部维护了一个View和两个Model,分别是默认的Model和重定向的Model。 + +WebDataBinderFactory + +翻译过来就是数据绑定工厂,我们拿之前表单提交的例子来说: + +form表单内容如下: + +```json +name=tom&age=18&pet.name=myDog&pet.age=18 +``` + +后端Controller: + +```java +@PostMapping("/user") +public User postUser(User user){ + return user; +} +``` + +这里很明显的是,SpringMVC将form提交的每一个参数信息**绑定**到了我们的User对象上,而这个绑定操作就是`WebDataBinderFactory`干的工作。 + +RequestMappingHandlerAdapter#invokeHandlerMethod() + +了解了上面那么多以后,我们就可以看下`RequestMappingHandlerAdapter#invokeHandlerMethod()`,其部分源码如下: + +```java +protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //webRequest是HttpServletRequest和HttpServletResponse的包装类 + ServletWebRequest webRequest = new ServletWebRequest(request, response); + //... + //ServletInvocableHandlerMethod是一个大的包装器,下面的一系列set操作都是对ServletInvocableHandlerMethod的丰富 + 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); + + //创建ModelAndViewContainer + ModelAndViewContainer mavContainer = new ModelAndViewContainer(); + //做一些ModelAndViewContainer的初始化工作 + + //... + + //包装之后的核心执行,包含参数解析,处理器执行和返回结果的处理 + invocableMethod.invokeAndHandle(webRequest, mavContainer); + + //... + + //处理完后返回ModelAndView + return getModelAndView(mavContainer, modelFactory, webRequest); + +} +``` + +`ServletInvocableHandlerMethod`是`HandlerMethod`的包装类(实际上它继承自`HandlerMethod`类),它里面装了很多信息,比如装了我们之前说的参数解析器,装了我们的`handlerMethod`,还装了`WebDataBinderFactory`等。另外还有一个`returnValueHandlers`,它是我们的返回结果处理器,还记得我们之前说的,handlerAdpter实际上帮我们做的事是**参数解析和返回结果的处理**。这里的`returnValueHandlers`就是用于返回结果处理的 + +`ServletWebRequest`也是个包装类,就是将`HttpServletRequest`和`HttpServletResponse`合并到了一个类里。`ModelAndViewContainer`我们在之前已经介绍过了,当都准备好这些信息后,就要开始执行 + +```java +invocableMethod.invokeAndHandle(webRequest, mavContainer); +``` + +因此下一步我们就需要追溯:`ServletInvocableHandlerMethod#invokeAndHandle()`的源码 + +#### 4.2.2 ServletInvocableHandlerMethod#invokeAndHandle() + +其部分源码如下: + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + + //...后面是返回结果的处理,我们在第五章讲 + //... +} +``` + +其中`invokeForRequest()`是参数解析以及执行`HandlerMethod`然后得到返回结果。 + +`invokeForRequest()`源码如下: + +```java +public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //根据HTTP请求解析参数 + Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); + + //...一些日志打印 + //... + //得到参数后,反射执行HandlerMethod + return doInvoke(args); +} +``` + +因此参数解析的核心代码就是`getMethodArgumentValues()`函数 + +#### 4.2.3 getMethodArgumentValues() + +```java +protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //拿到所有的参数信息 + MethodParameter[] parameters = getMethodParameters(); + + //... + //args就是装我们所有的参数,这里先声明出来 + Object[] args = new Object[parameters.length]; + //遍历所有的参数信息 + for (int i = 0; i < parameters.length; i++) { + MethodParameter parameter = parameters[i]; + + //...一些参数处理 + //使用参数解析器来解析参数 + args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); + //... + //一些异常处理 + //... + } + return args; +} +``` + +可以看到参数解析就是**以我们的`HandlerMethod`的参数信息为模板,使用解析器,从HTTP请求中拿到信息赋值到我们的参数上**,这样循环遍历`HandlerMethod`中的每个参数,就可以解析得到所有的参数对象。 + +那么我们就要看下参数解析器到底是如何解析参数的。 + +#### 4.2.4 HandlerMethodArgumentResolverComposite#resolveArgument() + +`HandlerMethodArgumentResolverComposite#resolveArgument()`源码很好理解,就是找到能解析这个参数的参数解析器,然后调用这个找到的解析器解析它。 + +```java +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); +} +``` + +其中获得参数解析器代码为: + +```java +private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + //先从缓存中寻找,找不到再遍历寻找 + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + if (result == null) { + //遍历每一个参数解析器,判断谁能处理这个参数 + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + result = resolver; + //找到能处理的参数后就将它放入缓存,保证下次不用再遍历 + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; +} +``` + +这里需要提一嘴的是`argumentResolverCache`属性,`argumentResolverCache`作用很简单,就是缓存的功能。一开始项目启动的时候缓存里面没有任何东西,这样解析每个参数就都需要遍历所有的参数解析器,判断谁能支持处理这个参数,一旦找到这个参数解析器就需要将它缓存起来,这样下次再请求的时候就可以直接从缓存拿避免再遍历,节省了下次HTTP请求的时间。 + +```java +public class HandlerMethodArgumentResolverComposite implements HandlerMethodArgumentResolver { + //... + private final Map argumentResolverCache = + new ConcurrentHashMap<>(256); + + //... +} +``` + +可以看到`argumentResolverCache`本质就是一个`ConcurrentHashMap`,它的key是`MethodParameter`也即参数信息(可以推测出`MethodParameter`一定重写了`equals()`和`hashCode()`),Value是能处理这个参数的参数解析器。 + +这里我们又看到了策略模式的使用,使用`argumentResolvers`将所有参数解析器汇总起来,在真正需要进行参数解析的时候就会挨个问这些参数解析器有没有人能处理这个参数,谁能处理就交由谁处理。 + +最后的一步就是调用参数解析器解析参数: + +```java +resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); +``` + +首先,参数解析器解析参数需要parameter,mavContainer,webRequest,binderFactory四个参数才能解析,每个参数的作用我们一一来说(虽然我们刚才已经讲了一些): + +- parameter 参数模板信息。我现在是参数解析器,我要解析这个参数肯定得有参数信息吧,这个参数是不是标了`@RequestBody`注解啊,是不是一个复杂类型比如User对象啊等等,如果是的话我得拿到它的Class,反射创建和赋值吧。只有获得这些信息才能从HTTP请求中对应的位置找到参数,按照参数模板信息来生成参数。 + +- mavContainer 我们之前说过,mavContainer 是一次HTTP请求的上下文,用来装返回的Model和View,既然是装返回的信息,那为啥解析参数的时候还需要它呢?在写Controller方法的时候,有一种场景可以如下: + + ```java + @GetMapping("/user") + public User getUser(Model model){ + model.addAttribute("key1","value1"); + //... + } + ``` + + 我们往参数Model里设置的任何信息,都会被放进返回结果处理,比如可以转发给下一级HTTP请求(或者返给前端)。 + + 那这个参数Model是哪里来的?**就是mavContainer里的defaultModel这个属性**。我们会在具体的例子中再详细的看相关的源码,这里大家只需要大概知道一下就可以。 + +- webRequest 封装了HTTP请求和HTTP响应的类。既然要从HTTP中解析参数,自然就需要HTTP请求和响应的信息,这都是原始数据。 + +- binderFactory 我们之前讲过,数据绑定工厂,将HTTP请求信息与我们的参数的值绑定上的东西,我们会在后面具体使用场景中分析这个东西,大家别急。 + +可以看到,要解析一个`HandlerMethod`上的参数,就需要一个特定的参数解析器,而参数解析器在进行解析的时候需要parameter,mavContainer,webRequest,binderFactory的帮助才能解析出来参数。我们上面也看到Spring为我们提供了27种参数解析器(SpringBoot 2.7.2版本下是27个),这27种参数解析器就是为了应付各种场景下的HTTP请求和参数处理: + +![image-20220819160536958](http://img.topjavaer.cn/img/202311230845947.png) + + + +通过上面的名字也很容易看出一些参数解析器的作用,比如`@RequestParamMethodArgumentResolver`是用来解析标了`@RequestParam`注解的参数;`@PathVariableMethodArgumentRResolver`是解析标了`@PathVariable`注解的参数;`@RequestResponseBodyMethodArgumentResolver`是用来解析标了`@RequestBody`注解的参数。 + +**每个参数解析器都会有一定的应用场景,本文目的是帮助大家快速的掌握SpringMvc执行源码的流程,具体的业务场景不在本文展开,后续会专门出文章,从应用角度讲述这些参数解析器的源码。** + +#### 4.2.5 doInvoke() + +最后提一嘴`HandlerMethod`的执行,我们在[4.2.2](https://www.coderzoe.com/archives/28/#))的源码中已经看到了一段话 + +```java +return doInvoke(args); +``` + +也即解析完所有的参数后就可以执行`HandlerMethod`了,`doInvoke()`的源码也很简单,就是单纯的调用`method.invoke()`: + +```java +protected Object doInvoke(Object... args) throws Exception { + Method method = getBridgedMethod(); + try { + //...一些异常处理,主要是针对Kotlin + return method.invoke(getBean(), args); + } + //...一些异常处理 +} +``` + +拿到方法,填入bean,填入参数,然后反射执行。 + +#### 4.2.6 总结 + +首先SpringMvc对不同情况下的参数解析定义了一个参数解析器接口,每个参数解析器接口的实现类就是一种情况下的参数解析,借助于策略模式,我们的`HandlerAdapter`内会有一些参数解析器的实现类。在要执行一个`HandlerMethod`前肯定要先把这个方法的所有参数解析出来,因此我们就需要遍历这个方法上的每一个参数,为每一个参数寻找一个合适的参数解析器来解析它。寻找合适参数解析器的方法很简单,就是挨个问`HandlerAdapter`持有的那些参数解析器是否支持解析,一旦有支持的就代表找到了,找到后就调用参数解析器解析参数。参数解析器在解析参数的时候,需要四个帮手才能真正的解析参数,分别是: + +- 参数模板,告知这个参数的一些元数据信息 +- ModelAndView上下文容器,用来处理多参数返回和HTTP请求见传递信息的作用。 +- 原生HTTP请求和HTTP响应 +- 数据绑定工厂,用来将从HTTP请求中解析出的信息绑定到参数对象上 + +目前SpringBoot2.7.2版本有27个参数解析器,他们用于不同的场景。 + +#### 4.2.7 一些补充 + +在获得某个参数的参数解析器后,我们使用`argumentResolverCache`将这一信息缓存起来,这里就会有个问题:这个缓存的生命周期是怎样的?它会在什么时候销毁?同样两次HTTP请求,后一次会使用前一次保存的缓存解析器信息吗? + +要回答这个问题,我们需要追溯源码看下这个缓存的创建时间和执行HTTP请求时的传递过程: + +首先`argumentResolverCache`是`HandlerMethodArgumentResolverComposite`内的属性,而`RequestMappingHandlerAdapter`的属性`argumentResolvers`正是`HandlerMethodArgumentResolverComposite`: + +通过`WebMvcAutoConfiguration`和`RequestMappingHandlerAdapter`源码: + +```java +public class WebMvcAutoConfiguration { + //... + //内部类 + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(WebProperties.class) + public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware{ + //... + //@Bean创建RequestMappingHandlerAdapter实例 + @Bean + @Override + public RequestMappingHandlerAdapter requestMappingHandlerAdapter( + @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager, + @Qualifier("mvcConversionService") FormattingConversionService conversionService, + @Qualifier("mvcValidator") Validator validator) { + RequestMappingHandlerAdapter adapter = super.requestMappingHandlerAdapter(contentNegotiationManager, + conversionService, validator); + adapter.setIgnoreDefaultModelOnRedirect( + this.mvcProperties == null || this.mvcProperties.isIgnoreDefaultModelOnRedirect()); + return adapter; + } + } + //... + +} + +//RequestMappingHandlerAdapter部分源码: +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter + implements BeanFactoryAware, InitializingBean { + + //... + //后置的属性设置,主要包含初始化argumentResolvers属性 + @Override + public void afterPropertiesSet() { + // Do this first, it may add ResponseBody advice beans + initControllerAdviceCache(); + //初始化argumentResolvers + if (this.argumentResolvers == null) { + List resolvers = getDefaultArgumentResolvers(); + this.argumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); + } + if (this.initBinderArgumentResolvers == null) { + List resolvers = getDefaultInitBinderArgumentResolvers(); + this.initBinderArgumentResolvers = new HandlerMethodArgumentResolverComposite().addResolvers(resolvers); + } + if (this.returnValueHandlers == null) { + List handlers = getDefaultReturnValueHandlers(); + this.returnValueHandlers = new HandlerMethodReturnValueHandlerComposite().addHandlers(handlers); + } + } +} +``` + +可以知道`RequestMappingHandlerAdapter`的创建是在项目启动的时候创建了,同样在项目启动的时候,我们的`argumentResolverCache`就被创建好了。 + +在执行HTTP请求的时候,invokeHandlerMethod()方法部分源码如下: + +```java +protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //... + + ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod); + if (this.argumentResolvers != null) { + invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); + } + + //... + + invocableMethod.invokeAndHandle(webRequest, mavContainer); + + //.. +} +``` + +每次HTTP请求的时候都会创建一个`ServletInvocableHandlerMethod`对象,但往`ServletInvocableHandlerMethod`对象内设置的`argumentResolvers`是由`RequestMappingHandlerAdapter#argumentResolvers`属性传进去的,`RequestMappingHandlerAdapter`是单例的,也即`RequestMappingHandlerAdapter#argumentResolvers`只有一份,即使每次HTTP请求都创建`ServletInvocableHandlerMethod`,但每个对象内持有的`argumentResolvers`都是同一份,再往下追寻: + +在`ServletInvocableHandlerMethod`对象内部执行参数解析操作 + +```java +protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + //... + if (!this.resolvers.supportsParameter(parameter)) { + throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver")); + } + try { + args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory); + } + //... +} +``` + +参数解析的时候就会将找到的参数放进`argumentResolverCache`缓存中,既然所有的`ServletInvocableHandlerMethod`对象都持有一份`argumentResolvers`,那自然也持有同一份`argumentResolverCache`,都是`RequestMappingHandlerAdapter`里的那一份。因此上一次HTTP请求做的缓存保存,自然可以被下一次HTTP请求使用。 + +这里也解释了`argumentResolverCache`是`ConcurrentHashMap`,而非HashMap,因为请求肯定是并发的,有人往Map里写有人从Map中拿,要保证线程安全。 + +因此可以得出结论`argumentResolverCache`是与项目生命周期基本相同的,同一HTTP请求可以被缓存,供以后使用。 + +## 5. 第三步 执行方法并处理返回 + +### 5.1 HandlerMethodReturnValueHandler + +我们上面说了,对于`HandlerMethod`的返回情况也是多种多样的,比如可以返回视图,可以返回一个对象,对象可以被解析为JSON,还可以被解析为XML返回,可以跳转/重定向等等。针对那么多种情况,就需要对返回的结果进行适配处理。 + +与参数处理器解析器类似,对于返回结果的处理,Spring也抽象为了一个接口叫`HandlerMethodReturnValueHandler`,翻译为返回值处理器,因此一个`HandlerMethodReturnValueHandler`的实现类就是一个场景下的返回结果处理。 + +同样与参数解析器类似,SpringMVC再次采用策略模式,将所有返回值处理器的实现类保存起来,当需要处理返回值的时候就从这些实现类中选择一个来处理返回。 + +`RequestMappingHandlerAdapter`类中有一个重要的属性`returnValueHandlers`: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter + implements BeanFactoryAware, InitializingBean { + //... + private HandlerMethodReturnValueHandlerComposite returnValueHandlers; + //... +} +``` + +其中`HandlerMethodReturnValueHandlerComposite`就是返回值处理器的集合: + +```java +public class HandlerMethodReturnValueHandlerComposite implements HandlerMethodReturnValueHandler { + //... + private final List returnValueHandlers = new ArrayList<>(); + //... +} +``` + +因此`RequestMappingHandlerAdapter`对象中持有多个`HandlerMethodReturnValueHandler`实现类,默认情况下`RequestMappingHandlerAdapter`持有15个返回值处理器(SpringBoot2.7.2版本) + +![image-20220822184726495](http://img.topjavaer.cn/img/202311230845634.png) + + + +### 5.2 ServletInvocableHandlerMethod#invokeAndHandle() + +`ServletInvocableHandlerMethod`类我们之前已经讲过,它就是`HandlerMethod`的一个包装类,里面装了很多东西,其中上面说的返回值处理器也被装在了里面: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter + implements BeanFactoryAware, InitializingBean { + //... + protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //... + if (this.returnValueHandlers != null) { + //将返回值处理器设置进包装类 + invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); + } + //... + } + //... +} +``` + +因此`ServletInvocableHandlerMethod`在解析完参数并执行完`HandlerMethod`拿到结果后就开始使用返回值处理器来处理结果: + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //解析参数并执行HandlerMethod,得到返回结果 + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + + //...一些返回信息的校验 + + this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); + //...一些异常处理 +} +``` + +可以看到直接使用`returnValueHandlers`来处理返回值,因此对返回值的处理就在代码 + +```java +this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); +``` + +我们继续深入。 + +### 5.3 HandlerMethodReturnValueHandlerComposite#handleReturnValue() + +其中`HandlerMethodReturnValueHandlerComposite#handleReturnValue()`代码也与我们在参数解析器里看到的思路基本相同,从众多返回值处理器实现类中获得能处理的返回值处理器,用这个得到的实现类来处理返回值。 + +```java +public void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, + ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception { + //得到返回值处理器 + HandlerMethodReturnValueHandler handler = selectHandler(returnValue, returnType); + + // .. 异常处理 + + //调用得到的返回值处理器处理结果 + handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest); +} +``` + +而`selectHandler()`也很简单,就是遍历挨个问返回值处理器,谁支持处理,谁支持就让谁处理。 + +```java +private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) { + //... + for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { + //... + //判断是否支持处理 + if (handler.supportsReturnType(returnType)) { + return handler; + } + } + return null; +} +``` + +我们刚才已经看到在SpringBoot2.7.2版本中已经默认带了15个返回值处理器 + +![image-20220822184726495](http://img.topjavaer.cn/img/202311230845673.png) + + + +其中通过类名不难看出,`@RequestResponseBodyMethodProcessor`是处理返回结果加了`@ResponseBody`注解的情况(准确来说是`HandelrMethod`上或者Controller类上加了`@ResponseBody`注解),`ViewNameMethodReturnValueHandler`是处理返回结果是视图名的等。 + +**每个返回值处理器都会有一定的应用场景,本文目的是帮助大家快速的掌握SpringMvc执行源码的流程,具体的业务场景不在本文展开,后续会专门出文章,从应用角度讲述这些返回值处理器的源码。** + +### 5.4 RequestMappingHandlerAdapter#getModelAndView() + +执行完获得结果以后,就需要将`ModelAndView`返回,在上面的源码中我们也看到,`HandlerAdapter#handle()`的返回是`ModelAndView`。 + +返回`ModelAndView`的源码为: + +```java +public class RequestMappingHandlerAdapter extends AbstractHandlerMethodAdapter implements BeanFactoryAware, InitializingBean { + protected ModelAndView invokeHandlerMethod(HttpServletRequest request, + HttpServletResponse response, HandlerMethod handlerMethod) throws Exception { + //... + //返回ModelAndView + return getModelAndView(mavContainer, modelFactory, webRequest); + } +} +``` + +`ModelAndView`的获取很简单,就是从mavContainer中得到Model和View信息,返回即可。 + +```java +private ModelAndView getModelAndView(ModelAndViewContainer mavContainer, + ModelFactory modelFactory, NativeWebRequest webRequest) throws Exception { + + //...一些更新操作 + + //设置Model和View + ModelMap model = mavContainer.getModel(); + ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, mavContainer.getStatus()); + //设置View,针对返回不是视图名的情况 + if (!mavContainer.isViewReference()) { + mav.setView((View) mavContainer.getView()); + } + + //....重定向的特殊处理 + + return mav; +} +``` + +## 6. 第四步 视图解析 + +走完了上面那些就到了第四步:视图解析。视图解析不是必须的,只有在需要SpringMVC判断返回结果包含视图的时候才会进行视图解析(也即ModelAndView中的view信息不为空),举个例子: + +在配置了 + +```yaml +spring: + mvc: + view: + prefix: / + suffix: .html +``` + +Controller层写为 + +```java +@Controller +public class HelloController { + @GetMapping("/hello") + public String hello(){ + return "hello"; + } +} +``` + +且静态资源路径下存在hello.html时,我们请求`/hello`便会返回hello.html页面。 + +为了最后再加强大家对于SpringMVC执行流程的记忆,我们最后再将`DispatcherServlet#doDispatch()`四个步骤的重要源码拿过来: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + //对应步骤1,找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //对应步骤1,找到处理方法的适配器(适配器我们下面会说,别着急) + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //对应步骤2和步骤3,解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //对应步骤4 视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... +} +``` + +可以看到,第四步的视图解析是`processDispatchResult()`函数。 + +### 6.1 DispatcherServlet#processDispatchResult() + +其主要源码如下: + +```java +private void processDispatchResult(HttpServletRequest request, HttpServletResponse response, + @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv, + @Nullable Exception exception) throws Exception { + + //... 异常校验 + + // Did the handler return a view to render? + if (mv != null && !mv.wasCleared()) { + //解析视图 + render(mv, request, response); + //... + } + //... 一些别的处理,如记录日志和拦截器的triggerAfterCompletion执行 +} +``` + +可以看到SpringMVC会先判断ModelAndView不为空且视图信息不为空(较低版本SpringBoot这里是`&&mv.view!=null`)。 + +因此视图解析的所有核心源码在`render()`函数中: + +```java +protected void render(ModelAndView mv, HttpServletRequest request, HttpServletResponse response) throws Exception { + //...国际化处理 + + View view; + String viewName = mv.getViewName(); + if (viewName != null) { + // We need to resolve the view name. + view = resolveViewName(viewName, mv.getModelInternal(), locale, request); + if (view == null) { + throw new ServletException("Could not resolve view with name '" + mv.getViewName() + + "' in servlet with name '" + getServletName() + "'"); + } + } + //... 处理viewName为空的异常 + + try { + if (mv.getStatus() != null) { + //设置HTTP响应状态码 + request.setAttribute(View.RESPONSE_STATUS_ATTRIBUTE, mv.getStatus()); + response.setStatus(mv.getStatus().value()); + } + //视图渲染 + view.render(mv.getModelInternal(), request, response); + } + + //...异常处理 +} +``` + +这里需要讲的一点是`View`对象,对于视图这一信息,SpringMVC抽象出了一个接口叫`View`。`View`接口下的实现类非常多,每个不同的实现类都是一种视图场景。 + +![image-20220823223018574](http://img.topjavaer.cn/img/202311230845196.png) + +比如处理重定向页面的`RedirectView`,处理内部视图资源的`InternalResourceView`。 + +因此上述`render()`函数的核心是**先解析视图名得到`View`对象,再通过`View`对象做视图渲染**。 + +我们先看第一个函数,通过视图名得到`View`对象`resolveViewName()` + +### 6.2 DispatcherServlet#resolveViewName() + +#### 6.2.1 ViewResolver + +SpringMVC再次借助策略模式,将解析视图名返回View对象这一能力作为策略,策略的接口为`ViewResolver`。`ViewResolver`接口下只有一个方法: + +```java +public interface ViewResolver { + @Nullable + View resolveViewName(String viewName, Locale locale) throws Exception; + +} +``` + +就是解析视图名称返回`View`对象。 + +同样的,大家应该猜到了`DispatcherServlet`内部持有多个`ViewResolver`实现类: + +```java +public class DispatcherServlet extends FrameworkServlet { + //... + private List viewResolvers; + //... +} +``` + +#### 6.2.2 DispatcherServlet#resolveViewName() + +有了这些信息后,我们再来看`resolveViewName`函数: + +```java +protected View resolveViewName(String viewName, @Nullable Map model, + Locale locale, HttpServletRequest request) throws Exception { + + if (this.viewResolvers != null) { + for (ViewResolver viewResolver : this.viewResolvers) { + View view = viewResolver.resolveViewName(viewName, locale); + if (view != null) { + return view; + } + } + } + return null; +} +``` + +就是拿到所有的实现类,挨个遍历调用`viewResolver.resolveViewName()`来解析视图,谁解析出来了(`View!=null`)就直接返回。 + +默认情况下`DispatcherServlet`内部持有4个视图解析器实现类: + +![image-20220823224512517](http://img.topjavaer.cn/img/202311230845917.png) + + + +其实第一个实现类内部持有其他三个实现类,这点我们会以后在具体场景中再讲。 + +### 6.3 view.render() + +视图解析器解析出的View对象后,调用`view.render()`就可以渲染视图,我们刚才已经看到了`View`接口下有很多实现类且每个实现类应用场景不同,因此具体的`render()`我们后续会以具体的场景来分析。 + +## 7. 总结 + +其实从上到下看过来,发现一切都是一个策略模式(其中还有一个适配器模式),首先根据HTTP请求处理的不同,SpringMVC抽象出了`HandlerMapping`接口,并且`DispatcherServlet`对象持有多个`HandlerMapping`实现类。 + +一般请求到我们Controller层的由实现类`RequestHandlerMapping`来处理,SpringMVC会在项目一启动的扫描我们所有Controller下的标了`@RequestMapping`注解的处理 函数,并将这个处理函数封装成`HandlerMethod`,使用一个大的集合管理这些`HandlerMethod`(就是`MappingRegistry`),因此`RequestHandlerMapping`会根据请求路径和请求方式从集合中找到合适的`HandlerMethod`来处理请求。 + +但是HTTP请求五花八门,我们写的Controller层方法各有不同,为了能比较通用的执行HTTP请求,就需要适配,因此再次借助策略模式,SpringMVC将适配器抽象为接口`HandlerAdapter`,并且`DispatcherServlet`对象持有多个`HandlerAdapter`实现类。 + +需要适配Controller层方法,也即`HandlerMethod`的适配器叫做`RequestMappingHandlerAdapter`,`RequestMappingHandlerAdapter`作为适配器主要做了两件事:参数解析和返回结果处理。所谓参数解析就是将HTTP请求中的内容解析为我们`HandlerMethod`上的参数,返回结果处理就是对一些返回的结果做特殊处理并写出到HTTP输出流中,比如将返回的Java对象转为JSON写进输出流。 + +`RequestMappingHandlerAdapter`的适配工作也依赖于了策略模式,解析参数的时候抽象出一个接口叫`HandlerMethodArgumentResolver`,同时自己持有多个`HandlerMethodArgumentResolver`的实现类。`RequestMappingHandlerAdapter`会遍历我们`HandlerMethod`上的每一个参数,然后对每个参数都挨个问`HandlerMethodArgumentResolver`能否解析,如果能解析就解析参数(实际会有个缓存,防止以后再挨个问)。同理返回处理的时候也使用了策略模式,将返回处理抽象为接口`HandlerMethodReturnValueHandler`,自己持有多个`HandlerMethodReturnValueHandler`的实现类,在解析完参数并调用`MethodHandler`执行完处理得到结果后,就开始结果处理,同样挨个调用`HandlerMethodReturnValueHandler`的实现类,问问它能不能处理,要是能就处理。 + +在上述这些流程都完成后,最后一步就是视图解析,视图解析一般分为两步,视图名解析和视图渲染。我们一般返回的信息是视图名,SpringMVC将所有视图抽象为接口`View`,因此就需要根据返回的视图名寻找对应的解析器将视图名解析为`View`对象,针对这一解析过程,使用策略模式,抽象为接口`ViewResolver`,`DispatcherServlet`内持有多个`ViewResolver`的实现类,也是一样的遍历每个实现类看看能不能解。在得到`View`对象后,调用`View.render()`对视图进行渲染,就完成了视图解析。 \ No newline at end of file diff --git a/docs/source/spring-mvc/2-guide.md b/docs/source/spring-mvc/2-guide.md new file mode 100644 index 0000000..8902e33 --- /dev/null +++ b/docs/source/spring-mvc/2-guide.md @@ -0,0 +1,93 @@ + +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,MVC模式,Spring MVC和Struts,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器,REST + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +在分析SpringMVC源码之前,先来分享一下:如何优雅高效地阅读源码 + +### 3.1 官方文档 + +https://docs.spring.io/spring/docs/4.3.7.RELEASE/spring-framework-reference/htmlsingle/#spring-introduction + +理论以官方文档和源码为准 + +### 3.2 下载源码 + +在阅读源码前,请大家下载源码,可以在Maven中下载,请大家自行百度如何下载源码。源码有丰富的注释 + +下面是DispatcherServlet.java源码截图,方法和变量都有详细的注释 + +![](http://img.topjavaer.cn/img/202310070843881.png) + +### 3.3 常用快捷键 + +记住快捷键会让你事半功倍 + +ctrl+n:快速进入类 + +ctrl+shift+n:进入普通文件 + +ctrl+f12:查看该类方法 + +在阅读源码的时候特别方便,因为你不可能每个方法都细细品读 + +![](http://img.topjavaer.cn/img/202310070843416.png) + +ctrl+alt+u:查看类结构图,这些类都可以点击进入,我比较喜欢用这个 + +![](http://img.topjavaer.cn/img/202310070843077.png) + +ctrl+shift+alt+u:查看类结构图,这些类不能进入 + +alt+f7:查看方法引用位置,以doDispatch()为例,可以看到DispatcherServlet 897行被引用,858行注释被引用 + +![](http://img.topjavaer.cn/img/202310070844708.png) + +ctrl+alt+b:跳转到方法实现处,对者接口方法点击,会弹出来在哪里实现。 + +![](http://img.topjavaer.cn/img/202310070844974.png) + +接下来是我不得不说的idea的神器——书签(bookmark),可以对代码行进行标记,并进行快速切换 + +ctrl+f11:显示bookmark标记情况,土黄色代表该字符已被占用,输入或者点击1代表在此位置书签为1 + +![](http://img.topjavaer.cn/img/202310070844271.png) + +我们以processDispatchResult()方法为例 + +![](http://img.topjavaer.cn/img/202310070844048.png) + +ctrl+标记编号 快速回到标记处,如我刚才在这留下了书签,ctrl+1,DispatcherServlet 1018行 + +shift+f11:显示所有书签,左栏是我打过书签的类、行信息,右边是代码详情 + +![](http://img.topjavaer.cn/img/202310070845135.png) + +当你所有书签都用完,0-9,a-z全部用完,可以直接ctrl+f11,记录普通书签,虽然无法用ctrl快速跳转,在shift+f11还是可以找到 + +![](http://img.topjavaer.cn/img/202310070845600.png) + +alt+f8:启用Evaluate窗口 + +当我们想看返回值,无法声明变量查看该变量的时候(源码不可更改) + +![](http://img.topjavaer.cn/img/202310070845830.png) + +可以使用Evaluate表达式 + +![](http://img.topjavaer.cn/img/202310070845794.png) + + + +以上就是导读篇的内容,下篇文章我们将进入源码分析部分。 \ No newline at end of file diff --git a/docs/source/spring-mvc/3-scene.md b/docs/source/spring-mvc/3-scene.md new file mode 100644 index 0000000..e7d8aa7 --- /dev/null +++ b/docs/source/spring-mvc/3-scene.md @@ -0,0 +1,1982 @@ +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,参数解析器,MVC模式,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器 + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +上篇中我们已经讲了SpringMVC处理HTTP请求的整体流程,其间我们讲到了很多接口,如参数解析器,返回结果处理器等,但我们都没有深入的进去查看这些解析器或处理器是如何处理的。本章就带大家进入真实的案例场景,看看这些接口在具体场景下是如何发挥作用的 + +## 0. 参数解析器 + +为了方便后面具体案例的分析,我们先来回顾下之前在上一篇中讲到的参数解析器。 + +当我们发送HTTP请求 时,根据之前的框架分析,我们知道这个请求会由`RequestMappingHandlerAdapter`来处理,在`RequestMappingHandlerAdapter`来处理的时候,会将自己的很多信息封装到`ServletInvocableHandlerMethod`中,包括自己的参数解析器和返回值处理器。`ServletInvocableHandlerMethod`做处理的代码我们之前看过了,这里再拿过来: + +```java +public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //参数解析与HandlerMethod的执行 + Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs); + //... 一些返回的处理 +} +``` + +其中`invokeForRequest()`内容如下: + +```java +public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + //参数解析 + Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs); + //...日志 + //函数执行 + return doInvoke(args); +} +``` + +`getMethodArgumentValues()`内容如下: + +```java +protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer, + Object... providedArgs) throws Exception { + + MethodParameter[] parameters = getMethodParameters(); + if (ObjectUtils.isEmpty(parameters)) { + return EMPTY_ARGS; + } + + 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); + } + //...异常处理 + } + return args; +} +``` + +这里的`supportsParameter()`源码如下: + +```java +public boolean supportsParameter(MethodParameter parameter) { + return getArgumentResolver(parameter) != null; +} +private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + //遍历判断每个参数解析器是否支持解析当前参数 + if (result == null) { + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + result = resolver; + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; +} +``` + +而`resolveArgument()`源码如下: + +```java +public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); + //异常处理 + return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory); +} +``` + +我们之前虽然大体上走完了每个流程,但其实并没有深入到每个接口的实现类里去看,这一主要原因是SpringMVC要处理的情况太多了,只能结合实际的场景来说源码。因此下面我们将会一个一个场景的讲源码处理,首先从参数解析器的源码场景说起。 + +## 1. @PathVariable + +`@PathVariable`注解是Web开发中十分常见的一个注解,我们可以从路径中获取信息作为参数。 + +举个例子如下: + +```java +@RestController +public class PathVariableController { + @GetMapping("/user/{id}") + public String getUser(@PathVariable("id") long id){ + return "hello user"; + } +} +``` + +当我们执行HTTP请求 `GET /user/1`的时候,上面的参数id值就被映射为了1。根据上面的源码我们知道,要想解析出参数id,必然有一个参数解析器做了这个工作。 + +首先打断点到`supportsParameter()`,查看到底是哪个参数解析器支持这样的处理。通过打断点可以很容易知道是`PathVariableMethodArgumentResolver`参数解析器支持解析这个参数,那我们就需要问两个问题了: + +1. 为什么`PathVariableMethodArgumentResolver`可以支持这个参数的解析,它是如何判定的 +2. `PathVariableMethodArgumentResolver`是如何解析参数,从HTTP请求中将信息抠出赋给id字段的呢? + +这两个问题的答案其实也是`PathVariableMethodArgumentResolver`的两个实现方法:`supportsParameter()`和`resolveArgument()` + +我们先来看第一个方法的源码: + +```java +public boolean supportsParameter(MethodParameter parameter) { + //不带PathVariable注解直接返回false + if (!parameter.hasParameterAnnotation(PathVariable.class)) { + return false; + } + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + PathVariable pathVariable = parameter.getParameterAnnotation(PathVariable.class); + return (pathVariable != null && StringUtils.hasText(pathVariable.value())); + } + return true; +} +``` + +这个函数的判断逻辑比较简单,首先是判断如果参数上不带`@PathVariable`注解直接返回false。其次带了注解,判断我们的参数类型是不是Map类型的,如果是的话就获取`@PathVariable`注解信息并要求其value内容不能为空。最后如果不是Map但是有`@PathVariable`注解就直接返回true。我们的参数是long型,且标了`@PathVariable`,因此会直接返回true。 + +再看第二个源码(其实是`PathVariableMethodArgumentResolver`的父类`AbstractNamedValueMethodArgumentResolver`实现的): + +```java +public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + //拿到我们的参数名,在上例中,我们的参数名是id(其实就是@PathVariable注解里的value值) + NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); + MethodParameter nestedParameter = parameter.nestedIfOptional(); + + Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); + + //... + + //拿到参数名后,从HTTP请求中解析出这个参数名位置的值,比如 GET /user/1 此时arg就是"1" + Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); + //... 一些校验和处理 + if (binderFactory != null) { + //创建数据绑定器 + WebDataBinder binder = binderFactory.createBinder(webRequest, null, namedValueInfo.name); + try { + //使用数据绑定器进行数据转化(如果需要的话) + arg = binder.convertIfNecessary(arg, parameter.getParameterType(), parameter); + } + + //... 一些异常处理 + } + + handleResolvedValue(arg, namedValueInfo.name, parameter, mavContainer, webRequest); + + return arg; +} +``` + +首先我们先看下参数解析器是如何从请求信息中拿到参数值,也即:`resolveName(resolvedName.toString(), nestedParameter, webRequest);`的实现: + +```java +protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + Map uriTemplateVars = (Map) request.getAttribute( + HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE, RequestAttributes.SCOPE_REQUEST); + return (uriTemplateVars != null ? uriTemplateVars.get(name) : null); +} +``` + +可以看到思路很简单,在走到当前步骤之前SpringMVC就做了一个处理,将我们Controller上写的URL与实际请求的URL做了一个映射处理,比如 Controller层为:`/user/{id}/{age}` ,实际请求为:`/user/1/27`。这时SpringMVC会根据路径匹配得到K,V,分别是id ->1和age ->27。并将这个Map存储在Request的请求域中,且将它的key值设为`HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE`。这样我们走到这里的时候再从请求域中根据key值就可以拿到这个信息,信息本身是个Map,再传入resolvedName就可以拿到value,因此很容易通过传入id得到1这个信息。(关心这个Map是何时构建的可以查阅源码的`RequestMappingInfoHandlerMapping#extractMatchDetails()`函数,它会在`RequestMappingInfoHandlerMapping#getHandlerInternal()`里被调用,只不过这里面的内容有些多,需要断点打的深一点) + +其次我们又看到了数据绑定器,我们前一篇中已经见过它了,当时我们说从HTTP请求中解析出数据后,得能将这些数据绑定到我们的参数上的,这个工作就是数据绑定器干的工作。由于我们的当前例子的参数比较简单(只是一个long id),所以很难发挥数据绑定器的作用,不过我们这里可以先简单的说一下: + +我们知道HTTP协议是文本协议,这代表从HTTP请求中解析出的东西都是文本(二进制除外),所谓文本也即字符串,因此上面的`resolveName()`函数得到的其实是字符串"1",但我们的HandlerMethod参数是long类型,这就需要一个字符串向long类型的转化,而这其实就是数据绑定器做的工作。 + +如果大家源码打的比较深的话会发现,实际进行转化的核心代码如下: + +```java +public class GenericConversionService implements ConfigurableConversionService { + //... + public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + //... + //拿到转化器进行转化 + GenericConverter converter = getConverter(sourceType, targetType); + if (converter != null) { + Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType); + } + //... + } + //... + +} +``` + +而获取转化器的代码如下: + +```java +protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType); + GenericConverter converter = this.converterCache.get(key); + if (converter != null) { + return (converter != NO_MATCH ? converter : null); + } + + converter = this.converters.find(sourceType, targetType); + if (converter == null) { + converter = getDefaultConverter(sourceType, targetType); + } + + if (converter != null) { + this.converterCache.put(key, converter); + return converter; + } + + this.converterCache.put(key, NO_MATCH); + return null; +} +``` + +是不是又想到了策略模式?自己持有多个转化器,在需要转化的时候就找到适合的转化器来转化。 + +在SpringBoot2.7.2版本中,默认情况下的转化器共有124个,部分内容如下: + +![](http://img.topjavaer.cn/img/202311262224580.png) + + + +![image-20220906212651245](http://img.topjavaer.cn/img/202311262224620.png) + + + +![image-20220906212707262](http://img.topjavaer.cn/img/202311262224059.png) + +可以看到这些参数解析器都是将一种数据类型转为另一种数据类型,针对于我们的情况就需要一个将String类型转为Long类型的转化器。 + +另外需要提一嘴的是,这里的策略模式与之前参数解析器或返回值处理器的设计不同,之前策略接口都都有个类似于`support()`的功能,主类会挨个问每个策略是否支持解析,支持了再调用它来解析。但是这里是用HashMap来做的。也即我们将情况设计为key值,解决方案设计为value值。这样直接输入key值就能得到策略,无需遍历询问。通过源码也很容易看到: + +```java +private final Map converters = new ConcurrentHashMap<>(256); +``` + +`converters`是一个Map。其Key是源类型+目标类型 + +```java +final class ConvertiblePair { + + private final Class sourceType; + + private final Class targetType; +} +``` + +另外,SpringMVC支持让我们自定义一些类型转化器的,可以按照自己的规则来做类型转化,我们下一章就会说到。 + +## 2. 表单提交 + +### 2.1. 源码分析 + +举例如下: + +```html +
+
+
+
+
+ +
+``` + + + +```java +@RestController +public class FormController { + + @PostMapping("/user") + public String addUser(User user){ + return user.toString(); + } +} +``` + +其中我们的POJO类,User和Pet信息如下: + +```java +public class User { + private String name; + private int age; + private Pet pet; + //省略getter setter +} +public class Pet { + private String name; + private int age; + //省略getter setter +} +``` + +当我们点击输入如下信息: + +![image-20220906214620559](http://img.topjavaer.cn/img/202311262225025.png) + +点击提交给后端,我们就能通过前端的参数将这些信息赋值到User对象上 + +![](http://img.topjavaer.cn/img/202311262225164.png) + +很明显,这是SpringMVC帮我们做的,依据之前的源码经验,我们知道必然有一个参数解析器帮我们做了这个工作,同样通过源码 + +```java +private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); + if (result == null) { + for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { + if (resolver.supportsParameter(parameter)) { + result = resolver; + this.argumentResolverCache.put(parameter, result); + break; + } + } + } + return result; +} +``` + +很快可以定位到帮我们解析参数的参数解析器是`ServletModelAttributeMethodProcessor`,因此这个参数解析器就是处理表单提交的,老规矩我们先看下它为什么能够处理表单请求再看下它是如何解析表单参数的: + +```java +public boolean supportsParameter(MethodParameter parameter) { + return (parameter.hasParameterAnnotation(ModelAttribute.class) || + (this.annotationNotRequired && !BeanUtils.isSimpleProperty(parameter.getParameterType()))); +} +``` + +其中`parameter.hasParameterAnnotation(ModelAttribute.class)`是指当前参数上包含注解`@ModelAttribute`,我们这里没有,因此是false。`this.annotationNotRequired`在当前对象中恒为true(构造方法中构造时就写死的true),最后一个`BeanUtils.isSimpleProperty(parameter.getParameterType())`是判断当前参数是否是基本类型,我们的参数是User对象,不是基本类型,取反后为true,因此整体返回true。 + +下面我们再看下`ServletModelAttributeMethodProcessor`是如何解析参数的: + +```java +public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + //... + //...获得参数名 + String name = ModelFactory.getNameForParameter(parameter); + //... + + Object attribute = null; + BindingResult bindingResult = null; + //... + //构造参数实例,其实就是调用默认构造方法,得到一个空的对象 + attribute = createAttribute(name, parameter, binderFactory, webRequest); + //...异常处理 + + if (bindingResult == null) { + //获得数据绑定器 + WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name); + if (binder.getTarget() != null) { + if (!mavContainer.isBindingDisabled(name)) { + //使用数据绑定器,从HTTP请求中取出信息做绑定操作,核心方法 + bindRequestParameters(binder, webRequest); + } + //... + } + //... + } + //将信息加入到ModelAndViewContainer中 + Map bindingResultModel = bindingResult.getModel(); + mavContainer.removeAttributes(bindingResultModel); + mavContainer.addAllAttributes(bindingResultModel); + //返回结果 + return attribute; +} +``` + +源码中的流程会比较长,这里只截取了主要的部分,其思路比较清晰: + +由于我们知道当前参数不是一个简单类型,因此需要先构造出来,通过 + +```java +attribute = createAttribute(name, parameter, binderFactory, webRequest); +``` + +就构造出了一个空的对象(如果点进去源码的话会发现其实就是反射拿到默认构造方法,然后调用默认的构造方法创建对象),比如如果是我们的User对象,执行完这句后就会得到: + +![](http://img.topjavaer.cn/img/202311262225846.png) + +我们不妨叫一个壳对象,然后构造数据绑定器,数据绑定器会解析HTTP请求,将HTTP请求的信息绑定到我们的壳对象上,这样我们就得到了一个有意义的对象,其中数据的绑定操作对应源码: + +```java +bindRequestParameters(binder, webRequest); +``` + +执行完成后,我们的User对象就变成了 + +![](http://img.topjavaer.cn/img/202311262225889.png) + +因此`bindRequestParameters()`方法是解析出来参数。 + +`bindRequestParameters()`源码如下: + +```java +protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) { + ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class); + Assert.state(servletRequest != null, "No ServletRequest"); + ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder; + servletBinder.bind(servletRequest); +} +``` + +而`servletBinder.bind()`源码如下: + +```java +public void bind(ServletRequest request) { + MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request); + //... 一些特殊处理 + addBindValues(mpvs, request); + doBind(mpvs); +} +``` + +十分关键的是`MutablePropertyValues`对象,这里我们已经将form表单提交的参数进行了初步解析,解析为了一个`MutablePropertyValues`对象,`MutablePropertyValues`内部有个List,装着解析后的每个参数信息: + +![](http://img.topjavaer.cn/img/202311262226515.png) + +这个解析其实不难,我们在提交form表单的时候,HTTP的参数原始数据如下: + +`name=tom&age=18&pet.name=myDog&pet.age=2`,因此我们可以很容易的按`&`和`=`进行拆分,得到上述`MutablePropertyValues`信息。 + +这个信息会非常的关键,我们在后面的数据绑定中就是以这个信息作为信息源来与我们的User对象进行绑定的。 + +多次源码深入后会走到 `DataBinder#applyPropertyValues()`方法,其源码如下: + +```java +protected void applyPropertyValues(MutablePropertyValues mpvs) { + try { + // Bind request parameters onto target object. + getPropertyAccessor().setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields()); + } + //...异常处理 +} +``` + +可以看到就是拿到属性访问器,设置属性。所谓属性访问器,大家不用想的多高大上,其实就是一个对象包装器,通过它可以反射的将一些属性设置值。这里的属性访问器就是`BeanWrapperImpl`,了解Spring的同学肯定对它很熟悉(其实我们三期网管的协议解析器就用了这个对象,我们可以简单将它理解为一个工具类,这个工具类可以反射设置对象里的属性值)。因此SpringMVC就是通过`BeanWrapperImpl`将HTTP请求信息绑定到`User`对象上的。 + +`BeanWrapperImpl#setPropertyValues()`函数源码如下: + +```java +public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean ignoreInvalid) + throws BeansException { + + List propertyAccessExceptions = null; + //获得每个属性的信息,在上面我们已经看到了pvs中是有一个List的,从HTTP请求中解析出的 + List propertyValues = (pvs instanceof MutablePropertyValues ? + ((MutablePropertyValues) pvs).getPropertyValueList() : Arrays.asList(pvs.getPropertyValues())); + //... + try { + //for循环遍历每个属性值,然后设置 + for (PropertyValue pv : propertyValues) { + try { + setPropertyValue(pv); + } + //... 异常处理 + } + } + //... +} +``` + +后面一层层的源码会很深,我们可以讲一些比较核心的部分: + +在进行`setPropertyValue()`设置时会走进`AbstractNestablePropertyAccessor#processLocalProperty()`函数(`BeanWrapperImpl`继承自`AbstractNestablePropertyAccessor`): + +`AbstractNestablePropertyAccessor#processLocalProperty()`函数中比较重要的一行信息是: + +```java +valueToApply = convertForProperty( + tokens.canonicalName, oldValue, originalValue, ph.toTypeDescriptor()); +``` + +其中`convertForProperty()`内容如下: + +```java +protected Object convertForProperty( + String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td) + throws TypeMismatchException { + + return convertIfNecessary(propertyName, oldValue, newValue, td.getType(), td); +} +``` + +走到这里的时候就可以看到与`@PathVariable`的一些共同之处,都需要使用类型转化器来转化数据。 + +这其实也容易理解,HTTP请求提交age=18,解出来的是字符串"18",自然就需要转为int类型。 + +因此大体的流程为:通过HTTP请求解析form提交的信息,根据KV值解析出来多条,如: + +```java +[ + "name": "张三", + "age": "18", + "pet.name": "狗子", + "pet.age": "18" +] +``` + +接着调用默认构造方法,new出空壳的参数对象,再借用`BeanWrapperImpl`,将解析出的多条KV信息绑定到参数对象上。在绑定的过程中可能需要类型转化,比如字符串转整型,这时就需要借助类型转化器来转化数据,将转化后的数据再绑定到属性上。 + +另外,`BeanWrapperImpl`其实也是持有124个类型转化器的: + +![](http://img.topjavaer.cn/img/202311262226320.png) + + + +### 2.2 自定义类型转化器 + +可以看到,类型转化器会将HTTP请求信息转化为我们参数上对应的数据类型,我们之前也说了SpringBoot2.7.2版本中默认包含124个类型转化器,这些类型转化器大都是基本类型转化器,如String转Integer等。我们可以自定义类型转化器,来扩展Spring自带的转化器功能,举例如下: + +```html +
+
+
+
+ +
+``` + +上例中,我们将`pet`信息写为**"狗子,18"**,也即我们想将**"狗子,18"**这一信息转为`Pet`对象,再或者说,我们想将字符串类型转为`Pet`对象。我们知道SpringBoot默认的数据转化器是没有这种功能的,此时就需要自定义类型转化器: + +```java +public class MyPetConverter implements Converter { + @Override + public Pet convert(String source) { + Pet pet = new Pet(); + String[] split = source.split(","); + pet.setName(split[0]); + pet.setAge(Integer.parseInt(split[1])); + return pet; + } +} +``` + +自定义类型转化器需要继承自`Convert`接口,我们这里的转化做的比较粗糙,大家明白就好。 + +接着我们就需要将自己的自定义转化器注册到SpringBoot中,**目前对于SpringMvc功能的增强可以通过自定义一个WebMvcConfigure Bean 或者继承WebMvcConfigure接口实现自己的对象注册到Bean中**。 + +```java +@Configuration +public class MyWebMvcConfigure implements WebMvcConfigurer { + @Override + public void addFormatters(FormatterRegistry registry) { + registry.addConverter(new MyPetConverter()); + } +} +``` + +这样SpringMVC在进行类型转化的时候,根据form表单提交的信息`pet=myDog,2`,SpringMVC根据key值pet找到User对象中属性是pet的属性,发现类型是Pet类型,然后就是将`myDog,2`字符串转为Pet类型,此时转化的源是String,目的是Pet类型,根据这一信息作为key值从converts这个map中获取转化器,很自然的就拿到了我们自己写的转化器,然后调用我们自己写的转化器将字符串转为Pet对象。 + +### 2.3 一些补充 + +我在B站看这块视频的时候看到很多弹幕会提一个问题,我们在2.1节举的例子中,为什么Controller层的方法没加`@RequestBody`注解? + +也即 + +```java +@RestController +public class FormController { + + @PostMapping("/user") + public String addUser(User user){ + return user.toString(); + } +} +``` + +这段代码中的参数User对象,为什么没有标@RequestBody注解。很多同学会认为只要是Post请求提交的对象,后端都应该是加`@RequestBody`注解的。 + +首先`@RequestBody`注解使用的场景是请求参数在请求体中并且是JSON格式(如果不了解你需要先学习基本的Spring MVC应用知识),而表单提交提交的数据虽然在请求体中,但不是JSON格式的数据,而是param1=value1¶m2=value2格式的数据,因此此处如果加`@RequestBody`注解会无法进来,从这也可以看出,这两种情况是使用不同的参数解析器来解析的。 + +注:上面说的不是很贴切,`@RequestBody`注解虽然拿的是请求体中的数据,但并不一定是JSON,你完全可以这样写: + +```java +@PostMapping("/user") +public String postUser(@RequestBody String json){ + return json; +} +``` + +这代表直接将请求体中所有的数据拿到作为一个字符串,这时请求体中到底传过来的是不是JSON都无所谓了。 + +但如果写出 + +```java +@PostMapping("/user") +public String postUser(@RequestBody User user){ + System.out.println(user); + return user.toString(); +} +``` + +则请求体中一定得是JSON格式的。 + +## 3. @RequestBody + +### 3.1 源码分析 + +上面我们已经提了一嘴`@RequestBody`注解,这个注解主要是将HTTP请求协议体中的JSON数据转为我们的Java对象,举例如下: + +```java +@RestController +public class RequestBodyController { + + @PostMapping("/user/2") + public Object addUse(@RequestBody User user){ + return user; + } +} +``` + +我们使用postman模拟发送请求如下: + +![](http://img.topjavaer.cn/img/202311262227870.png) + +很明显又是SpringMVC将HTTP请求体中的数据取出来,转为了我们的User对象并设置赋给了我们的参数,那么就来看看是哪个参数解析器做的工作: + +打断点定位是哪个参数解析器的工作我们不再重复了,实际上是`RequestResponseBodyMethodProcessor`参数解析器,我们还是看两个内容,为什么它支持解析这种情况,以及它是如何解析的: + +`supportsParameter()`源码如下: + +```java +@Override +public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(RequestBody.class); +} +``` + +可以看到很简单,就是判断参数上是否有`@RequestBody`注解,凡是有这个注解的就支持,我们的User对象上有这个注解,所以很明显,当前参数解析器能解析这种情况。 + +下面我们就看下`RequestResponseBodyMethodProcessor`是如何解析参数的: + +```java +@Override +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()); + //... + + return adaptArgumentIfNecessary(arg, parameter); +} +``` + +`readWithMessageConverters()`函数会将HTTP请求体中的JSON转为我们的Java对象,其源码如下: + +```java +@Nullable +protected Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter, + Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException { + + //拿到请求类型 + MediaType contentType; + boolean noContentType = false; + try { + contentType = inputMessage.getHeaders().getContentType(); + } + //... + + //拿到我们的参数类型和Controller类型 + Class contextClass = parameter.getContainingClass(); + Class targetClass = (targetType instanceof Class ? (Class) targetType : null); + //... + + //拿到HTTP请求类型 + HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null); + Object body = NO_VALUE; + + EmptyBodyCheckingHttpInputMessage message = null; + try { + message = new EmptyBodyCheckingHttpInputMessage(inputMessage); + + //开始遍历所有的HttpMessageConverter,看看谁支持将当前http请求内容转为我们的参数 + for (HttpMessageConverter converter : this.messageConverters) { + Class> converterType = (Class>) converter.getClass(); + GenericHttpMessageConverter genericConverter = + (converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter) converter : null); + if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) : + //核心就是这个canRead,与我们之前的参数解析器的support()功能一致 + (targetClass != null && converter.canRead(targetClass, contentType))) { + if (message.hasBody()) { + HttpInputMessage msgToUse = + getAdvice().beforeBodyRead(message, parameter, targetType, converterType); + //找到HttpMessageConverter后调用read方法进行转化 + body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) : + ((HttpMessageConverter) converter).read(targetClass, msgToUse)); + body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType); + } + else { + body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType); + } + break; + } + } + } + //...异常捕捉与日志记录 + + return body; +} +``` + +能够看到,这里依然是策略模式的使用,策略的接口是`HttpMessageConverter`,翻译过来即Http消息转化器。`RequestResponseBodyMethodProcessor`根据请求的**content-type**和我们的参数以及Controller对象类型等信息挨个询问每个消息转化器是否支持解析当前HTTP消息。在这里我们的content-type是**application/json**。 + +`RequestResponseBodyMethodProcessor`对象持有多个`HttpMessageConverter`,其中属性`messageConverters`是个`List`。SpringBoot2.7.2版本默认情况下`messageConverters`内有10个`HttpMessageConverter`实现对象 + +![](http://img.topjavaer.cn/img/202311262227622.png) + +通过名字不难看出`ByteArrayHttpMessageConverter`是用来解析byte数组的,`StringHttpMessageConverter`是用来解析字符串的,`MappingJackson2HttpMessageConverter`对象是用来转化JSON数据的,`Jaxb2RootElementHttpMessageConverter`是用来解析xml的。 + +打断点不难发现,`MappingJackson2HttpMessageConverter`解析器可以解析我们的请求,其判断自己是否能解析的代码如下: + +```java +public boolean canRead(Type type, @Nullable Class contextClass, @Nullable MediaType mediaType) { + //先判断是不是自己能支持的mediaType,这里我们的mediaType是application/json + if (!canRead(mediaType)) { + return false; + } + //得到参数类型,其实就是我们的User.class + JavaType javaType = getJavaType(type, contextClass); + + //得到ObjectMapper,jackson的核心组件 + ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), mediaType); + if (objectMapper == null) { + return false; + } + AtomicReference causeRef = new AtomicReference<>(); + //判断当前对象是否能序列化 + if (objectMapper.canDeserialize(javaType, causeRef)) { + return true; + } + logWarningIfNecessary(javaType, causeRef.get()); + return false; +} +``` + +`MappingJackson2HttpMessageConverter`支持两种mediaType:**application/json**和**application/\*+json** + +![](http://img.topjavaer.cn/img/202311262227182.png) + +我们发的HTTP请求content-type是**application/json**,同时我们的Java对象还支持序列化,因此自然返回true。 + +知道了`MappingJackson2HttpMessageConverter`为什么能够解析,再看`MappingJackson2HttpMessageConverter`是如何解析的: + +```java +public Object read(Type type, @Nullable Class contextClass, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException { + //拿到我们的参数类型 + JavaType javaType = getJavaType(type, contextClass); + //传参Java类型和Http输入信息 + return readJavaType(javaType, inputMessage); +} +``` + + + +```java +private Object readJavaType(JavaType javaType, HttpInputMessage inputMessage) throws IOException { + //拿到http请求的content-type和charset + MediaType contentType = inputMessage.getHeaders().getContentType(); + Charset charset = getCharset(contentType); + + //拿到ObjectMapper + ObjectMapper objectMapper = selectObjectMapper(javaType.getRawClass(), contentType); + //... + + //判断是不是unicode编码类型 + boolean isUnicode = ENCODINGS.containsKey(charset.name()) || + "UTF-16".equals(charset.name()) || + "UTF-32".equals(charset.name()); + try { + InputStream inputStream = StreamUtils.nonClosing(inputMessage.getBody()); + //.... + + //由于我们的请求类型是UTF-8,因此会进入if分支 + if (isUnicode) { + //直接调用objectMapper将HTTP输入流转为我们的java对象 + return objectMapper.readValue(inputStream, javaType); + } + //... + } + //...异常捕捉 +} +``` + +其实就是拿到`ObjectMappper`然后调用`ObjectMappper`来解析JSON。 + +### 3.2 一些总结 + +这样其实我们就基本分析完了参数解析器`RequestResponseBodyMethodProcessor`处理流程的源码,首先`RequestResponseBodyMethodProcessor`会解析所有标注了`@RequestBody`注解的参数,其次在解析的时候,`RequestResponseBodyMethodProcessor`内部持有多个`HttpMessageConverter`,`RequestResponseBodyMethodProcessor`会挨个遍历每个`HttpMessageConverter`询问其是否能够解析,`HttpMessageConverter`一般会根据请求的**content-type**和要转化的Java对象来判断自己是否能解析,如我们的`MappingJackson2HttpMessageConverter`只能解析请求的content-type是**application/json**和**application/\*+json**的。拿到`HttpMessageConverter`就可以直接进行解析了。**可以看到我们之前对于`@RequestBody`注解的理解比较片面,认为前端必须要传入JSON,然后它就会被解析为Java对象,现在看了源码会发现前端能传很多格式可以被`@RequestBody`解析,比如xml。** + +### 3.3 自定义HttpMessageConverter + +与自定义类型转化器一样,我们也可以自定义消息转化器。一个消息转化器往往是解析一种(或多种)mediaType类型下的HTTP请求,不同的mediaType的HTTP请求内容格式也不相同,比如application/json格式就是JSON类型,application-xml格式就是xml类型。 + +既然要自定义`HttpMessageConverter`,就使用自定义的media-type:**application-dabin** + +![](http://img.topjavaer.cn/img/202311262228041.png) + +同时要求这种media-type下前端传过来的参数只有value,没有key,且value之间逗号隔开,比如前端传过来的请求体是: + +![](http://img.topjavaer.cn/img/202311262228829.png) + +这时为保证我们的Controller层依然能够正常接收前端的请求,就需要自定义一个消息转化器,来专门解析**application-dabin**这种mediaType的请求: + + + +```java +static class MyHttpMessageConverter implements HttpMessageConverter{ + private final MediaType mediaType = new MediaType("application","dabin"); + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + //先不关心写 + return false; + } + + @Override + public List getSupportedMediaTypes() { + //先不关心 + return null; + } + + @Override + public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + User user = new User(); + + int available = inputMessage.getBody().available(); + byte[] bytes = new byte[available]; + inputMessage.getBody().read(bytes); + String[] split = new String(bytes).split(","); + user.setName(split[0]); + user.setAge(Integer.parseInt(split[1])); + Pet pet = new Pet(); + pet.setName(split[2]); + pet.setAge(Integer.parseInt(split[3])); + user.setPet(pet); + return user; + } + + @Override + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + //先不关心写 + } +} +``` + +我们主要实现了`canRead()`和`read()`方法,实现的源码比较简单,这里不再解释。 + +然后我们将这个自定义的消息转化器加入到Spring中: + +```java +@Configuration +public class MyWebMvcConfigure implements WebMvcConfigurer { + @Override + public void extendMessageConverters(List> converters) { + converters.add(new MyHttpMessageConverter()); + } +} +``` + +根据前面的postman截图发送消息,打断点可以看到`RequestResponseBodyMethodProcessor`的`messageConverters`已经有了11个实现类,其中就包含我们自定义的消息转化器。 + +![](http://img.topjavaer.cn/img/202311262228331.png) + + + +同样,我们的Controller层依然可以正常接收到参数: + +![](http://img.topjavaer.cn/img/202311262228197.png) + + + +## 4. @ResponseBody + +### 4.1 源码分析 + +前面我们看的都是参数解析器的源码,现在我们看下返回值处理器的源码,从我们使用最频繁的`@ResponseBody`说起,很多同学知道的是`@ResponseBody`会将我们返回给请求的对象转为JSON写出到HTTP响应。现在我们来看这一功能是如何实现的。 + +我们知道在执行完HandlerMethod拿到返回值的时候,SpringMVC会使用返回值处理器来处理返回值: + +```java +this.returnValueHandlers.handleReturnValue( + returnValue, getReturnValueType(returnValue), mavContainer, webRequest); +``` + +而处理返回的源码为: + +```java +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); +} + + +private HandlerMethodReturnValueHandler selectHandler(@Nullable Object value, MethodParameter returnType) { + boolean isAsyncValue = isAsyncReturnValue(value, returnType); + for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { + if (isAsyncValue && !(handler instanceof AsyncHandlerMethodReturnValueHandler)) { + continue; + } + if (handler.supportsReturnType(returnType)) { + return handler; + } + } + return null; +} +``` + +这些内容我们之前都看过,策略模式,选择一个能处理的`HandlerMethodReturnValueHandler`进行处理。 + +为了源码分析的顺序,我们依然举个例子: + +编写后端: + +```java +@RestController +public class ResponseBodyController { + @GetMapping("/user/3") + public User getUser(){ + User user = new User(); + user.setName("Tom"); + user.setAge(18); + Pet pet = new Pet(); + pet.setName("myDog"); + pet.setAge(2); + user.setPet(pet); + return user; + } +} +``` + +通过PostMan发送请求: + +![](http://img.topjavaer.cn/img/202311262229456.png) + + + +可以看到我们后端返回的是一个Java对象,前端拿到的是一个JSON。这肯定是SpringMVC帮我们做了处理,根据前面的源码,很容易打断点定位到能处理这个返回的是`RequestResponseBodyMethodProcessor`返回值处理器(是的,又是它,我们在看`@RequestBody`注解源码的时候也是它,它既是参数解析器也是返回值处理器)。 + +#### 4.1.1 RequestResponseBodyMethodProcessor + +同样,我们先看它为什么能处理这个返回,再看它是如何处理返回的: + +`supportsReturnType()`源码如下: + +```java +@Override +public boolean supportsReturnType(MethodParameter returnType) { + return (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || + returnType.hasMethodAnnotation(ResponseBody.class)); +} +``` + +可以看到就是判断方法所在的类上是否包含`@ResponseBody`注解和方法本身上是否包含`@ResponseBody`注解,由于`@RestController`是个复合注解,由`@Controller`与`@ResponseBody`组成,因此我们的返回值可以被`RequestResponseBodyMethodProcessor`处理。下面我们就看下`RequestResponseBodyMethodProcessor`是如何处理返回值的: + +```java +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); + + // Try even with null return value. ResponseBodyAdvice could get involved. + writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage); +} +``` + +这里会走进`writeWithMessageConverters()`函数: + +`writeWithMessageConverters()`的源码会相对比较多且复杂,在讲解源码前我们需要先讲一个东西叫**内容协商**。 + +#### 4.1.2 内容协商 + +我们之前在HTTP请求头中已经看到了**content-type**信息,这代表请求方会告诉服务端自己发来的数据是什么类型的数据,除此以外,HTTP还有另一个重要信息**accept**。 + +在发送HTTP请求的时候,浏览器会告诉服务器自己**支持**哪种数据的返回,这一信息会放在HTTP请求头的Accept字段。比如我们上面的Post表单提交,其Accept信息是: + +![](http://img.topjavaer.cn/img/202311262229278.png) + +上述代表当前浏览器可以接收(逗号分隔) + +- text/html +- application/xhtml+xml +- application/xml;q=0.9 +- image/webp +- image/apng +- */*;q=0.8 +- application/signed-exchange;v=b3;q=0.9 + +这些类型的返回,其中q代表权重(关于权重我们一会再说)。 + +但是我们的**服务器往往也需要判断自己能够返回哪些类型,然后对服务器能返回的且浏览器能接收的这两个类型集合做交集,得到的结果就是服务器可以返回给浏览器的数据类型,这就是内容协商**。 + +#### 4.1.3 writeWithMessageConverters() + +下面我们看下`writeWithMessageConverters()`的部分源码: + +```java +protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, + ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { + + Object body; + Class valueType; + Type targetType; + + //... + //得到返回结果的值和返回结果的类型 + body = value; + valueType = getReturnValueType(body, returnType); + targetType = GenericTypeResolver.resolveType(getGenericType(returnType), returnType.getContainingClass()); + + //一些IO的处理... + + //首先说明MediaType类型翻译过来是媒体类型,其实就是我们前面说的Http Accept和Content-Type里写的那些东西 + //也就是我们前面说的返回给(浏览器支持的)Http的数据类型,下面有很多MediaType 不再赘述 + //selectedMediaType就是被选中的那个输出类型 + MediaType selectedMediaType = null; + //判断用户是否自己设置了content-type,所谓自己设置了content-type也即我们自己指定的输出的mediaType + MediaType contentType = outputMessage.getHeaders().getContentType(); + boolean isContentTypePreset = contentType != null && contentType.isConcrete(); + if (isContentTypePreset) { + //... 日志 + //如果自己设置了输出的content-type就按用户自己设置的来,否则才内容协商 + selectedMediaType = contentType; + } + else { + //这整个else都是内容协商,目的就是找到需要返回的mediaType + HttpServletRequest request = inputMessage.getServletRequest(); + //acceptableTypes是浏览器可以接收的数据类型,也即从请求头里的Accept解析出的内容 + List acceptableTypes; + try { + //解析并获得浏览器可以接收的数据类型 + acceptableTypes = getAcceptableMediaTypes(request); + } + // ...异常处理 + + //这里是获得服务器可以输出的数据类型 + List producibleTypes = getProducibleMediaTypes(request, valueType, targetType); + + //...异常处理 + + //mediaTypesToUse是producibleTypes和acceptableType的交集,也即内容协商的结果 + //两层for循环遍历将两边都支持的数据类型放入mediaTypesToUse + List mediaTypesToUse = new ArrayList<>(); + for (MediaType requestedType : acceptableTypes) { + for (MediaType producibleType : producibleTypes) { + if (requestedType.isCompatibleWith(producibleType)) { + mediaTypesToUse.add(getMostSpecificMediaType(requestedType, producibleType)); + } + } + } + //...找不到可用media的异常处理 + + //按权重排序 + MediaType.sortBySpecificityAndQuality(mediaTypesToUse); + //遍历交集,从中确定一个输出的数据类型 + for (MediaType mediaType : mediaTypesToUse) { + if (mediaType.isConcrete()) { + selectedMediaType = mediaType; + break; + } + else if (mediaType.isPresentIn(ALL_APPLICATION_MEDIA_TYPES)) { + selectedMediaType = MediaType.APPLICATION_OCTET_STREAM; + break; + } + } + //... + } + //得到Http确定的输出类型后,开始将我们的返回值转化为这种输出类型 + //下面的代码我们一会再说 + if (selectedMediaType != null) { + selectedMediaType = selectedMediaType.removeQualityValue(); + for (HttpMessageConverter converter : this.messageConverters) { + GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? + (GenericHttpMessageConverter) converter : null); + if (genericConverter != null ? + ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : + converter.canWrite(valueType, selectedMediaType)) { + body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, + (Class>) converter.getClass(), + inputMessage, outputMessage); + if (body != null) { + Object theBody = body; + LogFormatUtils.traceDebug(logger, traceOn -> + "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); + addContentDispositionHeader(inputMessage, outputMessage); + if (genericConverter != null) { + genericConverter.write(body, targetType, selectedMediaType, outputMessage); + } + else { + ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Nothing to write: null body"); + } + } + return; + } + } + } + + if (body != null) { + Set producibleMediaTypes = + (Set) inputMessage.getServletRequest() + .getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + + if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) { + throw new HttpMessageNotWritableException( + "No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'"); + } + throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass())); + } +} +``` + +可以看到,得到selectedMediaType,也即得到了确定的Http输出类型(MediaType)后,就该将我们的输出值转为这个对应的类型数据了,但现在问题来了,要怎样转化呢? + +`AbstractMessageConverterMethodArgumentResolver`(`RequestResponseBodyMethodProcessor`的父类)内部有一个属性叫 + +```java +protected final List> messageConverters; +``` + +`HttpMessageConverter`的集合,这个接口我们上面讲过了HTTP消息转化器,但上面说的是将HTTP消息转为我们的Java对象,这里的作用是将Java对象转为我们的HTTP消息。对于`HttpMessageConverter`而言,HTTP转Java属于read,Java转HTTP属于write。 + +`HttpMessageConverter`接口源码如下: + +```java +public interface HttpMessageConverter { + + boolean canRead(Class clazz, @Nullable MediaType mediaType); + + boolean canWrite(Class clazz, @Nullable MediaType mediaType); + + List getSupportedMediaTypes(); + + default List getSupportedMediaTypes(Class clazz) { + return (canRead(clazz, null) || canWrite(clazz, null) ? + getSupportedMediaTypes() : Collections.emptyList()); + } + + T read(Class clazz, HttpInputMessage inputMessage) + throws IOException, HttpMessageNotReadableException; + + void write(T t, @Nullable MediaType contentType, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException; +} +``` + +其中`canRead`和`canWrite()`代表是否支持读取和写入。`read()`和`write()`代表进行读取和写入。 + +在默认情况下,`messageConverters`内有10个已经初始化的`HttpMessageConverter`(我们之前已经看到过了): + +![](http://img.topjavaer.cn/img/202311262229615.png) + + + +它们的功能通过名字也很容易看出来,比如`ByteArrayHttpMessageConvert`是将byte数组转为Http数据输出,`MappingJackson2HttpMessageConverter`是将Java对象转为JSON然后通过HTTP输出。 + +我们的返回处理器会挨个遍历这10个消息转化器,询问它们是否支持写入,如果支持写入,那么就用这个消息处理器来写入,对应的源码便是刚才的下半部分: + +```java +protected void writeWithMessageConverters(@Nullable T value, MethodParameter returnType, + ServletServerHttpRequest inputMessage, ServletServerHttpResponse outputMessage) + throws IOException, HttpMediaTypeNotAcceptableException, HttpMessageNotWritableException { + //...上面的源码我们已经说过了,这里不再贴出 + + //得到Http确定的输出类型后,开始将我们的返回值转化为这种输出类型 + if (selectedMediaType != null) { + //移除权重 + selectedMediaType = selectedMediaType.removeQualityValue(); + //遍历10个消息转化器 + for (HttpMessageConverter converter : this.messageConverters) { + GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? + (GenericHttpMessageConverter) converter : null); + //判断它们是否支持写入 + if (genericConverter != null ? + ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : + converter.canWrite(valueType, selectedMediaType)) { + body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, + (Class>) converter.getClass(), + inputMessage, outputMessage); + if (body != null) { + Object theBody = body; + LogFormatUtils.traceDebug(logger, traceOn -> + "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); + addContentDispositionHeader(inputMessage, outputMessage); + //如果支持写入就调用write方法写入(写入到Http的响应体中) + if (genericConverter != null) { + genericConverter.write(body, targetType, selectedMediaType, outputMessage); + } + else { + ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Nothing to write: null body"); + } + } + return; + } + } + } + + if (body != null) { + Set producibleMediaTypes = + (Set) inputMessage.getServletRequest() + .getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + + if (isContentTypePreset || !CollectionUtils.isEmpty(producibleMediaTypes)) { + throw new HttpMessageNotWritableException( + "No converter for [" + valueType + "] with preset Content-Type '" + contentType + "'"); + } + throw new HttpMediaTypeNotAcceptableException(getSupportedMediaTypes(body.getClass())); + } +} +``` + +通过上面说的内容协商,我们已经知道`selectedMediaType`是**application/json**。然后打断点会得到`MappingJackson2HttpMessageConverter`消息转化器支持处理我们的返回结果。我们这里自然需要看两点内容了,为什么它支持写出,它又是如何写出的。 + +#### 4.1.4 MappingJackson2HttpMessageConverter + +`canWrite()`源码如下: + +```java +public boolean canWrite(Class clazz, @Nullable MediaType mediaType) { + if (!canWrite(mediaType)) { + return false; + } + if (mediaType != null && mediaType.getCharset() != null) { + Charset charset = mediaType.getCharset(); + if (!ENCODINGS.containsKey(charset.name())) { + return false; + } + } + ObjectMapper objectMapper = selectObjectMapper(clazz, mediaType); + if (objectMapper == null) { + return false; + } + AtomicReference causeRef = new AtomicReference<>(); + if (objectMapper.canSerialize(clazz, causeRef)) { + return true; + } + logWarningIfNecessary(clazz, causeRef.get()); + return false; +} +``` + +可以看到与`canRead()`代码基本相同,就是判断要写出的mediaType自己是否支持,以及要写出的对象是否可以序列化。 + +`write()`源码如下: + +```java +public final void write(final T t, @Nullable final Type type, @Nullable MediaType contentType, + HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + + final HttpHeaders headers = outputMessage.getHeaders(); + addDefaultHeaders(headers, t, contentType); + //... + //调用子类的writeInternal() + writeInternal(t, type, outputMessage); + //通过输出流写出 + outputMessage.getBody().flush(); +} + +protected void writeInternal(Object object, @Nullable Type type, HttpOutputMessage outputMessage) + throws IOException, HttpMessageNotWritableException { + + //得到输出的mediaType和编码格式 + MediaType contentType = outputMessage.getHeaders().getContentType(); + JsonEncoding encoding = getJsonEncoding(contentType); + + //得到返回的对象类型 + Class clazz = (object instanceof MappingJacksonValue ? + ((MappingJacksonValue) object).getValue().getClass() : object.getClass()); + //拿到ObjectMapper + ObjectMapper objectMapper = selectObjectMapper(clazz, contentType); + + //... + + //拿到HTTP请求输出流 + OutputStream outputStream = StreamUtils.nonClosing(outputMessage.getBody()); + + //下面的内容都是使用jackson将参数写出 + //笔者不太熟悉jackson的api,因此只能写到这里 + try (JsonGenerator generator = objectMapper.getFactory().createGenerator(outputStream, encoding)) { + writePrefix(generator, object); + + Object value = object; + Class serializationView = null; + FilterProvider filters = null; + JavaType javaType = null; + + if (object instanceof MappingJacksonValue) { + MappingJacksonValue container = (MappingJacksonValue) object; + value = container.getValue(); + serializationView = container.getSerializationView(); + filters = container.getFilters(); + } + if (type != null && TypeUtils.isAssignable(type, value.getClass())) { + javaType = getJavaType(type, null); + } + + ObjectWriter objectWriter = (serializationView != null ? + objectMapper.writerWithView(serializationView) : objectMapper.writer()); + if (filters != null) { + objectWriter = objectWriter.with(filters); + } + if (javaType != null && javaType.isContainerType()) { + objectWriter = objectWriter.forType(javaType); + } + SerializationConfig config = objectWriter.getConfig(); + if (contentType != null && contentType.isCompatibleWith(MediaType.TEXT_EVENT_STREAM) && + config.isEnabled(SerializationFeature.INDENT_OUTPUT)) { + objectWriter = objectWriter.with(this.ssePrettyPrinter); + } + objectWriter.writeValue(generator, value); + + writeSuffix(generator, object); + generator.flush(); + } + //异常处理... +} +``` + +核心思想还是拿到jackson的ObjectMapper将Java对象转为JSON再写出到HTTP输出流。 + +### 4.2 XML + +看源码的过程中我们其实是看到SpringMVC是包含有xml消息转化器的,但xml消息转化器要想生效,还需要导入一个依赖包。 + +```xml + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + +``` + +此时我们的Controller层方法不变 + +```java +@PostMapping("/user") +public User postUser(@RequestBody User user){ + return user; +} +``` + +但这时的Http响应体返回为: + +![](http://img.topjavaer.cn/img/202311262230595.png) + +可以看到是一个xml类型的数据。 + +我们之前说过,内容协商是根据浏览器能处理的请求和我们服务器能返回的请求共同再根据权重排序共同得出来的结果。上面的浏览器请求中,我们返回的是`application/json`格式的数据,这是匹配到的浏览器的`*/*`类型,但这种类型只有0.8的权重,如果我们的服务器支持xml类型的返回结果,`application/xml;q=0.9`是0.9的权重,此时就会按xml转化返回。 + +并且由于我们支持xml类型的输出了,因此在内容协商的时候得到了服务端更多可支持的输出类型。 + +`getProducibleMediaTypes()`获得服务端支持的输出类型由之前的4种: + +```java +List producibleTypes = getProducibleMediaTypes(request, valueType, targetType); +``` + + + +![](http://img.topjavaer.cn/img/202311262230521.png) + + + +变为了7种(虽然是10个,但有3个重复的) + +![](http://img.topjavaer.cn/img/202311262230388.png) + + + +多出来的`application/xml`优先级肯定高于之前的`application/json`这种,因此SpringBoot会按xml去解析。 + +这时我们就要问一句,为什么多出来了几种解析方式,我们点进`getProducibleMediaTypes()`函数源码: + +```java +protected List getProducibleMediaTypes( + HttpServletRequest request, Class valueClass, @Nullable Type targetType) { + + Set mediaTypes = + (Set) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + if (!CollectionUtils.isEmpty(mediaTypes)) { + return new ArrayList<>(mediaTypes); + } + List result = new ArrayList<>(); + for (HttpMessageConverter converter : this.messageConverters) { + if (converter instanceof GenericHttpMessageConverter && targetType != null) { + if (((GenericHttpMessageConverter) converter).canWrite(targetType, valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + else if (converter.canWrite(valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result); +} +``` + +其源码思路比较简单,就是遍历每个`HttpMessageConverter`,判断其是否支持写入,如果支持写入的话,顺便也将其支持的mediaType拿到,所有支持写入的`HttpMessageConverter`对应的mediaType集合就是服务器支持的medisType。 + +不同的是由于xml解析依赖的导入,现在SpringBoot的`messageConverters`集合多了两种类型(同时也少了一个): + +![](http://img.topjavaer.cn/img/202311262230063.png) + +这俩是同一个类,都是`MappingJackson2XmlHttpMessageConverter`,它们是用于支持xml写出的,它们支持的mediaType为: + +```java +public MappingJackson2XmlHttpMessageConverter(ObjectMapper objectMapper) { + super(objectMapper, new MediaType("application", "xml", StandardCharsets.UTF_8), + new MediaType("text", "xml", StandardCharsets.UTF_8), + new MediaType("application", "*+xml", StandardCharsets.UTF_8)); + Assert.isInstanceOf(XmlMapper.class, objectMapper, "XmlMapper required"); +} +``` + +共三个mediaType: + +- `application/xml;charset=UTF-8` +- `text/xml;charset=UTF-8` +- `application/*+xml;charset=UTF-8` + +也就是我们上面截图多出来的那三个。 + +根据排序后,由于xml优先级高于json,自然`selectMedia`就是`application/xhtml+xml` + +能处理`application/xhtml+xml`类型的消息解析器自然是新加进来的`MappingJackson2XmlHttpMessageConverter`。 + +在这里就可以看到虽然后端业务代码同样返回的是User对象,但由于HTTP请求Accept字段的不同,就可以解析为不同的格式,有人想要json就将Accept写为**application/json**,有人想要xml就将Accept写为**application/xhtml+xml** + +这一做法的应用场景还是非常多的,比如不同的客户端想要不同的数据结构,浏览器想要json格式,而app想要xml格式,客户端只需要在自己的请求头的accept字段修改接收的数据类型或给要接收的数据类型排较高权重即可实现自适应返回数据。 + +还有类似的应用场景,比如我们自己写一个消息转化器,将返回的结果转为excel表格作为导出。同一个查询接口,前端可以根据请求accepet的不同,将json设置最高,可以查回来json结构直接展示。也可以将Accept设置为excel文件(自定义的mediaType),此时后端就会使用自定义消息转化器按文件输出。 + +刚才我们说了请求端可以修改accept的属性来决定返回类型,但有时修改accept会比较麻烦,比如对于表单提交。SpringBoot针对这种情况给出了可以通过请求参数来获得客户端想返回的数据类型,通过属性`spring.mvc.contentnegotiation.favorParameter`来开启这一功能 + +```yaml +spring: + mvc: + content negotiation: + favor-parameter: true +``` + +此时我们只需要在请求参数中加上format参数,即可指定客户端想要的数据类型,如: + +`ip:port/user?format=xml` 代表以xml形式返回 + +`ip:port/user?format=json`代表以json形式返回 + +### 4.3 为什么导入XML依赖就多了xml的消息转化器 + +这里再补充一个细节,我们之前看到,在项目启动的时候,`messageConverters`内就已经加载了很多实现好的消息转化器,并且当我们导入`jackson-dataformat-xml`依赖时,又会自动增加xml的消息转化器,这是怎么做到的? + +首先根据SpringBoot的自动装配我们知道,有关Spring Mvc的所有装配都在`WebMvcAutoConfiguration`下,在这个类下。在这个类下有一个继承自`WebMvcConfigurer`的方法 + +```java +@Override +public void configureMessageConverters(List> converters) { + this.messageConvertersProvider + .ifAvailable((customConverters) -> converters.addAll(customConverters.getConverters())); +} +``` + +在这里SpringBoot装配进了一些消息转化器,其中`customConverters.getConverters()`源码为: + +```java +public class HttpMessageConverters implements Iterable> { + //... + public List> getConverters() { + return this.converters; + } + //... +} +``` + +这里的`converters`属性是在构造方法中传进来的,也即一开始new的时候构造好的,构造方法如下: + +```java +public class HttpMessageConverters implements Iterable> { + //... + + public HttpMessageConverters(HttpMessageConverter... additionalConverters) { + this(Arrays.asList(additionalConverters)); + } + public HttpMessageConverters(Collection> additionalConverters) { + this(true, additionalConverters); + } + public HttpMessageConverters(boolean addDefaultConverters, Collection> converters) { + List> combined = getCombinedConverters(converters, + addDefaultConverters ? getDefaultConverters() : Collections.emptyList()); + combined = postProcessConverters(combined); + this.converters = Collections.unmodifiableList(combined); + } + //... +} +``` + +可以看到构造方法会执行一个叫`getDefaultConverters()`方法,这个方法会获得默认的`HttpMessageConverter` + +```javascript +public class HttpMessageConverters implements Iterable> { + //... + private List> getDefaultConverters() { + List> converters = new ArrayList<>(); + if (ClassUtils.isPresent("org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport",null)) { + converters.addAll(new WebMvcConfigurationSupport() { + public List> defaultMessageConverters() { + return super.getMessageConverters(); + } + }.defaultMessageConverters()); + } else { + converters.addAll(new RestTemplate().getMessageConverters()); + } + reorderXmlConvertersToEnd(converters); + return converters; + } + //... +} +``` + +其中`getDefaultConverters()`又会调用`super.getMessageConverters();` + +```java +public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware { + //... + protected final List> getMessageConverters() { + if (this.messageConverters == null) { + this.messageConverters = new ArrayList<>(); + configureMessageConverters(this.messageConverters); + if (this.messageConverters.isEmpty()) { + addDefaultHttpMessageConverters(this.messageConverters); + } + extendMessageConverters(this.messageConverters); + } + return this.messageConverters; + } + //... +} +``` + +上面的代码又会调用`addDefaultHttpMessageConverters(this.messageConverters);` + +```java +public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware { + //... + protected final void addDefaultHttpMessageConverters(List> messageConverters) { + messageConverters.add(new ByteArrayHttpMessageConverter()); + messageConverters.add(new StringHttpMessageConverter()); + messageConverters.add(new ResourceHttpMessageConverter()); + messageConverters.add(new ResourceRegionHttpMessageConverter()); + if (!shouldIgnoreXml) { + try { + messageConverters.add(new SourceHttpMessageConverter<>()); + } + catch (Throwable ex) { + // Ignore when no TransformerFactory implementation is available... + } + } + messageConverters.add(new AllEncompassingFormHttpMessageConverter()); + + if (romePresent) { + messageConverters.add(new AtomFeedHttpMessageConverter()); + messageConverters.add(new RssChannelHttpMessageConverter()); + } + + if (!shouldIgnoreXml) { + if (jackson2XmlPresent) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build())); + } + else if (jaxb2Present) { + messageConverters.add(new Jaxb2RootElementHttpMessageConverter()); + } + } + + if (kotlinSerializationJsonPresent) { + messageConverters.add(new KotlinSerializationJsonHttpMessageConverter()); + } + if (jackson2Present) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.json(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2HttpMessageConverter(builder.build())); + } + else if (gsonPresent) { + messageConverters.add(new GsonHttpMessageConverter()); + } + else if (jsonbPresent) { + messageConverters.add(new JsonbHttpMessageConverter()); + } + + if (jackson2SmilePresent) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.smile(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2SmileHttpMessageConverter(builder.build())); + } + if (jackson2CborPresent) { + Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.cbor(); + if (this.applicationContext != null) { + builder.applicationContext(this.applicationContext); + } + messageConverters.add(new MappingJackson2CborHttpMessageConverter(builder.build())); + } + } + //... +} +``` + +从上面的代码可以看到默认情况下导入了`ByteArrayHttpMessageConverter`、`StringHttpMessageConverter`、`ResourceHttpMessageConverter`、`ResourceRegionHttpMessageConverter`、`AllEncompassingFormHttpMessageConverter`,这些我们都在之前见到了,还有一些需要根据条件判断是否应该导入的,比如`MappingJackson2HttpMessageConverter`、`MappingJackson2XmlHttpMessageConverter`。 + +我们之前是导入了xml解析包,就自动添加了`MappingJackson2XmlHttpMessageConverter`消息转化器,这是因为此时`jackson2XmlPresent`属性为true,而`jackson2XmlPresent`属性的判断逻辑是: + +```java +jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader); +``` + +很简单,就是判断类`com.fasterxml.jackson.dataformat.xml.XmlMapper`是否存在,如果存在`jackson2XmlPresent`就为true,从而`MappingJackson2XmlHttpMessageConverter`就会被创建和加载。 + +### 4.4 自定义HttpMessageConverter + +我们在上一章`@RequestBody`中已经讲了自定义HttpMessageConverter,那时我们自定义了一个消息转化器,只不过它是用来解析HTTP请求的,现在我们需要自定义一个解析器是处理返回HTTP响应的。我们可以看下之前定的自定义消息转化器: + +```java +static class MyHttpMessageConverter implements HttpMessageConverter{ + private final MediaType mediaType = new MediaType("application","dabin"); + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + //先不关心写 + return false; + } + + @Override + public List getSupportedMediaTypes() { + //先不关心 + return null; + } + + @Override + public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + User user = new User(); + + int available = inputMessage.getBody().available(); + byte[] bytes = new byte[available]; + inputMessage.getBody().read(bytes); + String[] split = new String(bytes).split(","); + user.setName(split[0]); + user.setAge(Integer.parseInt(split[1])); + Pet pet = new Pet(); + pet.setName(split[2]); + pet.setAge(Integer.parseInt(split[3])); + user.setPet(pet); + return user; + } + + @Override + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + //先不关心写 + } +} +``` + +上面的`canWrite()`、`write()`和`getSupportedMediaTypes()`都是写出的时候需要实现的,我们现在就来实现: + +同样我们自定义一种mediaType叫`application/dabin`,然后自定义消息转化器,将Java对象按`application/dabin`的格式写出: + +```java +static class MyHttpMessageConverter implements HttpMessageConverter{ + private final MediaType mediaType = new MediaType("application","dabin"); + + @Override + public boolean canRead(Class clazz, MediaType mediaType) { + return this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public boolean canWrite(Class clazz, MediaType mediaType) { + return mediaType== null ||this.mediaType.includes(mediaType) && clazz == User.class; + } + + @Override + public List getSupportedMediaTypes() { + return Collections.singletonList(mediaType); + } + + @Override + public Object read(Class clazz, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException { + User user = new User(); + + int available = inputMessage.getBody().available(); + byte[] bytes = new byte[available]; + inputMessage.getBody().read(bytes); + String[] split = new String(bytes).split(","); + user.setName(split[0]); + user.setAge(Integer.parseInt(split[1])); + Pet pet = new Pet(); + pet.setName(split[2]); + pet.setAge(Integer.parseInt(split[3])); + user.setPet(pet); + return user; + } + + @Override + public void write(Object o, MediaType contentType, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException { + if(o instanceof User){ + User user = (User) o; + String stringBuilder = user.getName() + "," + + user.getAge() + "," + + user.getPet().getName() + "," + + user.getPet().getAge(); + outputMessage.getBody().write(stringBuilder.getBytes()); + } + } +} +``` + +同理,加这个convert加入到`HttpMessageConverters`中: + +```java +@Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void extendMessageConverters(List> converters) { + converters.add(new MyHttpMessageConverter()); + } + } +} +``` + +我们依然使用`application/dabin`的content-type方式提交,但同时将Accept也设置为`application/dabin`: + +此时,我们的返回结果为 + +![](http://img.topjavaer.cn/img/202311262232506.png) + +可以看到数据确实按照我们想要的结果类型返回了,也就是根据我们能处理的MedisType走到了我们自定义的消息转化器。 + +如果不想修改Http头的Accept信息,而是像我们之前那样,从参数中的format来决定Media类型,之前通过yml配置开启的方式在这种情况下已经不适用,因为那种方式只支持format=json和format=xml两种类型,我们现在想要类似于format=dabin这种类型,此时就需要自定义内容协商策略。 + +```java + @Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + HashMap map = new HashMap<>(4); + map.put("dabin",new MediaType("application","dabin")); + ParameterContentNegotiationStrategy myContentNegotiationStrategy = new ParameterContentNegotiationStrategy(map); + configurer.strategies(Collections.singletonList(myContentNegotiationStrategy)); + } + } +} +``` + +这种情况下,我们就可以通过`ip:port/user?format=dabin`的形式走到自定义消息转化器了 + +但这种方式有个问题,就是我们自定义的消息转化器会覆盖了SpringBoot自带的消息转化器,那么此时在协议头中的Accept等信息都无法处理了,这肯定不是我们想看到的,一种比较危险的办法是我们自己再把它new出来加进去: + +```java +@Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + HashMap map = new HashMap<>(4); + map.put("dabin",new MediaType("application","dabin")); + ParameterContentNegotiationStrategy myContentNegotiationStrategy = new ParameterContentNegotiationStrategy(map); + HeaderContentNegotiationStrategy headerContentNegotiationStrategy = new HeaderContentNegotiationStrategy(); + ArrayList list = new ArrayList<>(); + list.add(myContentNegotiationStrategy); + list.add(headerContentNegotiationStrategy); + configurer.strategies(list); + } + } +} +``` + +还一种比较简单,没有心里负担的做法: + +```java +@Bean +public WebMvcConfigurer webMvcConfigurer(){ + return new WebMvcConfigurer() { + @Override + public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { + configurer.mediaType("dabin",new MediaType("application","dabin")); + } + } +} +``` + +这种情况也是可以的,不过需要我们开启`spring.mvc.contentnegotiation.favorParameter` + +这种就是在默认的`ParameterContentNegotiationStrategy`中添加支持的mediaType,在原来支持的xml和json里又加入dabin。 + +![](http://img.topjavaer.cn/img/202311262232459.png) + +还有一种更简单的方案,`ContentNegotiationConfigurer`中的`mediaTypes`支持yml配置,也即我们只需要: + +```java +spring: + mvc: + content negotiation: + favor-parameter: true + media-types: {dabin: application/dabin} +``` + +即可。 + +### 4.5 源码优化 + +在前面的源码分析中,其实是可以看到一个SpringMVC的优化点的,在说如何优化前我们先说回内容协商: + +内容协商的时候,会执行 + +```java +List producibleTypes = getProducibleMediaTypes(request, valueType, targetType); +``` + +语句,这条语句会得到当前服务器端支持返回的mediaType。其源码如下: + +```java +protected List getProducibleMediaTypes( + HttpServletRequest request, Class valueClass, @Nullable Type targetType) { + + Set mediaTypes = + (Set) request.getAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); + if (!CollectionUtils.isEmpty(mediaTypes)) { + return new ArrayList<>(mediaTypes); + } + List result = new ArrayList<>(); + for (HttpMessageConverter converter : this.messageConverters) { + if (converter instanceof GenericHttpMessageConverter && targetType != null) { + if (((GenericHttpMessageConverter) converter).canWrite(targetType, valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + else if (converter.canWrite(valueClass, null)) { + result.addAll(converter.getSupportedMediaTypes(valueClass)); + } + } + return (result.isEmpty() ? Collections.singletonList(MediaType.ALL) : result); +} +``` + +可以看到就是遍历所有的`HttpMessageConverter`实例,判断其是否支持将当前返回值写出,如果支持就将这个`HttpMessageConverter`对应的mediaType记录起来,然后汇总返回,这个汇总结果就是当前服务器端支持的返回类型。 + +在内容协商后,我们拿到了要输出的mediaType理论上就该使用`HttpMessageConverter`将信息写出,此时SpringMVC的做法如下: + +```java +for (HttpMessageConverter converter : this.messageConverters) { + GenericHttpMessageConverter genericConverter = (converter instanceof GenericHttpMessageConverter ? + (GenericHttpMessageConverter) converter : null); + if (genericConverter != null ? + ((GenericHttpMessageConverter) converter).canWrite(targetType, valueType, selectedMediaType) : + converter.canWrite(valueType, selectedMediaType)) { + body = getAdvice().beforeBodyWrite(body, returnType, selectedMediaType, + (Class>) converter.getClass(), + inputMessage, outputMessage); + if (body != null) { + Object theBody = body; + LogFormatUtils.traceDebug(logger, traceOn -> + "Writing [" + LogFormatUtils.formatValue(theBody, !traceOn) + "]"); + addContentDispositionHeader(inputMessage, outputMessage); + if (genericConverter != null) { + genericConverter.write(body, targetType, selectedMediaType, outputMessage); + } + else { + ((HttpMessageConverter) converter).write(body, selectedMediaType, outputMessage); + } + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Nothing to write: null body"); + } + } + return; + } +} +``` + +依然是遍历所有的`HttpMessageConverter`,判断其是否支持写出,支持再调用写出函数转化和写出结果。 + +其实这里很多人已经看明白了,上一步内容协商的时候已经遍历过所有的`HttpMessageConverter`了,这其中有些是支持写出,有些是不支持的,将支持写出的`HttpMessageConverter`对应的mediaType汇总起来。再从这些汇总后候选的mediaType中选出一个合适的mediaType作为写出类型,再选择一个能处理这个mediaType的`HttpMessageConverter`写出。这时第二遍就不需要再遍历所有的mediaType,直接遍历第一遍支持写出的`HttpMessageConverter`结果就可以了,相当于第一遍做个初筛选,第二遍做可以在初筛选的结果上再遍历得到具体的那个`HttpMessageConverter`做转化,而无需在第二次时再遍历所有的mediaType。甚至如果mediaType与`HttpMessageConverter`具有映射关系,可以将第一步的初筛结果转为map,key是支持的mediaType,value是`HttpMessageConverter`,内容协商后可以直接通过key拿到`HttpMessageConverter`,时间复杂度O(1),无需二次遍历。 \ No newline at end of file diff --git a/docs/source/spring-mvc/4-fileupload-interceptor.md b/docs/source/spring-mvc/4-fileupload-interceptor.md new file mode 100644 index 0000000..285e5a2 --- /dev/null +++ b/docs/source/spring-mvc/4-fileupload-interceptor.md @@ -0,0 +1,631 @@ +--- +sidebar: heading +title: Spring MVC源码分析 +category: 源码分析 +tag: + - Spring MVC +head: + - - meta + - name: keywords + content: Spring MVC面试题,Spring MVC源码解析,文件上传,拦截器,MVC模式,Spring MVC工作原理,Spring MVC常用注解,Spring MVC异常处理,Spring MVC拦截器 + - - meta + - name: description + content: 高质量的Spring MVC常见知识点和面试题总结,让天下没有难背的八股文! +--- + +## 1. 文件上传 + +我们先从文件上传说起,相信大家都写过这样的代码: + +```java +@PostMapping("/file") +public String upload(MultipartFile file){ + //... + return "ok"; +} +``` + +其中参数`MultipartFile`是SpringMVC为我们提供的,SpringMVC会将HTTP请求传入的文件转为`MultipartFile`对象,我们直接操作这个对象即可。但我们不禁要问,SpringMVC是如何做的呢?在文件上传的时候,SpringMVC帮我们做了哪些事情呢?要回答这个问题还是得回到梦开始的地方:`DispatcherServlet#doDispatch()` + +我们之前已经在前面两篇文章讲了`DispatcherServlet#doDispatch()`的大体流程,这里不妨再拿过来,不过这里我们需要细化一点关于文件上传的东西: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + //文件上传的处理 + boolean multipartRequestParsed = false; + processedRequest = checkMultipart(request); + multipartRequestParsed = (processedRequest != request); + + ModelAndView mv = null; + //找到处理方法 + mappedHandler = getHandler(processedRequest); + + //... + + //找到处理方法的适配器 + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //解析参数,执行方法,并处理方法的返回 + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + + //视图解析 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + + //... + + //文件资源的释放 + if (multipartRequestParsed) { + cleanupMultipart(processedRequest); + } +} +``` + +可以看到,这里面最核心的代码是`checkMultipart(request)`,我们不妨点进源码: + +```java +protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException { + if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) { + //... + return this.multipartResolver.resolveMultipart(request); + //... + } + // If not returned before: return original request. + return request; +} +``` + +首先`multipartResolver`就是文件解析器,`DispatcherServlet`对象持有`multipartResolver`属性,if语句会先判断当前HTTP请求是否是文件,如果是文件则用文件解析器来解析HTTP请求。 + +```java +public class DispatcherServlet extends FrameworkServlet { + //... + @Nullable + private MultipartResolver multipartResolver; + //... +} +``` + +其中`multipartResolver`的具体实现类为`StandardServletMultipartResolver`,因此`StandardServletMultipartResolver`的`isMultipart()`判断内容如下: + +```java +public boolean isMultipart(HttpServletRequest request) { + return StringUtils.startsWithIgnoreCase(request.getContentType(), + (this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/")); +} +``` + +简单来讲就是判断当前HTTP请求的`contentType`是否是以`multipart/`开头的(严格模式下是以`multipart/form-data`开头),我们知道,如果前端传文件,则HTTP请求的请求往往是`multipart/form-data`,因此 如果请求的参数包含文件,这个if语句肯定是判断为true的 + +再往下走就是使用文件解析器来解析HTTP请求,也即: + +```java +return this.multipartResolver.resolveMultipart(request); +``` + +很明显,上面的代码就是最核心的文件解析,`StandardServletMultipartResolver#resolveMultipart()`源码内容如下: + +```java +public MultipartHttpServletRequest resolveMultipart(HttpServletRequest request) throws MultipartException { + return new StandardMultipartHttpServletRequest(request, this.resolveLazily); +} +``` + + + +```java +public StandardMultipartHttpServletRequest(HttpServletRequest request, boolean lazyParsing) + throws MultipartException { + + super(request); + if (!lazyParsing) { + parseRequest(request); + } +} +``` + +其中`resolveLazily`默认是false,`resolveLazily`代表是否懒解析,也即是否到参数需要文件的时候再解析,默认为false。因此我们会在这里直接解析,解析的核心也很简单,就是将当前HTTP请求对象包装为一个更复杂的的HTTP请求对象:`StandardMultipartHttpServletRequest`,这个对象在我们原来的HTTP请求对象的基础上多了一个关键的属性:`multipartFiles` + +```java +public abstract class AbstractMultipartHttpServletRequest extends HttpServletRequestWrapper + implements MultipartHttpServletRequest { + + @Nullable + private MultiValueMap multipartFiles; + //... +} +``` + +`AbstractMultipartHttpServletRequest`是`StandardMultipartHttpServletRequest`的抽象父类。 + +`multipartFiles`属性是个`Map`,`Map`的key为文件名,value为`MultipartFile`对象,`MultipartFile`对象大家应该已经很熟悉了。补充一句的是这是个`MultiValueMap`,也即允许一个key对应多个value,也即允许多个同名的文件上传。 + +很明显我们也可以得出结论,所谓文件解析,就是构造那么一个`StandardMultipartHttpServletRequest`对象,并从当前HTP请求中将文件解出来赋值给`multipartFiles`属性,这样在`HandlerAdapter`做参数解析的时候就可以直接从`StandardMultipartHttpServletRequest`中拿出文件信息赋值给我们的参数了。 + +那么就让我们看下文件解析器是如何解析文件的: + +```java +private void parseRequest(HttpServletRequest request) { + try { + //从原始的HTTP请求中得到Part,Part其实就可以认为是文件对象了 + Collection parts = request.getParts(); + this.multipartParameterNames = new LinkedHashSet<>(parts.size()); + //files对象就是用来存储解析结果的 + MultiValueMap files = new LinkedMultiValueMap<>(parts.size()); + //遍历parts + for (Part part : parts) { + //下面三行代码是要拿到文件名 + String headerValue = part.getHeader(HttpHeaders.CONTENT_DISPOSITION); + ContentDisposition disposition = ContentDisposition.parse(headerValue); + String filename = disposition.getFilename(); + //如果文件名存在 + if (filename != null) { + //做一下文件名的特殊处理 + if (filename.startsWith("=?") && filename.endsWith("?=")) { + filename = MimeDelegate.decode(filename); + } + //将文件加入到files中,其中key为文件名,value就是StandardMultipartFile对象 + files.add(part.getName(), new StandardMultipartFile(part, filename)); + } + else { + this.multipartParameterNames.add(part.getName()); + } + } + //将files对象赋值给multipartFiles属性 + setMultipartFiles(files); + } + catch (Throwable ex) { + handleParseFailure(ex); + } +} +//做了一个map的不可修改 +protected final void setMultipartFiles(MultiValueMap multipartFiles) { + this.multipartFiles = + new LinkedMultiValueMap<>(Collections.unmodifiableMap(multipartFiles)); +} +``` + +很明显,最最核心的代码其实就是 + +```java +Collection parts = request.getParts(); +``` + +这句话其实就是从HTTP请求中解析出来`Part`对象,然后取Part对象的各种属性以及将它包装为`StandardMultipartFile`,最终就是赋值到`multipartFiles`属性上。 + +`request.getParts()`的内容非常长,也非常深,就不带大家一起看了,感兴趣的同学可以自己去看下,这里补充一句的是,有时我们对HTTP请求文件的一些设置,如 + +```yaml +spring: + servlet: + multipart: + max-file-size: 1GB +``` + +其实都是在这里校验的,如果不满足都是在这里抛出来的异常。 + +下面我们再看下参数解析器是如何将我们解析出来的`multipartFiles`属性赋值到我们的参数上的。 + +在我们一开始示例 + +```java +@PostMapping("/file") +public String upload(MultipartFile file){ + //... + return "ok"; +} +``` + +这种写法中,使用的参数解析器为`RequestParamMethodArgumentResolver`,那么我们必然就需要看下为什么是这个参数解析器,以及这个参数解析器是如何解析的: + +```java +public boolean supportsParameter(MethodParameter parameter) { + if (parameter.hasParameterAnnotation(RequestParam.class)) { + if (Map.class.isAssignableFrom(parameter.nestedIfOptional().getNestedParameterType())) { + RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class); + return (requestParam != null && StringUtils.hasText(requestParam.name())); + } + else { + return true; + } + } + else { + if (parameter.hasParameterAnnotation(RequestPart.class)) { + return false; + } + parameter = parameter.nestedIfOptional(); + if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { + return true; + } + else if (this.useDefaultResolution) { + return BeanUtils.isSimpleProperty(parameter.getNestedParameterType()); + } + else { + return false; + } + } +} +``` + +对于文件上传请求,代码会走入上述第16行代码,也即: + +```java +if (MultipartResolutionDelegate.isMultipartArgument(parameter)) { + return true; +} +``` + +判断我们当前的参数是否是一个`Multipart`参数,判断的方法如下: + +```java +public static boolean isMultipartArgument(MethodParameter parameter) { + Class paramType = parameter.getNestedParameterType(); + return (MultipartFile.class == paramType || + isMultipartFileCollection(parameter) || isMultipartFileArray(parameter) || + (Part.class == paramType || isPartCollection(parameter) || isPartArray(parameter))); +} +``` + +拿到参数的class,如果是`MultipartFile`类型,或者是`MultipartFile`集合类型,再或者是`MultipartFile`数组类型,亦或者是`Part`类型、`Part`集合,`Part`数组类型都可以。 + +很明显我们的参数是符合的,因此返回true,也就代表`RequestParamMethodArgumentResolver`可以解析这种情况,那我们再看下它是如何解析的: + +```java +public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, + NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception { + + + //... + + //拿到参数名 + Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name); + //... + + //按参数名解析 + Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest); + //... +} + + + +protected Object resolveName(String name, MethodParameter parameter, NativeWebRequest request) throws Exception { + HttpServletRequest servletRequest = request.getNativeRequest(HttpServletRequest.class); + + if (servletRequest != null) { + //按照文件的形式解析参数 + Object mpArg = MultipartResolutionDelegate.resolveMultipartArgument(name, parameter, servletRequest); + if (mpArg != MultipartResolutionDelegate.UNRESOLVABLE) { + return mpArg; + } + } + + //... +} +``` + +可以看到上面参数解析,最核心的内容便是`MultipartResolutionDelegate.resolveMultipartArgument()` + +```java +public static Object resolveMultipartArgument(String name, MethodParameter parameter, HttpServletRequest request) + throws Exception { + + //将当前HTTP请求按MultipartHttpServletRequest解析 + MultipartHttpServletRequest multipartRequest = + WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class); + //判断当前HTTP请求是真的是Multipart,也即传文件的 + //这里可能很多人有疑问,直接看前半部分multipartRequest != null不等于空不就行了吗,为什么还要后半部分 + //因为我们前面提到了懒解析,如果是懒解析,当前HTTP请求对象不是MultipartHttpServletRequest类型 + //但HTTP请求的contentType依然是multipart + boolean isMultipart = (multipartRequest != null || isMultipartContent(request)); + + //如果当前参数类型是MultipartFile类型 + if (MultipartFile.class == parameter.getNestedParameterType()) { + //但当前HTTP请求不是multipart,也即没传来文件,那直接返回null + if (!isMultipart) { + return null; + } + //当前HTTP请求没有解析成MultipartHttpServletRequest + //很明显这是懒解析的场景,因为上面的isMultipart为true才能走到这里,既然HTTP请求是文件请求,但没解析为 + //MultipartHttpServletRequest对象,那只能是懒解析,刚才没解析,那就这里解析好了 + if (multipartRequest == null) { + multipartRequest = new StandardMultipartHttpServletRequest(request); + } + //从MultipartHttpServletRequest中将文件取出来返回 + return multipartRequest.getFile(name); + } + //下面内容都是类似,不再赘述。 + else if (isMultipartFileCollection(parameter)) { + if (!isMultipart) { + return null; + } + if (multipartRequest == null) { + multipartRequest = new StandardMultipartHttpServletRequest(request); + } + List files = multipartRequest.getFiles(name); + return (!files.isEmpty() ? files : null); + } + else if (isMultipartFileArray(parameter)) { + if (!isMultipart) { + return null; + } + if (multipartRequest == null) { + multipartRequest = new StandardMultipartHttpServletRequest(request); + } + List files = multipartRequest.getFiles(name); + return (!files.isEmpty() ? files.toArray(new MultipartFile[0]) : null); + } + else if (Part.class == parameter.getNestedParameterType()) { + if (!isMultipart) { + return null; + } + return request.getPart(name); + } + else if (isPartCollection(parameter)) { + if (!isMultipart) { + return null; + } + List parts = resolvePartList(request, name); + return (!parts.isEmpty() ? parts : null); + } + else if (isPartArray(parameter)) { + if (!isMultipart) { + return null; + } + List parts = resolvePartList(request, name); + return (!parts.isEmpty() ? parts.toArray(new Part[0]) : null); + } + else { + return UNRESOLVABLE; + } +} +``` + + + +```java +public MultipartFile getFile(String name) { + return getMultipartFiles().getFirst(name); +} +protected MultiValueMap getMultipartFiles() { + if (this.multipartFiles == null) { + initializeMultipart(); + } + return this.multipartFiles; +} +``` + +这样,其实我们就把SpringMVC解析文件上传讲完了。 + +## 2. 拦截器 + +拦截器是我们在Spring MVC开发中使用的非常频繁的功能,如果我们需要做一些统一处理的时候往往就可以使用拦截器,比如鉴权,日志记录等。 + +在讲拦截器前我们先回顾一下Spring MVC的处理流程: + +![](https://coderzoe.oss-cn-beijing.aliyuncs.com/202212012203622.png) + + + +Spring MVC提供的拦截器接口如下: + +```java +public interface HandlerInterceptor { + default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + return true; + } + default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable ModelAndView modelAndView) throws Exception { + } + + default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, + @Nullable Exception ex) throws Exception { + } + +} +``` + +拦截器里的这些方法都切入到MVC的处理过程中: + +![](https://coderzoe.oss-cn-beijing.aliyuncs.com/202212012209775.png) + +也即`preHandle`的切入点是在获得适配器以后但实际执行请求处理以前,`postHandle`是在实际执行处理之后但在渲染返回结果之前,`afterCompletion`是在渲染返回结果之后,实际返回给请求方之前。 + +> **注:上图的拦截器切入流程是不准确的,因为很多时候拦截器是可以短路(拦截)整个处理的,上图我们只是画出了正常情况下的处理,对于更通用的情况会在我们看源码的时候详细的说。** + +它们对应的源码内容如下: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + //... + + ModelAndView mv = null; + mappedHandler = getHandler(processedRequest); + + //... + + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + //... + + //拦截器 preHandle方法的执行 + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + //... + //拦截器 postHandle方法的执行 + mappedHandler.applyPostHandle(processedRequest, response, mv); + //... + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + //拦截器 afterCompletion的执行 + //这里的顺序虽然是对的,但triggerAfterCompletion函数会在多处多种情况下被调用,我们下面会说 + mappedHandler.triggerAfterCompletion(request, response, null); + //... + +} +``` + + + +### 2.1 源码分析 + +通过上面的方法,我们看到`preHandle()`方法在找到handler与handlerAdapter之后但实际执行`HandlerMethod`之前之前被调用。`preHandle()`接口方法如下: + +```java +default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) + throws Exception { + + return true; +} +``` + +函数会返回一个`boolean`值,如果为true则代表前置处理成功,可以放行执行`HandlerMethod`,否则就代表执行被拦截,不再实际执行`HandlerMethod`,其中参数`Object handler`在请求是动态请求(会打到Controller上,由`RequestMappingHandlerMapping`处理的请求)时,往往实际的类型就是`HandlerMethod`对象,因此这里其实可以做强转。 + +很明显,`preHandle()`更适合做统一拦截,如鉴权的时候判断用户是否有权限,如果没有权限就直接拦截驳回即可。 + +现在我们看下,`DispatcherServlet`中对它的调用源码: + +首先我们需要先知道`applyPreHandle()`的`HandlerExecutionChain`内部方法: + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HandlerExecutionChain mappedHandler = null; + mappedHandler = getHandler(processedRequest); + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } +} +``` + +`HandlerExecutionChain`内部有几个重要的属性: + +```java +public class HandlerExecutionChain { + + private final Object handler; + + private final List interceptorList = new ArrayList<>(); + + private int interceptorIndex = -1; +} +``` + +其中`handler`往往就是我们的`HandlerMethod`,而`interceptorList`就是属于当前`handler`的拦截器对象,**一个请求可以有多个拦截器,它们是有序的**。`interceptorIndex`属性很关键,它指向的是**当前执行到的拦截器位置**,这在我们讲整个拦截器处理流程中异常重要。 + +现在我们看下`applyPreHandle()`的源码: + +```java +boolean applyPreHandle(HttpServletRequest request, HttpServletResponse response) throws Exception { + for (int i = 0; i < this.interceptorList.size(); i++) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + if (!interceptor.preHandle(request, response, this.handler)) { + triggerAfterCompletion(request, response, null); + return false; + } + this.interceptorIndex = i; + } + return true; +} +``` + +可以看到比较简单,就是遍历`interceptorList`,挨个执行这些拦截器的`preHandle()`方法。但这里有两点十分重要的内容: + +1. 如果当前拦截器的`preHandle()`返回true,则interceptorIndex记录遍历的拦截器位置。 +2. 如果拦截器的`preHandle()`有一个返回false,就直接执行`triggerAfterCompletion()`方法,然后整个处理流程就结束了。 + +为什么要记录执行到的拦截器的位置?我们看下`triggerAfterCompletion()`的源码就明白了: + +```java +void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { + for (int i = this.interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + try { + interceptor.afterCompletion(request, response, this.handler, ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } +} +``` + +可以看到`triggerAfterCompletion()`就是遍历执行拦截器的`afterCompletion()`方法,但它的执行顺序有些不同,它是从`interceptorIndex`开始,倒序的方式执行。 + +假设我们现在写了3个拦截器,它们的顺序分别是A、B、C。假设现在A、B的`preHandle()`返回的是`true`,但C返回的是`false`。一个请求过来后,会先执行A的`preHandle()`方法,此时`interceptorIndex`更新为0(A在数组的第一个位置);然后执行B,将`interceptorIndex`更新为1;最后执行C的`preHandle()`,返回为`false`,终止执行。调用`triggerAfterCompletion()`,此时`triggerAfterCompletion()`会从B开始执行B的`afterCompletion()`,然后再执行A的`afterCompletion()`。 + +看明白了吗?`interceptorIndex`是用于**记录当前拦截器是在执行到第几个出问题的,一旦出了问题,就从出问题前的那一个拦截器开始倒序执行`afterCompletion()`方法。** + +如果都没有问题(`preHandle()`均返回的`true`),那`interceptorIndex`就更新为最后那个拦截器的位置。然后使用handlerAdapter来实际的执行`HandlerMethod`,如果执行没有异常,则会执行拦截器的`postHandle()`方法。其源码如下: + +```java +void applyPostHandle(HttpServletRequest request, HttpServletResponse response, @Nullable ModelAndView mv) + throws Exception { + + for (int i = this.interceptorList.size() - 1; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + interceptor.postHandle(request, response, this.handler, mv); + } +} +``` + +需要先明确的是,会执行到`postHandle()`则代表: + +1. 当前方法所属所有拦截器的`preHandle()`都返回的`true` +2. `HandlerMethod`的执行没有出错 + +`applyPostHandle()`的执行也比较简单,就是挨个遍历每个拦截器,然后执行它们的`postHandle()`方法,但是需要注意的是,这里的执行是倒序执行的,也即对于拦截器A、B、C,它们的`preHandle()`执行顺序是A、B、C,但`postHandle()`是C、B、A的顺序执行。 + +最后,当所有处理均执行完(做完数据和视图的处理了)或者**整个`doDispatch()`的执行出现任何异常**,都会调用`DispatcherServlet#triggerAfterCompletion()`,尝试执行拦截器的`afterCompletion`方法。 + +这里的异常包括但不限于: + +1. `getHandler()`找`HandlerMethod`出现异常(更具体地说是找`HandlerExecutionChain`) +2. `getHandlerAdapter()`找`HandlerAdapter`出现异常 +3. 执行拦截器的`preHandle()`出现异常 +4. 实际执行`HandlerMethod`出现异常 +5. 执行拦截器的`postHandle()`出现异常 +6. 数据和视图的解析渲染出现异常 + +而`DispatcherServlet#triggerAfterCompletion()`的源码如下: + +```java +private void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, + @Nullable HandlerExecutionChain mappedHandler, Exception ex) throws Exception { + + if (mappedHandler != null) { + mappedHandler.triggerAfterCompletion(request, response, ex); + } + throw ex; +} +``` + +可以看到会先判断是否找到了`HandlerExecutionChain`(因为我们上面说了`getHandler()`也可能会出现异常),如果找到了就调用它的`triggerAfterCompletion()`。 + +`HandlerExecutionChain#triggerAfterCompletion()`的源码我们其实上面已经看过了: + +```java +void triggerAfterCompletion(HttpServletRequest request, HttpServletResponse response, @Nullable Exception ex) { + for (int i = this.interceptorIndex; i >= 0; i--) { + HandlerInterceptor interceptor = this.interceptorList.get(i); + try { + interceptor.afterCompletion(request, response, this.handler, ex); + } + catch (Throwable ex2) { + logger.error("HandlerInterceptor.afterCompletion threw exception", ex2); + } + } +} +``` + +就是从`interceptorIndex`开始倒序执行拦截器的`afterCompletion()`方法。 + +### 2.2 总结 + +拦截器的执行源码其实还是很简单的,大家需要自己手动的翻一翻看一看。总的来说就是会先正序的执行每个拦截器的`preHandle()`,如果有任何异常(包括返回`false`),就直接从那个异常的拦截器之前开始,倒序执行它们的`afterCompletion()`方法。如果没有异常,且使用适配器执行完了处理方法,就再倒叙的执行每个拦截器的`postHandle()`方法。然后执行`ModelAndView`的解析和渲染,最终如果均没有异常或者再任何一步出现异常,都会倒序的执行拦截器的`afterCompletion()`方法。 \ No newline at end of file diff --git "a/docs/source/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" b/docs/source/spring/1-architect.md similarity index 91% rename from "docs/source/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" rename to docs/source/spring/1-architect.md index ce9103c..c9cc97f 100644 --- "a/docs/source/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" +++ b/docs/source/spring/1-architect.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,源码分析,Spring设计模式,Spring AOP,Spring IOC,Spring 动态代理,Bean生命周期,自动装配,Spring注解,Spring事务,Async注解,Spring架构 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + ## 概述 Spring是一个开放源代码的设计层面框架,他解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson创建。简单来说,Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。 diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean \347\232\204\345\210\235\345\247\213\345\214\226.md" b/docs/source/spring/10-bean-initial.md similarity index 91% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean \347\232\204\345\210\235\345\247\213\345\214\226.md" rename to docs/source/spring/10-bean-initial.md index 87e8a80..3bd63c8 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean \347\232\204\345\210\235\345\247\213\345\214\226.md" +++ b/docs/source/spring/10-bean-initial.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Bean初始化,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + **正文** 一个 bean 经历了 `createBeanInstance()` 被创建出来,然后又经过一番属性注入,依赖处理,历经千辛万苦,千锤百炼,终于有点儿 bean 实例的样子,能堪大任了,只需要经历最后一步就破茧成蝶了。这最后一步就是初始化,也就是 `initializeBean()`,所以这篇文章我们分析 `doCreateBean()` 中最后一步:初始化 bean。 @@ -7,6 +22,8 @@ 在populateBean方法下面有一个initializeBean(beanName, exposedObject, mbd)方法,这个就是用来执行用户设定的初始化操作。我们看下方法体: +> [最全面的Java面试网站](https://topjavaer.cn) + ```java protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) { if (System.getSecurityManager() != null) { @@ -82,6 +99,16 @@ public class MyBeanAware implements BeanFactoryAware { } ``` +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + 进行测试 ```java diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224ApplicationContext\345\256\271\345\231\250refresh\350\277\207\347\250\213.md" b/docs/source/spring/11-application-refresh.md similarity index 96% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224ApplicationContext\345\256\271\345\231\250refresh\350\277\207\347\250\213.md" rename to docs/source/spring/11-application-refresh.md index b0126d2..e1d4995 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224ApplicationContext\345\256\271\345\231\250refresh\350\277\207\347\250\213.md" +++ b/docs/source/spring/11-application-refresh.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,容器刷新,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + **正文** 在之前的博文中我们一直以BeanFactory接口以及它的默认实现类XmlBeanFactory为例进行分析,但是Spring中还提供了另一个接口ApplicationContext,用于扩展BeanFactory中现有的功能。 @@ -16,6 +31,8 @@ ApplicationContext bf = new ClassPathXmlApplicationContext("beanFactoryTest.xml" 接下来我们就以ClassPathXmlApplicationContext作为切入点,开始对整体功能进行分析。首先看下其构造函数: +> [最全面的Java面试网站](https://topjavaer.cn) + ```java public ClassPathXmlApplicationContext() { } @@ -171,6 +188,16 @@ spring之所以强大,为世人所推崇,除了它功能上为大家提供 接下来我们就详细的讲解每一个过程 +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + ## prepareRefresh刷新上下文的准备工作 ```java @@ -1161,4 +1188,16 @@ protected void finishRefresh() { // Participate in LiveBeansView MBean, if active. LiveBeansView.registerApplicationContext(this); } -``` \ No newline at end of file +``` + + + +分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ + +![](http://img.topjavaer.cn/image/image-20211127150136157.png) + +![](http://img.topjavaer.cn/image/image-20220316234337881.png) + +需要的小伙伴可以自行**下载**: + +http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\232\204\344\275\277\347\224\250\345\217\212AOP\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276.md" b/docs/source/spring/12-aop-custom-tag.md similarity index 88% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\232\204\344\275\277\347\224\250\345\217\212AOP\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276.md" rename to docs/source/spring/12-aop-custom-tag.md index 209b7be..bc8ec1a 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\232\204\344\275\277\347\224\250\345\217\212AOP\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276.md" +++ b/docs/source/spring/12-aop-custom-tag.md @@ -1,7 +1,24 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,AOP自定义标签,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + **正文** 我们知道在面向对象OOP编程存在一些弊端,当需要为多个不具有继承关系的对象引入同一个公共行为时,例如日志,安全检测等,我们只有在每个对象里引入公共行为,这样程序中就产生了大量的重复代码,所以有了面向对象编程的补充,面向切面编程(AOP),AOP所关注的方向是横向的,不同于OOP的纵向。接下来我们就详细分析下spring中的AOP。首先我们从动态AOP的使用开始。 +> [最全面的Java面试网站](https://topjavaer.cn) + ## AOP的使用 在开始前,先引入Aspect。 @@ -123,6 +140,16 @@ Spring实现了对所有类的test方法进行增强,使辅助功能可以独 那么,Spring是如何实现AOP的呢?首先我们知道,SPring是否支持注解的AOP是由一个配置文件控制的,也就是``,当在配置文件中声明了这句配置的时候,Spring就会支持注解的AOP,那么我们的分析就从这句注解开始。 +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + ## AOP自定义标签 @@ -310,4 +337,18 @@ public class AServicelmpll implements AService { ``` -然后将以上代码中的 “this.b();” 修改为 “((AService) AopContext.currentProxy()).b();” 即可。 通过以上的修改便可以完成对a和b方法的同时增强。 \ No newline at end of file +然后将以上代码中的 “this.b();” 修改为 “((AService) AopContext.currentProxy()).b();” 即可。 通过以上的修改便可以完成对a和b方法的同时增强。 + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\345\210\233\345\273\272AOP\344\273\243\347\220\206\344\271\213\350\216\267\345\217\226\345\242\236\345\274\272\345\231\250.md" b/docs/source/spring/13-aop-proxy-advisor.md similarity index 94% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\345\210\233\345\273\272AOP\344\273\243\347\220\206\344\271\213\350\216\267\345\217\226\345\242\236\345\274\272\345\231\250.md" rename to docs/source/spring/13-aop-proxy-advisor.md index b826341..c8bf06b 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\345\210\233\345\273\272AOP\344\273\243\347\220\206\344\271\213\350\216\267\345\217\226\345\242\236\345\274\272\345\231\250.md" +++ b/docs/source/spring/13-aop-proxy-advisor.md @@ -1,10 +1,25 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,增强器,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + **正文** 在上一篇的博文中我们讲解了通过自定义配置完成了对AnnotationAwareAspectJAutoProxyCreator类型的自动注册,那么这个类到底做了什么工作来完成AOP的操作呢?首先我们看看AnnotationAwareAspectJAutoProxyCreator的层次结构,如下图所示: ![](http://img.topjavaer.cn/img/202309262317016.png) - +> 内容摘自我的学习网站:topjavaer.cn 从上图的类层次结构图中我们看到这个类实现了BeanPostProcessor接口,而实现BeanPostProcessor后,当Spring加载这个Bean时会在实例化前调用其postProcesssAfterIntialization方法,而我们对于AOP逻辑的分析也由此开始。 首先看下其父类AbstractAutoProxyCreator中的postProcessAfterInitialization方法: @@ -94,7 +109,15 @@ protected List findEligibleAdvisors(Class beanClass, String beanName 对于指定bean的增强方法的获取一定是包含两个步骤的,获取所有的增强以及寻找所有增强中使用于bean的增强并应用,那么**findCandidateAdvisors**与**findAdvisorsThatCanApply**便是做了这两件事情。当然,如果无法找到对应的增强器便返回DO_NOT_PROXY,其中DO_NOT_PROXY=null。 - +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd ## 获取增强器 @@ -243,7 +266,7 @@ private static A findAnnotation(Class clazz, Class } ``` -如果 bean 存在 Aspect.class注解,就可以获取此bean中的增强器了,接着我们来看看 List classAdvisors = this.advisorFactory.getAdvisors(factory); +如果 bean 存在 Aspect.class注解,就可以获取此bean中的增强器了,接着我们来看看 `List classAdvisors = this.advisorFactory.getAdvisors(factory)`; ```java @Override @@ -801,3 +824,18 @@ private boolean matchesMethod(Method method) { 至此,我们在后置处理器中找到了所有匹配Bean中的增强器,下一篇讲解如何使用找到切面,来创建代理。 + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 AOP\344\273\243\347\220\206\347\232\204\347\224\237\346\210\220.md" b/docs/source/spring/14-aop-proxy-create.md similarity index 94% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 AOP\344\273\243\347\220\206\347\232\204\347\224\237\346\210\220.md" rename to docs/source/spring/14-aop-proxy-create.md index 1d23eeb..19e87b3 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 AOP\344\273\243\347\220\206\347\232\204\347\224\237\346\210\220.md" +++ b/docs/source/spring/14-aop-proxy-create.md @@ -1,3 +1,28 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理生成,AOP代理,增强器,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +> 本文已经收录到大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + **正文** 在获取了所有对应bean的增强后,便可以进行代理的创建了。回到AbstractAutoProxyCreator的wrapIfNecessary方法中,如下所示: @@ -34,6 +59,8 @@ 我们上一篇文章分析完了第16行,获取到了所有对应bean的增强器,并获取到了此目标bean所有匹配的 Advisor,接下来我们要从第17行开始分析,如果 specificInterceptors 不为空,则要为当前bean创建代理类,接下来我们来看创建代理类的方法 **createProxy:** +> [最全面的Java面试网站](https://topjavaer.cn) + ```java protected Object createProxy(Class beanClass, @Nullable String beanName, @Nullable Object[] specificInterceptors, TargetSource targetSource) { @@ -631,4 +658,16 @@ public Object proceed() throws Throwable { -下一篇文章讲解目标方法和增强方法是如何执行的。 \ No newline at end of file +下一篇文章讲解目标方法和增强方法是如何执行的。 + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247486208&idx=1&sn=dbeedf47c50b1be67b2ef31a901b8b56&chksm=ce98f646f9ef7f506a1f7d72fc9384ba1b518072b44d157f657a8d5495a1c78c3e5de0b41efd&token=1652861108&lang=zh_CN#rd \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\233\256\346\240\207\346\226\271\346\263\225\345\222\214\345\242\236\345\274\272\346\226\271\346\263\225\347\232\204\346\211\247\350\241\214.md" b/docs/source/spring/15-aop-advice-create.md similarity index 82% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\233\256\346\240\207\346\226\271\346\263\225\345\222\214\345\242\236\345\274\272\346\226\271\346\263\225\347\232\204\346\211\247\350\241\214.md" rename to docs/source/spring/15-aop-advice-create.md index 901cfed..22df32a 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224AOP\347\233\256\346\240\207\346\226\271\346\263\225\345\222\214\345\242\236\345\274\272\346\226\271\346\263\225\347\232\204\346\211\247\350\241\214.md" +++ b/docs/source/spring/15-aop-advice-create.md @@ -1,6 +1,21 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,目标方法,增强方法,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + **正文** -上一篇博文中我们讲了代理类的生成,这一篇主要讲解剩下的部分,当代理类调用时,目标方法和代理方法是如何执行的,我们还是接着上篇的ReflectiveMethodInvocation类Proceed方法来看 +上一篇博文中我们讲了代理类的生成,这一篇主要讲解剩下的部分,当代理类调用时,目标方法和代理方法是如何执行的,我们还是接着上篇的ReflectiveMethodInvocation类Proceed方法来看。[最全面的Java面试网站](https://topjavaer.cn) ```java public Object proceed() throws Throwable { @@ -40,6 +55,16 @@ public Object proceed() throws Throwable { 我们看到链中的顺序是AspectJAfterThrowingAdvice、AfterReturningAdviceInterceptor、AspectJAfterAdvice、MethodBeforeAdviceInterceptor,这些拦截器是按顺序执行的,那我们来看看第一个拦截器AspectJAfterThrowingAdvice中的invoke方法 +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + ## **AspectJAfterThrowingAdvice** ```java @@ -151,4 +176,18 @@ before方法执行完后,就通过反射的方式执行目标bean中的method ## 总结 -这个代理类调用过程,我们可以看到是一个递归的调用过程,通过ReflectiveMethodInvocation类中Proceed方法递归调用,顺序执行拦截器链中AspectJAfterThrowingAdvice、AfterReturningAdviceInterceptor、AspectJAfterAdvice、MethodBeforeAdviceInterceptor这几个拦截器,在拦截器中反射调用增强方法 \ No newline at end of file +这个代理类调用过程,我们可以看到是一个递归的调用过程,通过ReflectiveMethodInvocation类中Proceed方法递归调用,顺序执行拦截器链中AspectJAfterThrowingAdvice、AfterReturningAdviceInterceptor、AspectJAfterAdvice、MethodBeforeAdviceInterceptor这几个拦截器,在拦截器中反射调用增强方法 + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224@Transactional\346\263\250\350\247\243\347\232\204\345\243\260\346\230\216\345\274\217\344\272\213\347\211\251\344\273\213\347\273\215.md" b/docs/source/spring/16-transactional.md similarity index 90% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224@Transactional\346\263\250\350\247\243\347\232\204\345\243\260\346\230\216\345\274\217\344\272\213\347\211\251\344\273\213\347\273\215.md" rename to docs/source/spring/16-transactional.md index 748cbc0..e63dd5a 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224@Transactional\346\263\250\350\247\243\347\232\204\345\243\260\346\230\216\345\274\217\344\272\213\347\211\251\344\273\213\347\273\215.md" +++ b/docs/source/spring/16-transactional.md @@ -1,6 +1,21 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,事物注解,声明式事物,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + **正文** -面的几个章节已经分析了spring基于`@AspectJ`的源码,那么接下来我们分析一下Aop的另一个重要功能,事物管理。 +面的几个章节已经分析了spring基于`@AspectJ`的源码,那么接下来我们分析一下Aop的另一个重要功能,事物管理。[最全面的Java面试网站](https://topjavaer.cn) ## 事务的介绍 @@ -108,6 +123,16 @@ MySQL默认的事务隔离级别为 **可重复读repeatable-read**   3.PlatformTransactionManager-->Spring事务基础结构中的中心接口 +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + ```java public interface PlatformTransactionManager { // 根据指定的传播行为,返回当前活动的事务或创建新事务。 @@ -386,4 +411,20 @@ java.lang.RuntimeException: ==手动抛出一个异常 -下一篇我们分析基于@Transactional注解的声明式事物的的源码实现。 \ No newline at end of file +下一篇我们分析基于@Transactional注解的声明式事物的的源码实现。 + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/17-spring-transaction-aop.md b/docs/source/spring/17-spring-transaction-aop.md new file mode 100644 index 0000000..8888543 --- /dev/null +++ b/docs/source/spring/17-spring-transaction-aop.md @@ -0,0 +1,687 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,事物注解,声明式事物,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +此篇文章需要有SpringAOP基础,知道AOP底层原理可以更好的理解Spring的事务处理。[最全面的Java面试网站](https://topjavaer.cn) + + + +## 自定义标签 + +对于Spring中事务功能的代码分析,我们首先从配置文件开始人手,在配置文件中有这样一个配置:``。可以说此处配置是事务的开关,如果没有此处配置,那么Spring中将不存在事务的功能。那么我们就从这个配置开始分析。 + +根据之前的分析,我们因此可以判断,在自定义标签中的解析过程中一定是做了一些辅助操作,于是我们先从自定义标签入手进行分析。使用Idea搜索全局代码,关键字annotation-driven,最终锁定类TxNamespaceHandler,在TxNamespaceHandler中的 init 方法中: + +```java +@Override +public void init() { + registerBeanDefinitionParser("advice", new TxAdviceBeanDefinitionParser()); + registerBeanDefinitionParser("annotation-driven", new AnnotationDrivenBeanDefinitionParser()); + registerBeanDefinitionParser("jta-transaction-manager", new JtaTransactionManagerBeanDefinitionParser()); +} +``` + +在遇到诸如tx:annotation-driven为开头的配置后,Spring都会使用AnnotationDrivenBeanDefinitionParser类的parse方法进行解析。 + +```java +@Override +@Nullable +public BeanDefinition parse(Element element, ParserContext parserContext) { + registerTransactionalEventListenerFactory(parserContext); + String mode = element.getAttribute("mode"); + if ("aspectj".equals(mode)) { + // mode="aspectj" + registerTransactionAspect(element, parserContext); + if (ClassUtils.isPresent("javax.transaction.Transactional", getClass().getClassLoader())) { + registerJtaTransactionAspect(element, parserContext); + } + } + else { + // mode="proxy" + AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); + } + return null; +} +``` + + + +在解析中存在对于mode属性的判断,根据代码,如果我们需要使用AspectJ的方式进行事务切入(Spring中的事务是以AOP为基础的),那么可以使用这样的配置: + +``` + +``` + +## 注册 InfrastructureAdvisorAutoProxyCreator + +我们以默认配置为例进行分析,进人AopAutoProxyConfigurer类的configureAutoProxyCreator: + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +```java +public static void configureAutoProxyCreator(Element element, ParserContext parserContext) { + //向IOC注册InfrastructureAdvisorAutoProxyCreator这个类型的Bean + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); + + String txAdvisorBeanName = TransactionManagementConfigUtils.TRANSACTION_ADVISOR_BEAN_NAME; + if (!parserContext.getRegistry().containsBeanDefinition(txAdvisorBeanName)) { + Object eleSource = parserContext.extractSource(element); + + // Create the TransactionAttributeSource definition. + // 创建AnnotationTransactionAttributeSource类型的Bean + RootBeanDefinition sourceDef = new RootBeanDefinition("org.springframework.transaction.annotation.AnnotationTransactionAttributeSource"); + sourceDef.setSource(eleSource); + sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); + + // Create the TransactionInterceptor definition. + // 创建TransactionInterceptor类型的Bean + RootBeanDefinition interceptorDef = new RootBeanDefinition(TransactionInterceptor.class); + interceptorDef.setSource(eleSource); + interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + registerTransactionManager(element, interceptorDef); + interceptorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); + String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); + + // Create the TransactionAttributeSourceAdvisor definition. + // 创建BeanFactoryTransactionAttributeSourceAdvisor类型的Bean + RootBeanDefinition advisorDef = new RootBeanDefinition(BeanFactoryTransactionAttributeSourceAdvisor.class); + advisorDef.setSource(eleSource); + advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + // 将上面AnnotationTransactionAttributeSource类型Bean注入进上面的Advisor + advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); + // 将上面TransactionInterceptor类型Bean注入进上面的Advisor + advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); + if (element.hasAttribute("order")) { + advisorDef.getPropertyValues().add("order", element.getAttribute("order")); + } + parserContext.getRegistry().registerBeanDefinition(txAdvisorBeanName, advisorDef); + // 将上面三个Bean注册进IOC中 + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource); + compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, txAdvisorBeanName)); + parserContext.registerComponent(compositeDef); + } +} +``` + +这里分别是注册了三个Bean,和一个`InfrastructureAdvisorAutoProxyCreator`,其中三个Bean支撑了整个事务的功能。 + +我们首先需要回顾一下AOP的原理,AOP中有一个 Advisor 存放在代理类中,而Advisor中有advise与pointcut信息,每次执行被代理类的方法时都会执行代理类的invoke(如果是JDK代理)方法,而invoke方法会根据advisor中的pointcut动态匹配这个方法需要执行的advise链,遍历执行advise链,从而达到AOP切面编程的目的。 + +- `BeanFactoryTransactionAttributeSourceAdvisor`:首先看这个类的继承结构,可以看到这个类其实是一个Advisor,其实由名字也能看出来,类中有几个关键地方注意一下,在之前的注册过程中,将两个属性注入进这个Bean中: + +```java +// 将上面AnnotationTransactionAttributeSource类型Bean注入进上面的Advisor +advisorDef.getPropertyValues().add("transactionAttributeSource", new RuntimeBeanReference(sourceName)); +// 将上面TransactionInterceptor类型Bean注入进上面的Advisor +advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); +``` + +那么它们被注入成什么了呢?进入`BeanFactoryTransactionAttributeSourceAdvisor`一看便知。 + +```java +@Nullable +private TransactionAttributeSource transactionAttributeSource; +``` + +在其父类中有属性: + +```java +@Nullable +private String adviceBeanName; +``` + +也就是说,这里先将上面的TransactionInterceptor的BeanName传入到Advisor中,然后将AnnotationTransactionAttributeSource这个Bean注入到Advisor中,那么这个Source Bean有什么用呢?可以继续看看BeanFactoryTransactionAttributeSourceAdvisor的源码。 + +```java +private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { + @Override + @Nullable + protected TransactionAttributeSource getTransactionAttributeSource() { + return transactionAttributeSource; + } +}; +``` + +看到这里应该明白了,这里的Source是提供了pointcut信息,作为存放事务属性的一个类注入进Advisor中,到这里应该知道注册这三个Bean的作用了吧?首先注册pointcut、advice、advisor,然后将pointcut和advice注入进advisor中,在之后动态代理的时候会使用这个Advisor去寻找每个Bean是否需要动态代理(取决于是否有开启事务),因为Advisor有pointcut信息。 + +![](http://img.topjavaer.cn/img/202310031553121.png) + +- InfrastructureAdvisorAutoProxyCreator:在方法开头,首先就调用了AopNamespeceUtils去注册了这个Bean,那么这个Bean是干什么用的呢?还是先看看这个类的结构。这个类继承了AbstractAutoProxyCreator,看到这个名字,熟悉AOP的话应该已经知道它是怎么做的了吧?其次这个类还实现了BeanPostProcessor接口,凡事实现了这个BeanPost接口的类,我们首先关注的就是它的postProcessAfterInitialization方法,这里在其父类也就是刚刚提到的AbstractAutoProxyCreator这里去实现。(这里需要知道Spring容器初始化Bean的过程,关于BeanPostProcessor的使用我会另开一篇讲解。如果不知道只需了解如果一个Bean实现了BeanPostProcessor接口,当所有Bean实例化且依赖注入之后初始化方法之后会执行这个实现Bean的postProcessAfterInitialization方法) + +进入这个函数: + +```java +public static void registerAutoProxyCreatorIfNecessary( + ParserContext parserContext, Element sourceElement) { + + BeanDefinition beanDefinition = AopConfigUtils.registerAutoProxyCreatorIfNecessary( + parserContext.getRegistry(), parserContext.extractSource(sourceElement)); + useClassProxyingIfNecessary(parserContext.getRegistry(), sourceElement); + registerComponentIfNecessary(beanDefinition, parserContext); +} + +@Nullable +public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry, + @Nullable Object source) { + + return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source); +} +``` + +对于解析来的代码流程AOP中已经有所分析,上面的两个函数主要目的是注册了InfrastructureAdvisorAutoProxyCreator类型的bean,那么注册这个类的目的是什么呢?查看这个类的层次,如下图所示: + +![](http://img.topjavaer.cn/img/202310031554128.png) + +从上面的层次结构中可以看到,InfrastructureAdvisorAutoProxyCreator间接实现了SmartInstantiationAwareBeanPostProcessor,而SmartInstantiationAwareBeanPostProcessor又继承自InstantiationAwareBeanPostProcessor,也就是说在Spring中,所有bean实例化时Spring都会保证调用其postProcessAfterInstantiation方法,其实现是在父类AbstractAutoProxyCreator类中实现。 + +以之前的示例为例,当实例化AccountServiceImpl的bean时便会调用此方法,方法如下: + +```java +@Override +public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { + if (bean != null) { + // 根据给定的bean的class和name构建出key,格式:beanClassName_beanName + Object cacheKey = getCacheKey(bean.getClass(), beanName); + if (!this.earlyProxyReferences.contains(cacheKey)) { + // 如果它适合被代理,则需要封装指定bean + return wrapIfNecessary(bean, beanName, cacheKey); + } + } + return bean; +} +``` + +这里实现的主要目的是对指定bean进行封装,当然首先要确定是否需要封装,检测与封装的工作都委托给了wrapIfNecessary函数进行。 + +```java +protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { + // 如果处理过这个bean的话直接返回 + if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { + return bean; + } + // 之后如果Bean匹配不成功,会将Bean的cacheKey放入advisedBeans中 + // value为false,所以这里可以用cacheKey判断此bean是否之前已经代理不成功了 + if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { + return bean; + } + // 这里会将Advise、Pointcut、Advisor类型的类过滤,直接不进行代理,return + if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { + // 这里即为不成功的情况,将false放入Map中 + this.advisedBeans.put(cacheKey, Boolean.FALSE); + return bean; + } + + // Create proxy if we have advice. + // 这里是主要验证的地方,传入Bean的class与beanName去判断此Bean有哪些Advisor + Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null); + // 如果有相应的advisor被找到,则用advisor与此bean做一个动态代理,将这两个的信息 + // 放入代理类中进行代理 + if (specificInterceptors != DO_NOT_PROXY) { + this.advisedBeans.put(cacheKey, Boolean.TRUE); + // 创建代理的地方 + Object proxy = createProxy( + bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean)); + this.proxyTypes.put(cacheKey, proxy.getClass()); + // 返回代理对象 + return proxy; + } + // 如果此Bean没有一个Advisor匹配,将返回null也就是DO_NOT_PROXY + // 也就是会走到这一步,将其cacheKey,false存入Map中 + this.advisedBeans.put(cacheKey, Boolean.FALSE); + // 不代理直接返回原bean + return bean; +} +``` + +wrapIfNecessary函数功能实现起来很复杂,但是逻辑上理解起来还是相对简单的,在wrapIfNecessary函数中主要的工作如下: + +(1)找出指定bean对应的增强器。 + +(2)根据找出的增强器创建代理。 + +听起来似乎简单的逻辑,Spring中又做了哪些复杂的工作呢?对于创建代理的部分,通过之前的分析相信大家已经很熟悉了,但是对于增强器的获取,Spring又是怎么做的呢? + +## 获取对应class/method的增强器 + +获取指定bean对应的增强器,其中包含两个关键字:增强器与对应。也就是说在 getAdvicesAndAdvisorsForBean函数中,不但要找出增强器,而且还需要判断增强器是否满足要求。 + +```java +@Override +@Nullable +protected Object[] getAdvicesAndAdvisorsForBean( + Class beanClass, String beanName, @Nullable TargetSource targetSource) { + + List advisors = findEligibleAdvisors(beanClass, beanName); + if (advisors.isEmpty()) { + return DO_NOT_PROXY; + } + return advisors.toArray(); +} + +protected List findEligibleAdvisors(Class beanClass, String beanName) { + List candidateAdvisors = findCandidateAdvisors(); + List eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); + extendAdvisors(eligibleAdvisors); + if (!eligibleAdvisors.isEmpty()) { + eligibleAdvisors = sortAdvisors(eligibleAdvisors); + } + return eligibleAdvisors; +} +``` + + + +### 寻找候选增强器 + +```java +protected List findCandidateAdvisors() { + Assert.state(this.advisorRetrievalHelper != null, "No BeanFactoryAdvisorRetrievalHelper available"); + return this.advisorRetrievalHelper.findAdvisorBeans(); +} + +public List findAdvisorBeans() { + // Determine list of advisor bean names, if not cached already. + String[] advisorNames = this.cachedAdvisorBeanNames; + if (advisorNames == null) { + // 获取BeanFactory中所有对应Advisor.class的类名 + // 这里和AspectJ的方式有点不同,AspectJ是获取所有的Object.class,然后通过反射过滤有注解AspectJ的类 + advisorNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors( + this.beanFactory, Advisor.class, true, false); + this.cachedAdvisorBeanNames = advisorNames; + } + if (advisorNames.length == 0) { + return new ArrayList<>(); + } + + List advisors = new ArrayList<>(); + for (String name : advisorNames) { + if (isEligibleBean(name)) { + if (this.beanFactory.isCurrentlyInCreation(name)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping currently created advisor '" + name + "'"); + } + } + else { + try { + //直接获取advisorNames的实例,封装进advisors数组 + advisors.add(this.beanFactory.getBean(name, Advisor.class)); + } + catch (BeanCreationException ex) { + Throwable rootCause = ex.getMostSpecificCause(); + if (rootCause instanceof BeanCurrentlyInCreationException) { + BeanCreationException bce = (BeanCreationException) rootCause; + String bceBeanName = bce.getBeanName(); + if (bceBeanName != null && this.beanFactory.isCurrentlyInCreation(bceBeanName)) { + if (logger.isDebugEnabled()) { + logger.debug("Skipping advisor '" + name + + "' with dependency on currently created bean: " + ex.getMessage()); + } + continue; + } + } + throw ex; + } + } + } + } + return advisors; +} +``` + +首先是通过BeanFactoryUtils类提供的工具方法获取所有对应Advisor.class的类,获取办法无非是使用ListableBeanFactory中提供的方法: + +```java +String[] getBeanNamesForType(@Nullable Class type, boolean includeNonSingletons, boolean allowEagerInit); +``` + +在我们讲解自定义标签时曾经注册了一个类型为 BeanFactoryTransactionAttributeSourceAdvisor 的 bean,而在此 bean 中我们又注入了另外两个Bean,那么此时这个 Bean 就会被开始使用了。因为 BeanFactoryTransactionAttributeSourceAdvisor同样也实现了 Advisor接口,那么在获取所有增强器时自然也会将此bean提取出来, 并随着其他增强器一起在后续的步骤中被织入代理。 + +### 候选增强器中寻找到匹配项 + +当找出对应的增强器后,接下来的任务就是看这些增强器是否与对应的class匹配了,当然不只是class,class内部的方法如果匹配也可以通过验证。 + +```java +public static List findAdvisorsThatCanApply(List candidateAdvisors, Class clazz) { + if (candidateAdvisors.isEmpty()) { + return candidateAdvisors; + } + List eligibleAdvisors = new ArrayList<>(); + // 首先处理引介增强 + for (Advisor candidate : candidateAdvisors) { + if (candidate instanceof IntroductionAdvisor && canApply(candidate, clazz)) { + eligibleAdvisors.add(candidate); + } + } + boolean hasIntroductions = !eligibleAdvisors.isEmpty(); + for (Advisor candidate : candidateAdvisors) { + // 引介增强已经处理 + if (candidate instanceof IntroductionAdvisor) { + // already processed + continue; + } + // 对于普通bean的处理 + if (canApply(candidate, clazz, hasIntroductions)) { + eligibleAdvisors.add(candidate); + } + } + return eligibleAdvisors; +} + +public static boolean canApply(Advisor advisor, Class targetClass, boolean hasIntroductions) { + if (advisor instanceof IntroductionAdvisor) { + return ((IntroductionAdvisor) advisor).getClassFilter().matches(targetClass); + } + else if (advisor instanceof PointcutAdvisor) { + PointcutAdvisor pca = (PointcutAdvisor) advisor; + return canApply(pca.getPointcut(), targetClass, hasIntroductions); + } + else { + // It doesn't have a pointcut so we assume it applies. + return true; + } +} +``` + +BeanFactoryTransactionAttributeSourceAdvisor 间接实现了PointcutAdvisor。 因此,在canApply函数中的第二个if判断时就会通过判断,会将BeanFactoryTransactionAttributeSourceAdvisor中的getPointcut()方法返回值作为参数继续调用canApply方法,而 getPoint()方法返回的是TransactionAttributeSourcePointcut类型的实例。对于 transactionAttributeSource这个属性大家还有印象吗?这是在解析自定义标签时注入进去的。 + +```java +private final TransactionAttributeSourcePointcut pointcut = new TransactionAttributeSourcePointcut() { + @Override + @Nullable + protected TransactionAttributeSource getTransactionAttributeSource() { + return transactionAttributeSource; + } +}; +``` + +那么,使用TransactionAttributeSourcePointcut类型的实例作为函数参数继续跟踪canApply。 + +```java +public static boolean canApply(Pointcut pc, Class targetClass, boolean hasIntroductions) { + Assert.notNull(pc, "Pointcut must not be null"); + if (!pc.getClassFilter().matches(targetClass)) { + return false; + } + + // 此时的pc表示TransactionAttributeSourcePointcut + // pc.getMethodMatcher()返回的正是自身(this) + MethodMatcher methodMatcher = pc.getMethodMatcher(); + if (methodMatcher == MethodMatcher.TRUE) { + // No need to iterate the methods if we're matching any method anyway... + return true; + } + + IntroductionAwareMethodMatcher introductionAwareMethodMatcher = null; + if (methodMatcher instanceof IntroductionAwareMethodMatcher) { + introductionAwareMethodMatcher = (IntroductionAwareMethodMatcher) methodMatcher; + } + + Set> classes = new LinkedHashSet<>(); + if (!Proxy.isProxyClass(targetClass)) { + classes.add(ClassUtils.getUserClass(targetClass)); + } + //获取对应类的所有接口 + classes.addAll(ClassUtils.getAllInterfacesForClassAsSet(targetClass)); + //对类进行遍历 + for (Class clazz : classes) { + //反射获取类中所有的方法 + Method[] methods = ReflectionUtils.getAllDeclaredMethods(clazz); + for (Method method : methods) { + //对类和方法进行增强器匹配 + if (introductionAwareMethodMatcher != null ? + introductionAwareMethodMatcher.matches(method, targetClass, hasIntroductions) : + methodMatcher.matches(method, targetClass)) { + return true; + } + } + } + + return false; +} +``` + +通过上面函数大致可以理清大体脉络,首先获取对应类的所有接口并连同类本身一起遍历,遍历过程中又对类中的方法再次遍历,一旦匹配成功便认为这个类适用于当前增强器。 + +到这里我们不禁会有疑问,对于事物的配置不仅仅局限于在函数上配置,我们都知道,在类或接口上的配置可以延续到类中的每个函数,那么,如果针对每个函数迸行检测,在类本身上配罝的事务属性岂不是检测不到了吗?带着这个疑问,我们继续探求matcher方法。 + +做匹配的时候 methodMatcher.matches(method, targetClass)会使用 TransactionAttributeSourcePointcut 类的 matches 方法。 + +```java +@Override +public boolean matches(Method method, Class targetClass) { + if (TransactionalProxy.class.isAssignableFrom(targetClass)) { + return false; + } + // 自定义标签解析时注入 + TransactionAttributeSource tas = getTransactionAttributeSource(); + return (tas == null || tas.getTransactionAttribute(method, targetClass) != null); +} +``` + +此时的 tas 表示 AnnotationTransactionAttributeSource 类型,这里会判断**tas.getTransactionAttribute(method, targetClass) != null,**而 AnnotationTransactionAttributeSource 类型的 getTransactionAttribute 方法如下: + +```java +@Override +@Nullable +public TransactionAttribute getTransactionAttribute(Method method, @Nullable Class targetClass) { + if (method.getDeclaringClass() == Object.class) { + return null; + } + + Object cacheKey = getCacheKey(method, targetClass); + Object cached = this.attributeCache.get(cacheKey); + //先从缓存中获取TransactionAttribute + if (cached != null) { + if (cached == NULL_TRANSACTION_ATTRIBUTE) { + return null; + } + else { + return (TransactionAttribute) cached; + } + } + else { + // 如果缓存中没有,工作又委托给了computeTransactionAttribute函数 + TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass); + // Put it in the cache. + if (txAttr == null) { + // 设置为空 + this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE); + } + else { + String methodIdentification = ClassUtils.getQualifiedMethodName(method, targetClass); + if (txAttr instanceof DefaultTransactionAttribute) { + ((DefaultTransactionAttribute) txAttr).setDescriptor(methodIdentification); + } + if (logger.isDebugEnabled()) { + logger.debug("Adding transactional method '" + methodIdentification + "' with attribute: " + txAttr); + } + //加入缓存中 + this.attributeCache.put(cacheKey, txAttr); + } + return txAttr; + } +} +``` + +尝试从缓存加载,如果对应信息没有被缓存的话,工作又委托给了computeTransactionAttribute函数,在computeTransactionAttribute函数中我们终于看到了事务标签的提取过程。 + +### 提取事务标签 + +```java +@Nullable +protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class targetClass) { + // Don't allow no-public methods as required. + if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { + return null; + } + + // The method may be on an interface, but we need attributes from the target class. + // If the target class is null, the method will be unchanged. + // method代表接口中的方法,specificMethod代表实现类中的方法 + Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass); + + // First try is the method in the target class. + // 查看方法中是否存在事务声明 + TransactionAttribute txAttr = findTransactionAttribute(specificMethod); + if (txAttr != null) { + return txAttr; + } + + // Second try is the transaction attribute on the target class. + // 查看方法所在类中是否存在事务声明 + txAttr = findTransactionAttribute(specificMethod.getDeclaringClass()); + if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { + return txAttr; + } + + // 如果存在接口,则到接口中去寻找 + if (specificMethod != method) { + // Fallback is to look at the original method. + // 查找接口方法 + txAttr = findTransactionAttribute(method); + if (txAttr != null) { + return txAttr; + } + // Last fallback is the class of the original method. + // 到接口中的类中去寻找 + txAttr = findTransactionAttribute(method.getDeclaringClass()); + if (txAttr != null && ClassUtils.isUserLevelMethod(method)) { + return txAttr; + } + } + + return null; +} +``` + +对于事务属性的获取规则相信大家都已经很清楚,如果方法中存在事务属性,则使用方法上的属性,否则使用方法所在的类上的属性,如果方法所在类的属性上还是没有搜寻到对应的事务属性,那么在搜寻接口中的方法,再没有的话,最后尝试搜寻接口的类上面的声明。对于函数computeTransactionAttribute中的逻辑与我们所认识的规则并无差別,但是上面函数中并没有真正的去做搜寻事务属性的逻辑,而是搭建了个执行框架,将搜寻事务属性的任务委托给了 findTransactionAttribute 方法去执行。 + +```java +@Override +@Nullable +protected TransactionAttribute findTransactionAttribute(Class clazz) { + return determineTransactionAttribute(clazz); +} + +@Nullable +protected TransactionAttribute determineTransactionAttribute(AnnotatedElement ae) { + for (TransactionAnnotationParser annotationParser : this.annotationParsers) { + TransactionAttribute attr = annotationParser.parseTransactionAnnotation(ae); + if (attr != null) { + return attr; + } + } + return null; +} +``` + +this.annotationParsers 是在当前类 AnnotationTransactionAttributeSource 初始化的时候初始化的,其中的值被加入了 SpringTransactionAnnotationParser,也就是当进行属性获取的时候其实是使用 SpringTransactionAnnotationParser 类的 parseTransactionAnnotation 方法进行解析的。 + +```java +@Override +@Nullable +public TransactionAttribute parseTransactionAnnotation(AnnotatedElement ae) { + AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes( + ae, Transactional.class, false, false); + if (attributes != null) { + return parseTransactionAnnotation(attributes); + } + else { + return null; + } +} +``` + +至此,我们终于看到了想看到的获取注解标记的代码。首先会判断当前的类是否含有 Transactional注解,这是事务属性的基础,当然如果有的话会继续调用parseTransactionAnnotation 方法解析详细的属性。 + +```java +protected TransactionAttribute parseTransactionAnnotation(AnnotationAttributes attributes) { + RuleBasedTransactionAttribute rbta = new RuleBasedTransactionAttribute(); + Propagation propagation = attributes.getEnum("propagation"); + // 解析propagation + rbta.setPropagationBehavior(propagation.value()); + Isolation isolation = attributes.getEnum("isolation"); + // 解析isolation + rbta.setIsolationLevel(isolation.value()); + // 解析timeout + rbta.setTimeout(attributes.getNumber("timeout").intValue()); + // 解析readOnly + rbta.setReadOnly(attributes.getBoolean("readOnly")); + // 解析value + rbta.setQualifier(attributes.getString("value")); + ArrayList rollBackRules = new ArrayList<>(); + // 解析rollbackFor + Class[] rbf = attributes.getClassArray("rollbackFor"); + for (Class rbRule : rbf) { + RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + // 解析rollbackForClassName + String[] rbfc = attributes.getStringArray("rollbackForClassName"); + for (String rbRule : rbfc) { + RollbackRuleAttribute rule = new RollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + // 解析noRollbackFor + Class[] nrbf = attributes.getClassArray("noRollbackFor"); + for (Class rbRule : nrbf) { + NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + // 解析noRollbackForClassName + String[] nrbfc = attributes.getStringArray("noRollbackForClassName"); + for (String rbRule : nrbfc) { + NoRollbackRuleAttribute rule = new NoRollbackRuleAttribute(rbRule); + rollBackRules.add(rule); + } + rbta.getRollbackRules().addAll(rollBackRules); + return rbta; +} +``` + +至此,我们终于完成了事务标签的解析。回顾一下,我们现在的任务是找出某个增强器是否适合于对应的类,而是否匹配的关键则在于是否从指定的类或类中的方法中找到对应的事务属性,现在,我们以AccountServiceImpl为例,已经在它的接口AccountServiceImp中找到了事务属性,所以,它是与事务增强器匹配的,也就是它会被事务功能修饰。 + +至此,事务功能的初始化工作便结束了,当判断某个bean适用于事务增强时,也就是适用于增强器BeanFactoryTransactionAttributeSourceAdvisor。 + +BeanFactoryTransactionAttributeSourceAdvisor 作为 Advisor 的实现类,自然要遵从 Advisor 的处理方式,当代理被调用时会调用这个类的增强方法,也就是此bean的Advice,又因为在解析事务定义标签时我们把Transactionlnterceptor类的bean注人到了 BeanFactoryTransactionAttributeSourceAdvisor中,所以,在调用事务增强器增强的代理类时会首先执行Transactionlnterceptor进行增强,同时,也就是在Transactionlnterceptor类中的invoke方法中完成了整个事务的逻辑。 + + + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) + diff --git a/docs/source/spring/18-transaction-advice.md b/docs/source/spring/18-transaction-advice.md new file mode 100644 index 0000000..fd08171 --- /dev/null +++ b/docs/source/spring/18-transaction-advice.md @@ -0,0 +1,840 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,代理,事物增强器,声明式事物,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +上一篇文章我们讲解了事务的Advisor是如何注册进Spring容器的,也讲解了Spring是如何将有配置事务的类配置上事务的,实际上也就是用了AOP那一套,也讲解了Advisor,pointcut验证流程,至此,事务的初始化工作都已经完成了,在之后的调用过程,如果代理类的方法被调用,都会调用BeanFactoryTransactionAttributeSourceAdvisor这个Advisor的增强方法,也就是我们还未提到的那个Advisor里面的advise,还记得吗,在自定义标签的时候我们将TransactionInterceptor这个Advice作为bean注册进IOC容器,并且将其注入进Advisor中,这个Advice在代理类的invoke方法中会被封装到拦截器链中,最终事务的功能都在advise中体现,所以我们先来关注一下TransactionInterceptor这个类吧。[最全面的Java面试网站](https://topjavaer.cn) + +TransactionInterceptor类继承自MethodInterceptor,所以调用该类是从其invoke方法开始的,首先预览下这个方法: + +```java +@Override +@Nullable +public Object invoke(final MethodInvocation invocation) throws Throwable { + // Work out the target class: may be {@code null}. + // The TransactionAttributeSource should be passed the target class + // as well as the method, which may be from an interface. + Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); + + // Adapt to TransactionAspectSupport's invokeWithinTransaction... + return invokeWithinTransaction(invocation.getMethod(), targetClass, invocation::proceed); +} +``` + +重点来了,进入invokeWithinTransaction方法: + +```java +@Nullable +protected Object invokeWithinTransaction(Method method, @Nullable Class targetClass, + final InvocationCallback invocation) throws Throwable { + + // If the transaction attribute is null, the method is non-transactional. + TransactionAttributeSource tas = getTransactionAttributeSource(); + // 获取对应事务属性 + final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null); + // 获取beanFactory中的transactionManager + final PlatformTransactionManager tm = determineTransactionManager(txAttr); + // 构造方法唯一标识(类.方法,如:service.UserServiceImpl.save) + final String joinpointIdentification = methodIdentification(method, targetClass, txAttr); + + // 声明式事务处理 + if (txAttr == null || !(tm instanceof CallbackPreferringPlatformTransactionManager)) { + // 创建TransactionInfo + TransactionInfo txInfo = createTransactionIfNecessary(tm, txAttr, joinpointIdentification); + Object retVal = null; + try { + // 执行原方法 + // 继续调用方法拦截器链,这里一般将会调用目标类的方法,如:AccountServiceImpl.save方法 + retVal = invocation.proceedWithInvocation(); + } + catch (Throwable ex) { + // 异常回滚 + completeTransactionAfterThrowing(txInfo, ex); + // 手动向上抛出异常,则下面的提交事务不会执行 + // 如果子事务出异常,则外层事务代码需catch住子事务代码,不然外层事务也会回滚 + throw ex; + } + finally { + // 消除信息 + cleanupTransactionInfo(txInfo); + } + // 提交事务 + commitTransactionAfterReturning(txInfo); + return retVal; + } + + else { + final ThrowableHolder throwableHolder = new ThrowableHolder(); + try { + // 编程式事务处理 + Object result = ((CallbackPreferringPlatformTransactionManager) tm).execute(txAttr, status -> { + TransactionInfo txInfo = prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); + try { + return invocation.proceedWithInvocation(); + } + catch (Throwable ex) { + if (txAttr.rollbackOn(ex)) { + // A RuntimeException: will lead to a rollback. + if (ex instanceof RuntimeException) { + throw (RuntimeException) ex; + } + else { + throw new ThrowableHolderException(ex); + } + } + else { + // A normal return value: will lead to a commit. + throwableHolder.throwable = ex; + return null; + } + } + finally { + cleanupTransactionInfo(txInfo); + } + }); + + // Check result state: It might indicate a Throwable to rethrow. + if (throwableHolder.throwable != null) { + throw throwableHolder.throwable; + } + return result; + } + catch (ThrowableHolderException ex) { + throw ex.getCause(); + } + catch (TransactionSystemException ex2) { + if (throwableHolder.throwable != null) { + logger.error("Application exception overridden by commit exception", throwableHolder.throwable); + ex2.initApplicationException(throwableHolder.throwable); + } + throw ex2; + } + catch (Throwable ex2) { + if (throwableHolder.throwable != null) { + logger.error("Application exception overridden by commit exception", throwableHolder.throwable); + } + throw ex2; + } + } +} +``` + +## 创建事务Info对象 + +我们先分析事务创建的过程。 + +```java +protected TransactionInfo createTransactionIfNecessary(@Nullable PlatformTransactionManager tm, + @Nullable TransactionAttribute txAttr, final String joinpointIdentification) { + + // If no name specified, apply method identification as transaction name. + // 如果没有名称指定则使用方法唯一标识,并使用DelegatingTransactionAttribute封装txAttr + if (txAttr != null && txAttr.getName() == null) { + txAttr = new DelegatingTransactionAttribute(txAttr) { + @Override + public String getName() { + return joinpointIdentification; + } + }; + } + + TransactionStatus status = null; + if (txAttr != null) { + if (tm != null) { + // 获取TransactionStatus + status = tm.getTransaction(txAttr); + } + else { + if (logger.isDebugEnabled()) { + logger.debug("Skipping transactional joinpoint [" + joinpointIdentification + + "] because no transaction manager has been configured"); + } + } + } + // 根据指定的属性与status准备一个TransactionInfo + return prepareTransactionInfo(tm, txAttr, joinpointIdentification, status); +} +``` + +对于createTransactionlfNecessary函数主要做了这样几件事情。 + +(1)使用 DelegatingTransactionAttribute 封装传入的 TransactionAttribute 实例。 + +对于传入的TransactionAttribute类型的参数txAttr,当前的实际类型是RuleBasedTransactionAttribute,是由获取事务属性时生成,主要用于数据承载,而这里之所以使用DelegatingTransactionAttribute进行封装,当然是提供了更多的功能。 + +(2)获取事务。 + +事务处理当然是以事务为核心,那么获取事务就是最重要的事情。 + +(3)构建事务信息。 + +根据之前几个步骤获取的信息构建Transactionlnfo并返回。 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + +### 获取事务 + +其中核心是在getTransaction方法中: + +```java +@Override +public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException { + // 获取一个transaction + Object transaction = doGetTransaction(); + + boolean debugEnabled = logger.isDebugEnabled(); + + if (definition == null) { + definition = new DefaultTransactionDefinition(); + } + // 如果在这之前已经存在事务了,就进入存在事务的方法中 + if (isExistingTransaction(transaction)) { + return handleExistingTransaction(definition, transaction, debugEnabled); + } + + // 事务超时设置验证 + if (definition.getTimeout() < TransactionDefinition.TIMEOUT_DEFAULT) { + throw new InvalidTimeoutException("Invalid transaction timeout", definition.getTimeout()); + } + + // 走到这里说明此时没有存在事务,如果传播特性是MANDATORY时抛出异常 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { + throw new IllegalTransactionStateException( + "No existing transaction found for transaction marked with propagation 'mandatory'"); + } + // 如果此时不存在事务,当传播特性是REQUIRED或NEW或NESTED都会进入if语句块 + else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED都需要新建事务 + // 因为此时不存在事务,将null挂起 + SuspendedResourcesHolder suspendedResources = suspend(null); + if (debugEnabled) { + logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); + } + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status,存放刚刚创建的transaction,然后将其标记为新事务! + // 这里transaction后面一个参数决定是否是新事务! + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 新开一个连接的地方,非常重要 + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException | Error ex) { + resume(null, suspendedResources); + throw ex; + } + } + else { + // Create "empty" transaction: no actual transaction, but potentially synchronization. + if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT && logger.isWarnEnabled()) { + logger.warn("Custom isolation level specified but no actual transaction initiated; " + + "isolation level will effectively be ignored: " + definition); + } + // 其他的传播特性一律返回一个空事务,transaction = null + //当前不存在事务,且传播机制=PROPAGATION_SUPPORTS/PROPAGATION_NOT_SUPPORTED/PROPAGATION_NEVER,这三种情况,创建“空”事务 + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); + } +} +``` + +先来看看transaction是如何被创建出来的: + +```java +@Override +protected Object doGetTransaction() { + // 这里DataSourceTransactionObject是事务管理器的一个内部类 + // DataSourceTransactionObject就是一个transaction,这里new了一个出来 + DataSourceTransactionObject txObject = new DataSourceTransactionObject(); + txObject.setSavepointAllowed(isNestedTransactionAllowed()); + // 解绑与绑定的作用在此时体现,如果当前线程有绑定的话,将会取出holder + // 第一次conHolder肯定是null + ConnectionHolder conHolder = + (ConnectionHolder) TransactionSynchronizationManager.getResource(obtainDataSource()); + // 此时的holder被标记成一个旧holder + txObject.setConnectionHolder(conHolder, false); + return txObject; +} +``` + +创建transaction过程很简单,接着就会判断当前是否存在事务: + +```java +@Override +protected boolean isExistingTransaction(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + return (txObject.hasConnectionHolder() && txObject.getConnectionHolder().isTransactionActive()); +} + +public boolean hasConnectionHolder() { + return (this.connectionHolder != null); +} +``` + +这里判断是否存在事务的依据主要是获取holder中的transactionActive变量是否为true,如果是第一次进入事务,holder直接为null判断不存在了,如果是第二次进入事务transactionActive变量是为true的(后面会提到是在哪里把它变成true的),由此来判断当前是否已经存在事务了。 + +至此,源码分成了2条处理线: + +**1.当前已存在事务:isExistingTransaction()判断是否存在事务,存在事务handleExistingTransaction()根据不同传播机制不同处理** + +**2.当前不存在事务: 不同传播机制不同处理** + + + +## 当前不存在事务 + +如果不存在事务,传播特性又是REQUIRED或NEW或NESTED,将会先挂起null,这个挂起方法我们后面再讲,然后创建一个DefaultTransactionStatus ,并将其标记为新事务,然后执行doBegin(transaction, definition);这个方法也是一个关键方法 + +### 神秘又关键的status对象 + +**TransactionStatus接口** + +```java +public interface TransactionStatus extends SavepointManager, Flushable { + // 返回当前事务是否为新事务(否则将参与到现有事务中,或者可能一开始就不在实际事务中运行) + boolean isNewTransaction(); + // 返回该事务是否在内部携带保存点,也就是说,已经创建为基于保存点的嵌套事务。 + boolean hasSavepoint(); + // 设置事务仅回滚。 + void setRollbackOnly(); + // 返回事务是否已标记为仅回滚 + boolean isRollbackOnly(); + // 将会话刷新到数据存储区 + @Override + void flush(); + // 返回事物是否已经完成,无论提交或者回滚。 + boolean isCompleted(); +} +``` + +再来看看实现类DefaultTransactionStatus + +**DefaultTransactionStatus** + +```java +public class DefaultTransactionStatus extends AbstractTransactionStatus { + + //事务对象 + @Nullable + private final Object transaction; + + //事务对象 + private final boolean newTransaction; + + private final boolean newSynchronization; + + private final boolean readOnly; + + private final boolean debug; + + //事务对象 + @Nullable + private final Object suspendedResources; + + public DefaultTransactionStatus( + @Nullable Object transaction, boolean newTransaction, boolean newSynchronization, + boolean readOnly, boolean debug, @Nullable Object suspendedResources) { + + this.transaction = transaction; + this.newTransaction = newTransaction; + this.newSynchronization = newSynchronization; + this.readOnly = readOnly; + this.debug = debug; + this.suspendedResources = suspendedResources; + } + + //略... +} +``` + +我们看看这行代码 DefaultTransactionStatus status = **newTransactionStatus**( definition, **transaction, true**, newSynchronization, debugEnabled, **suspendedResources**); + +```java +// 这里是构造一个status对象的方法 +protected DefaultTransactionStatus newTransactionStatus( + TransactionDefinition definition, @Nullable Object transaction, boolean newTransaction, + boolean newSynchronization, boolean debug, @Nullable Object suspendedResources) { + + boolean actualNewSynchronization = newSynchronization && + !TransactionSynchronizationManager.isSynchronizationActive(); + return new DefaultTransactionStatus( + transaction, newTransaction, actualNewSynchronization, + definition.isReadOnly(), debug, suspendedResources); +} +``` + +实际上就是封装了事务属性definition,新创建的**transaction,**并且将事务状态属性设置为新事务,最后一个参数为被挂起的事务。 + +简单了解一下关键参数即可: + +第二个参数transaction:事务对象,在一开头就有创建,其就是事务管理器的一个内部类。 + +第三个参数newTransaction:布尔值,一个标识,用于判断是否是新的事务,用于提交或者回滚方法中,是新的才会提交或者回滚。 + +最后一个参数suspendedResources:被挂起的对象资源,挂起操作会返回旧的holder,将其与一些事务属性一起封装成一个对象,就是这个suspendedResources这个对象了,它会放在status中,在最后的清理工作方法中判断status中是否有这个挂起对象,如果有会恢复它 + +接着我们来看看关键代码 **doBegin(transaction, definition);** + +```java + @Override + protected void doBegin(Object transaction, TransactionDefinition definition) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection con = null; + + try { + // 判断如果transaction没有holder的话,才去从dataSource中获取一个新连接 + if (!txObject.hasConnectionHolder() || + txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + //通过dataSource获取连接 + Connection newCon = this.dataSource.getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); + } + // 所以,只有transaction中的holder为空时,才会设置为新holder + // 将获取的连接封装进ConnectionHolder,然后封装进transaction的connectionHolder属性 + txObject.setConnectionHolder(new ConnectionHolder(newCon), true); + } +      //设置新的连接为事务同步中 + txObject.getConnectionHolder().setSynchronizedWithTransaction(true); + con = txObject.getConnectionHolder().getConnection(); +      //conn设置事务隔离级别,只读 + Integer previousIsolationLevel = DataSourceUtils.prepareConnectionForTransaction(con, definition); + txObject.setPreviousIsolationLevel(previousIsolationLevel);//DataSourceTransactionObject设置事务隔离级别 + + // 如果是自动提交切换到手动提交 + if (con.getAutoCommit()) { + txObject.setMustRestoreAutoCommit(true); + if (logger.isDebugEnabled()) { + logger.debug("Switching JDBC Connection [" + con + "] to manual commit"); + } + con.setAutoCommit(false); + } +      // 如果只读,执行sql设置事务只读 + prepareTransactionalConnection(con, definition); + // 设置connection持有者的事务开启状态 + txObject.getConnectionHolder().setTransactionActive(true); + + int timeout = determineTimeout(definition); + if (timeout != TransactionDefinition.TIMEOUT_DEFAULT) { + // 设置超时秒数 + txObject.getConnectionHolder().setTimeoutInSeconds(timeout); + } + + // 将当前获取到的连接绑定到当前线程 + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.bindResource(getDataSource(), txObject.getConnectionHolder()); + } + }catch (Throwable ex) { + if (txObject.isNewConnectionHolder()) { + DataSourceUtils.releaseConnection(con, this.dataSource); + txObject.setConnectionHolder(null, false); + } + throw new CannotCreateTransactionException("Could not open JDBC Connection for transaction", ex); + } + } +``` + +conn设置事务隔离级别 + +```java +@Nullable +public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) + throws SQLException { + + Assert.notNull(con, "No Connection specified"); + + // Set read-only flag. + // 设置数据连接的只读标识 + if (definition != null && definition.isReadOnly()) { + try { + if (logger.isDebugEnabled()) { + logger.debug("Setting JDBC Connection [" + con + "] read-only"); + } + con.setReadOnly(true); + } + catch (SQLException | RuntimeException ex) { + Throwable exToCheck = ex; + while (exToCheck != null) { + if (exToCheck.getClass().getSimpleName().contains("Timeout")) { + // Assume it's a connection timeout that would otherwise get lost: e.g. from JDBC 4.0 + throw ex; + } + exToCheck = exToCheck.getCause(); + } + // "read-only not supported" SQLException -> ignore, it's just a hint anyway + logger.debug("Could not set JDBC Connection read-only", ex); + } + } + + // Apply specific isolation level, if any. + // 设置数据库连接的隔离级别 + Integer previousIsolationLevel = null; + if (definition != null && definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { + if (logger.isDebugEnabled()) { + logger.debug("Changing isolation level of JDBC Connection [" + con + "] to " + + definition.getIsolationLevel()); + } + int currentIsolation = con.getTransactionIsolation(); + if (currentIsolation != definition.getIsolationLevel()) { + previousIsolationLevel = currentIsolation; + con.setTransactionIsolation(definition.getIsolationLevel()); + } + } + + return previousIsolationLevel; +} +``` + +我们看到都是通过 Connection 去设置 + +#### 线程变量的绑定 + +我们看 doBegin 方法的47行,**将当前获取到的连接绑定到当前线程,**绑定与解绑围绕一个线程变量,此变量在**`TransactionSynchronizationManager`**类中: + +```java +private static final ThreadLocal> resources = new NamedThreadLocal<>("Transactional resources"); +``` + + 这是一个 **static final** 修饰的 线程变量,存储的是一个Map,我们来看看47行的静态方法,**bindResource** + +```java +public static void bindResource(Object key, Object value) throws IllegalStateException { + // 从上面可知,线程变量是一个Map,而这个Key就是dataSource + // 这个value就是holder + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Assert.notNull(value, "Value must not be null"); + // 获取这个线程变量Map + Map map = resources.get(); + // set ThreadLocal Map if none found + if (map == null) { + map = new HashMap<>(); + resources.set(map); + } + // 将新的holder作为value,dataSource作为key放入当前线程Map中 + Object oldValue = map.put(actualKey, value); + // Transparently suppress a ResourceHolder that was marked as void... + if (oldValue instanceof ResourceHolder && ((ResourceHolder) oldValue).isVoid()) { + oldValue = null; + } + if (oldValue != null) { + throw new IllegalStateException("Already value [" + oldValue + "] for key [" + + actualKey + "] bound to thread [" + Thread.currentThread().getName() + "]"); + } Thread.currentThread().getName() + "]"); + } + // 略... +} +``` + +### 扩充知识点 + +这里再扩充一点,mybatis中获取的数据库连接,就是根据 **dataSource** 从ThreadLocal中获取的 + +以查询举例,会调用Executor#doQuery方法: + +![](http://img.topjavaer.cn/img/202310031612318.png) + +最终会调用DataSourceUtils#doGetConnection获取,真正的数据库连接,其中TransactionSynchronizationManager中保存的就是方法调用前,spring增强方法中绑定到线程的connection,从而保证整个事务过程中connection的一致性 + +![](http://img.topjavaer.cn/img/202310031612223.png) + +![](http://img.topjavaer.cn/img/202310031612891.png) + + 我们看看TransactionSynchronizationManager.getResource(Object key)这个方法 + +```java +@Nullable +public static Object getResource(Object key) { + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Object value = doGetResource(actualKey); + if (value != null && logger.isTraceEnabled()) { + logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + + Thread.currentThread().getName() + "]"); + } + return value; +} + + @Nullable +private static Object doGetResource(Object actualKey) { + Map map = resources.get(); + if (map == null) { + return null; + } + Object value = map.get(actualKey); + // Transparently remove ResourceHolder that was marked as void... + if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { + map.remove(actualKey); + // Remove entire ThreadLocal if empty... + if (map.isEmpty()) { + resources.remove(); + } + value = null; + } + return value; +} +``` + +**就是从线程变量的Map中根据** **DataSource获取** **ConnectionHolder** + +## 已经存在的事务 + +前面已经提到,第一次事务开始时必会新创一个holder然后做绑定操作,此时线程变量是有holder的且avtive为true,如果第二个事务进来,去new一个transaction之后去线程变量中取holder,holder是不为空的且active是为true的,所以会进入handleExistingTransaction方法: + +```java + private TransactionStatus handleExistingTransaction( + TransactionDefinition definition, Object transaction, boolean debugEnabled) + throws TransactionException { +    // 1.NERVER(不支持当前事务;如果当前事务存在,抛出异常)报错 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); + } +    // 2.NOT_SUPPORTED(不支持当前事务,现有同步将被挂起)挂起当前事务,返回一个空事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { + if (debugEnabled) { + logger.debug("Suspending current transaction"); + } + // 这里会将原来的事务挂起,并返回被挂起的对象 + Object suspendedResources = suspend(transaction); + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + // 这里可以看到,第二个参数transaction传了一个空事务,第三个参数false为旧标记 + // 最后一个参数就是将前面挂起的对象封装进新的Status中,当前事务执行完后,就恢复suspendedResources + return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); + } +    // 3.REQUIRES_NEW挂起当前事务,创建新事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + if (debugEnabled) { + logger.debug("Suspending current transaction, creating new transaction with name [" + + definition.getName() + "]"); + } + // 将原事务挂起,此时新建事务,不与原事务有关系 + // 会将transaction中的holder设置为null,然后解绑! + SuspendedResourcesHolder suspendedResources = suspend(transaction); + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status出来,传入transaction,并且为新事务标记,然后传入挂起事务 + DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 这里也做了一次doBegin,此时的transaction中holer是为空的,因为之前的事务被挂起了 + // 所以这里会取一次新的连接,并且绑定! + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException beginEx) { + resumeAfterBeginException(transaction, suspendedResources, beginEx); + throw beginEx; + } + catch (Error beginErr) { + resumeAfterBeginException(transaction, suspendedResources, beginErr); + throw beginErr; + } + } +   // 如果此时的传播特性是NESTED,不会挂起事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + if (!isNestedTransactionAllowed()) { + throw new NestedTransactionNotSupportedException( + "Transaction manager does not allow nested transactions by default - " + + "specify 'nestedTransactionAllowed' property with value 'true'"); + } + if (debugEnabled) { + logger.debug("Creating nested transaction with name [" + definition.getName() + "]"); + } + // 这里如果是JTA事务管理器,就不可以用savePoint了,将不会进入此方法 + if (useSavepointForNestedTransaction()) { + // 这里不会挂起事务,说明NESTED的特性是原事务的子事务而已 + // new一个status,传入transaction,传入旧事务标记,传入挂起对象=null + DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + // 这里是NESTED特性特殊的地方,在先前存在事务的情况下会建立一个savePoint + status.createAndHoldSavepoint(); + return status; + } + else { + // JTA事务走这个分支,创建新事务 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, null); + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + } + + // 到这里PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY,存在事务加入事务即可,标记为旧事务,空挂起 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); + } +``` + +对于已经存在事务的处理过程中,我们看到了很多熟悉的操作,但是,也有些不同的地方,函数中对已经存在的事务处理考虑两种情况。 + +(1)PROPAGATION_REQUIRES_NEW表示当前方法必须在它自己的事务里运行,一个新的事务将被启动,而如果有一个事务正在运行的话,则在这个方法运行期间被挂起。而Spring中对于此种传播方式的处理与新事务建立最大的不同点在于使用suspend方法将原事务挂起。 将信息挂起的目的当然是为了在当前事务执行完毕后在将原事务还原。 + +(2)PROPAGATION_NESTED表示如果当前正有一个事务在运行中,则该方法应该运行在一个嵌套的事务中,被嵌套的事务可以独立于封装事务进行提交或者回滚,如果封装事务不存在,行为就像PROPAGATION_REQUIRES_NEW。对于嵌入式事务的处理,Spring中主要考虑了两种方式的处理。 + +- Spring中允许嵌入事务的时候,则首选设置保存点的方式作为异常处理的回滚。 +- 对于其他方式,比如JTA无法使用保存点的方式,那么处理方式与PROPAGATION_ REQUIRES_NEW相同,而一旦出现异常,则由Spring的事务异常处理机制去完成后续操作。 + +对于挂起操作的主要目的是记录原有事务的状态,以便于后续操作对事务的恢复 + + + +### 小结 + +到这里我们可以知道,在当前存在事务的情况下,根据传播特性去决定是否为新事务,是否挂起当前事务。 + +**NOT_SUPPORTED :会挂起事务,不运行doBegin方法传空`transaction`,标记为旧事务。封装`status`对象:** + +```java +return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources) +``` + + + +**REQUIRES_NEW :将会挂起事务且运行doBegin方法,标记为新事务。封装`status`对象:** + +```java +DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); +``` + +**NESTED :不会挂起事务且不会运行doBegin方法,标记为旧事务,但会创建`savePoint`。封装`status`对象:** + +```java +DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); +``` + +**其他事务例如REQUIRED :不会挂起事务,封装原有的transaction不会运行doBegin方法,标记旧事务,封装`status`对象:** + +```java +return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); +``` + +### 挂起 + +对于挂起操作的主要目的是记录原有事务的状态,以便于后续操作对事务的恢复: + +```java +@Nullable +protected final SuspendedResourcesHolder suspend(@Nullable Object transaction) throws TransactionException { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + List suspendedSynchronizations = doSuspendSynchronization(); + try { + Object suspendedResources = null; + if (transaction != null) { + // 这里是真正做挂起的方法,这里返回的是一个holder + suspendedResources = doSuspend(transaction); + } + // 这里将名称、隔离级别等信息从线程变量中取出并设置对应属性为null到线程变量 + String name = TransactionSynchronizationManager.getCurrentTransactionName(); + TransactionSynchronizationManager.setCurrentTransactionName(null); + boolean readOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(false); + Integer isolationLevel = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(null); + boolean wasActive = TransactionSynchronizationManager.isActualTransactionActive(); + TransactionSynchronizationManager.setActualTransactionActive(false); + // 将事务各个属性与挂起的holder一并封装进SuspendedResourcesHolder对象中 + return new SuspendedResourcesHolder( + suspendedResources, suspendedSynchronizations, name, readOnly, isolationLevel, wasActive); + } + catch (RuntimeException | Error ex) { + // doSuspend failed - original transaction is still active... + doResumeSynchronization(suspendedSynchronizations); + throw ex; + } + } + else if (transaction != null) { + // Transaction active but no synchronization active. + Object suspendedResources = doSuspend(transaction); + return new SuspendedResourcesHolder(suspendedResources); + } + else { + // Neither transaction nor synchronization active. + return null; + } +} +``` + + + +```java +@Override +protected Object doSuspend(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + // 将transaction中的holder属性设置为空 + txObject.setConnectionHolder(null); + // ConnnectionHolder从线程变量中解绑! + return TransactionSynchronizationManager.unbindResource(obtainDataSource()); +} +``` + +我们来看看 **unbindResource** + +```java +private static Object doUnbindResource(Object actualKey) { + // 取得当前线程的线程变量Map + Map map = resources.get(); + if (map == null) { + return null; + } + // 将key为dataSourece的value移除出Map,然后将旧的Holder返回 + Object value = map.remove(actualKey); + // Remove entire ThreadLocal if empty... + // 如果此时map为空,直接清除线程变量 + if (map.isEmpty()) { + resources.remove(); + } + // Transparently suppress a ResourceHolder that was marked as void... + if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { + value = null; + } + if (value != null && logger.isTraceEnabled()) { + logger.trace("Removed value [" + value + "] for key [" + actualKey + "] from thread [" + + Thread.currentThread().getName() + "]"); + } + // 将旧Holder返回 + return value; +} +``` + +可以回头看一下解绑操作的介绍。这里挂起主要干了三件事: + +1. **将transaction中的holder属性设置为空** +2. **解绑(会返回线程中的那个旧的holder出来,从而封装到SuspendedResourcesHolder对象中)** +3. **将SuspendedResourcesHolder放入status中,方便后期子事务完成后,恢复外层事务** + + + +最后给大家分享一个Github仓库,上面有大彬整理的**300多本经典的计算机书籍PDF**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,可以star一下,下次找书直接在上面搜索,仓库持续更新中~ + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +[**Github地址**](https://github.com/Tyson0314/java-books) + +如果访问不了Github,可以访问码云地址。 + +[码云地址](https://gitee.com/tysondai/java-books) \ No newline at end of file diff --git a/docs/source/spring/19-transaction-rollback-commit.md b/docs/source/spring/19-transaction-rollback-commit.md new file mode 100644 index 0000000..fbb7f2d --- /dev/null +++ b/docs/source/spring/19-transaction-rollback-commit.md @@ -0,0 +1,975 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,事物回滚,事物提交,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + +**正文** + +上一篇文章讲解了获取事务,并且通过获取的connection设置只读、隔离级别等,这篇文章讲解剩下的事务的回滚和提交。[最全面的Java面试网站](https://topjavaer.cn) + +## 回滚处理 + +之前已经完成了目标方法运行前的事务准备工作,而这些准备工作最大的目的无非是对于程序没有按照我们期待的那样进行,也就是出现特定的错误,那么,当出现错误的时候,Spring是怎么对数据进行恢复的呢? + +```java + protected void completeTransactionAfterThrowing(@Nullable TransactionInfo txInfo, Throwable ex) { + // 当抛出异常时首先判断当前是否存在事务,这是基础依据 + if (txInfo != null && txInfo.getTransactionStatus() != null) { + if (logger.isTraceEnabled()) { + logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + + "] after exception: " + ex); + } + // 这里判断是否回滚默认的依据是抛出的异常是否是RuntimeException或者是Error的类型 + if (txInfo.transactionAttribute != null && txInfo.transactionAttribute.rollbackOn(ex)) { + try { + txInfo.getTransactionManager().rollback(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) { + logger.error("Application exception overridden by rollback exception", ex); + ex2.initApplicationException(ex); + throw ex2; + } + catch (RuntimeException | Error ex2) { + logger.error("Application exception overridden by rollback exception", ex); + throw ex2; + } + } + else { + // We don't roll back on this exception. + // Will still roll back if TransactionStatus.isRollbackOnly() is true. + // 如果不满足回滚条件即使抛出异常也同样会提交 + try { + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } + catch (TransactionSystemException ex2) { + logger.error("Application exception overridden by commit exception", ex); + ex2.initApplicationException(ex); + throw ex2; + } + catch (RuntimeException | Error ex2) { + logger.error("Application exception overridden by commit exception", ex); + throw ex2; + } + } + } + } +``` + +在对目标方法的执行过程中,一旦出现Throwable就会被引导至此方法处理,但是并不代表所有的Throwable都会被回滚处理,比如我们最常用的Exception,默认是不会被处理的。 默认情况下,即使出现异常,数据也会被正常提交,而这个关键的地方就是在txlnfo.transactionAttribute.rollbackOn(ex)这个函数。 + +### 回滚条件 + +```java +@Override +public boolean rollbackOn(Throwable ex) { + return (ex instanceof RuntimeException || ex instanceof Error); +} +``` + +默认情况下Spring中的亊务异常处理机制只对RuntimeException和Error两种情况感兴趣,我们可以利用注解方式来改变,例如: + +```java +@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) +``` + +### 回滚处理 + +当然,一旦符合回滚条件,那么Spring就会将程序引导至回滚处理函数中。 + +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd +> +> + +```java +private void processRollback(DefaultTransactionStatus status, boolean unexpected) { + try { + boolean unexpectedRollback = unexpected; + + try { + triggerBeforeCompletion(status); + // 如果status有savePoint,说明此事务是NESTD,且为子事务,只回滚到savePoint + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Rolling back transaction to savepoint"); + } + //回滚到保存点 + status.rollbackToHeldSavepoint(); + } + // 如果此时的status显示是新的事务才进行回滚 + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction rollback"); + } + //如果此时是子事务,我们想想哪些类型的事务会进入到这里? + //回顾上一篇文章中已存在事务的处理,NOT_SUPPORTED创建的Status是prepareTransactionStatus(definition, null, false...),说明是旧事物,并且事务为null,不会进入 + //REQUIRES_NEW会创建一个新的子事务,Status是newTransactionStatus(definition, transaction, true...)说明是新事务,将会进入到这个分支 + //PROPAGATION_NESTED创建的Status是prepareTransactionStatus(definition, transaction, false...)是旧事物,使用的是外层的事务,不会进入 + //PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY存在事务加入事务即可,标记为旧事务,prepareTransactionStatus(definition, transaction, false..) + //说明当子事务,只有REQUIRES_NEW会进入到这里进行回滚 + doRollback(status); + } + else { + // Participating in larger transaction + // 如果status中有事务,进入下面 + // 根据上面分析,PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY创建的Status是prepareTransactionStatus(definition, transaction, false..) + // 如果此事务时子事务,表示存在事务,并且事务为旧事物,将进入到这里 + if (status.hasTransaction()) { + if (status.isLocalRollbackOnly() || isGlobalRollbackOnParticipationFailure()) { + if (status.isDebug()) { + logger.debug("Participating transaction failed - marking existing transaction as rollback-only"); + } + // 对status中的transaction作一个回滚了的标记,并不会立即回滚 + doSetRollbackOnly(status); + } + else { + if (status.isDebug()) { + logger.debug("Participating transaction failed - letting transaction originator decide on rollback"); + } + } + } + else { + logger.debug("Should roll back transaction but cannot - no transaction available"); + } + // Unexpected rollback only matters here if we're asked to fail early + if (!isFailEarlyOnGlobalRollbackOnly()) { + unexpectedRollback = false; + } + } + } + catch (RuntimeException | Error ex) { + triggerAfterCompletion(status, TransactionSynchronization.STATUS_UNKNOWN); + throw ex; + } + + triggerAfterCompletion(status, TransactionSynchronization.STATUS_ROLLED_BACK); + + } + finally { + // 清空记录的资源并将挂起的资源恢复 + // 子事务结束了,之前挂起的事务就要恢复了 + cleanupAfterCompletion(status); + } +} +``` + + + +我i们先来看看第13行,回滚到保存点的代码,根据保存点回滚的实现方式其实是根据底层的数据库连接进行的。回滚到保存点之后,也要释放掉当前的保存点 + +```java +public void rollbackToHeldSavepoint() throws TransactionException { + Object savepoint = getSavepoint(); + if (savepoint == null) { + throw new TransactionUsageException( + "Cannot roll back to savepoint - no savepoint associated with current transaction"); + } + getSavepointManager().rollbackToSavepoint(savepoint); + getSavepointManager().releaseSavepoint(savepoint); + setSavepoint(null); +} +``` + +这里使用的是JDBC的方式进行数据库连接,那么getSavepointManager()函数返回的是JdbcTransactionObjectSupport,也就是说上面函数会调用JdbcTransactionObjectSupport 中的 rollbackToSavepoint 方法。 + +```java +@Override +public void rollbackToSavepoint(Object savepoint) throws TransactionException { + ConnectionHolder conHolder = getConnectionHolderForSavepoint(); + try { + conHolder.getConnection().rollback((Savepoint) savepoint); + conHolder.resetRollbackOnly(); + } + catch (Throwable ex) { + throw new TransactionSystemException("Could not roll back to JDBC savepoint", ex); + } +} +``` + +当之前已经保存的事务信息中的事务为新事物,那么直接回滚。常用于单独事务的处理。对于没有保存点的回滚,Spring同样是使用底层数据库连接提供的API来操作的。由于我们使用的是DataSourceTransactionManager,那么doRollback函数会使用此类中的实现: + +```java +@Override +protected void doRollback(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Rolling back JDBC transaction on Connection [" + con + "]"); + } + try { + con.rollback(); + } + catch (SQLException ex) { + throw new TransactionSystemException("Could not roll back JDBC transaction", ex); + } +} +``` + +当前事务信息中表明是存在事务的,又不属于以上两种情况,只做回滚标识,等到提交的时候再判断是否有回滚标识,下面回滚的时候再介绍,子事务中状态为**PROPAGATION_SUPPORTS** 或 **PROPAGATION_REQUIRED**或**PROPAGATION_MANDATORY**回滚的时候将会标记为回滚标识,我们来看看是怎么标记的 + +```java +@Override +protected void doSetRollbackOnly(DefaultTransactionStatus status) { + // 将status中的transaction取出 + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + + "] rollback-only"); + } + // transaction执行标记回滚 + txObject.setRollbackOnly(); +} +public void setRollbackOnly() { + // 这里将transaction里面的connHolder标记回滚 + getConnectionHolder().setRollbackOnly(); +} +public void setRollbackOnly() { + // 将holder中的这个属性设置成true + this.rollbackOnly = true; +} +``` + +我们看到将status中的Transaction中的 ConnectionHolder的属性**rollbackOnly标记为true,**这里我们先不多考虑,等到下面提交的时候再介绍 + +我们简单的做个小结 + +- **status.hasSavepoint()如果status中有savePoint,只回滚到savePoint!** +- **status.isNewTransaction()如果status是一个新事务,才会真正去回滚!** +- **status.hasTransaction()如果status有事务,将会对staus中的事务标记!** + + + +## 事务提交 + +在事务的执行并没有出现任何的异常,也就意味着事务可以走正常事务提交的流程了。这里回到流程中去,看看`commitTransactionAfterReturning(txInfo)`方法做了什么: + +```java +protected void commitTransactionAfterReturning(@Nullable TransactionInfo txInfo) { + if (txInfo != null && txInfo.getTransactionStatus() != null) { + if (logger.isTraceEnabled()) { + logger.trace("Completing transaction for [" + txInfo.getJoinpointIdentification() + "]"); + } + txInfo.getTransactionManager().commit(txInfo.getTransactionStatus()); + } +} +``` + +在真正的数据提交之前,还需要做个判断。不知道大家还有没有印象,在我们分析事务异常处理规则的时候,当某个事务既没有保存点又不是新事物,Spring对它的处理方式只是设置一个回滚标识。这个回滚标识在这里就会派上用场了,如果子事务状态是 + +**PROPAGATION_SUPPORTS** 或 **PROPAGATION_REQUIRED**或**PROPAGATION_MANDATORY,**将会在外层事务中运行,回滚的时候,并不执行回滚,只是标记一下回滚状态,当外层事务提交的时候,会先判断ConnectionHolder中的回滚状态,如果已经标记为回滚,则不会提交,而是外层事务进行回滚 + +```java +@Override +public final void commit(TransactionStatus status) throws TransactionException { + if (status.isCompleted()) { + throw new IllegalTransactionStateException( + "Transaction is already completed - do not call commit or rollback more than once per transaction"); + } + + DefaultTransactionStatus defStatus = (DefaultTransactionStatus) status; + // 如果在事务链中已经被标记回滚,那么不会尝试提交事务,直接回滚 + if (defStatus.isLocalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Transactional code has requested rollback"); + } + processRollback(defStatus, false); + return; + } + + if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Global transaction is marked as rollback-only but transactional code requested commit"); + } + // 这里会进行回滚,并且抛出一个异常 + processRollback(defStatus, true); + return; + } + + // 如果没有被标记回滚之类的,这里才真正判断是否提交 + processCommit(defStatus); + } +``` + +而当事务执行一切都正常的时候,便可以真正地进入提交流程了。 + +```java +private void processCommit(DefaultTransactionStatus status) throws TransactionException { + try { + boolean beforeCompletionInvoked = false; + + try { + boolean unexpectedRollback = false; + prepareForCommit(status); + triggerBeforeCommit(status); + triggerBeforeCompletion(status); + beforeCompletionInvoked = true; + + // 判断是否有savePoint + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Releasing transaction savepoint"); + } + unexpectedRollback = status.isGlobalRollbackOnly(); + // 不提交,仅仅是释放savePoint + status.releaseHeldSavepoint(); + } + // 判断是否是新事务 + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction commit"); + } + unexpectedRollback = status.isGlobalRollbackOnly(); + // 这里才真正去提交! + doCommit(status); + } + else if (isFailEarlyOnGlobalRollbackOnly()) { + unexpectedRollback = status.isGlobalRollbackOnly(); + } + + // Throw UnexpectedRollbackException if we have a global rollback-only + // marker but still didn't get a corresponding exception from commit. + if (unexpectedRollback) { + throw new UnexpectedRollbackException( + "Transaction silently rolled back because it has been marked as rollback-only"); + } + } + catch (UnexpectedRollbackException ex) { + // 略... + } + + // Trigger afterCommit callbacks, with an exception thrown there + // propagated to callers but the transaction still considered as committed. + try { + triggerAfterCommit(status); + } + finally { + triggerAfterCompletion(status, TransactionSynchronization.STATUS_COMMITTED); + } + + } + finally { + // 清空记录的资源并将挂起的资源恢复 + cleanupAfterCompletion(status); + } +} +``` + + + +- status.hasSavepoint()如果status有savePoint,说明此时的事务是嵌套事务NESTED,这个事务外面还有事务,这里不提交,只是释放保存点。这里也可以看出来NESTED的传播行为了。 +- status.isNewTransaction()如果是新的事务,才会提交!!,这里如果是子事务,只有**PROPAGATION_NESTED**状态才会走到这里提交,也说明了此状态子事务提交和外层事务是隔离的 +- **如果是子事务,PROPAGATION_SUPPORTS** 或 **PROPAGATION_REQUIRED**或**PROPAGATION_MANDATORY**这几种状态是旧事物,提交的时候将什么都不做,因为他们是运行在外层事务当中,如果子事务没有回滚,将由外层事务一次性提交 + +如果程序流通过了事务的层层把关,最后顺利地进入了提交流程,那么同样,Spring会将事务提交的操作引导至底层数据库连接的API,进行事务提交。 + +```java +@Override +protected void doCommit(DefaultTransactionStatus status) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + Connection con = txObject.getConnectionHolder().getConnection(); + if (status.isDebug()) { + logger.debug("Committing JDBC transaction on Connection [" + con + "]"); + } + try { + con.commit(); + } + catch (SQLException ex) { + throw new TransactionSystemException("Could not commit JDBC transaction", ex); + } +} +``` + +**从回滚和提交的逻辑看,只有status是新事务,才会进行提交或回滚,需要读者记好这个状态–>是否是新事务。** + + + +## 清理工作 + +而无论是在异常还是没有异常的流程中,最后的finally块中都会执行一个方法**cleanupAfterCompletion(status)** + +```java +private void cleanupAfterCompletion(DefaultTransactionStatus status) { + // 设置完成状态 + status.setCompleted(); + if (status.isNewSynchronization()) { + TransactionSynchronizationManager.clear(); + } + if (status.isNewTransaction()) { + doCleanupAfterCompletion(status.getTransaction()); + } + if (status.getSuspendedResources() != null) { + if (status.isDebug()) { + logger.debug("Resuming suspended transaction after completion of inner transaction"); + } + Object transaction = (status.hasTransaction() ? status.getTransaction() : null); + // 结束之前事务的挂起状态 + resume(transaction, (SuspendedResourcesHolder) status.getSuspendedResources()); + } +} +``` + +如果是新事务需要做些清除资源的工作? + +```java +@Override +protected void doCleanupAfterCompletion(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + + // Remove the connection holder from the thread, if exposed. + if (txObject.isNewConnectionHolder()) { + // 将数据库连接从当前线程中解除绑定,解绑过程我们在挂起的过程中已经分析过 + TransactionSynchronizationManager.unbindResource(obtainDataSource()); + } + + // Reset connection. + // 释放连接,当前事务完成,则需要将连接释放,如果有线程池,则重置数据库连接,放回线程池 + Connection con = txObject.getConnectionHolder().getConnection(); + try { + if (txObject.isMustRestoreAutoCommit()) { + // 恢复数据库连接的自动提交属性 + con.setAutoCommit(true); + } + // 重置数据库连接 + DataSourceUtils.resetConnectionAfterTransaction(con, txObject.getPreviousIsolationLevel()); + } + catch (Throwable ex) { + logger.debug("Could not reset JDBC Connection after transaction", ex); + } + + if (txObject.isNewConnectionHolder()) { + if (logger.isDebugEnabled()) { + logger.debug("Releasing JDBC Connection [" + con + "] after transaction"); + } + // 如果当前事务是独立的新创建的事务则在事务完成时释放数据库连接 + DataSourceUtils.releaseConnection(con, this.dataSource); + } + + txObject.getConnectionHolder().clear(); +} +``` + +如果在事务执行前有事务挂起,那么当前事务执行结束后需要将挂起事务恢复。 + +如果有挂起的事务的话,`status.getSuspendedResources() != null`,也就是说status中会有suspendedResources这个属性,取得status中的transaction后进入resume方法: + +```java +protected final void resume(@Nullable Object transaction, @Nullable SuspendedResourcesHolder resourcesHolder) +throws TransactionException { + + if (resourcesHolder != null) { + Object suspendedResources = resourcesHolder.suspendedResources; + // 如果有被挂起的事务才进入 + if (suspendedResources != null) { + // 真正去resume恢复的地方 + doResume(transaction, suspendedResources); + } + List suspendedSynchronizations = resourcesHolder.suspendedSynchronizations; + if (suspendedSynchronizations != null) { + // 将上面提到的TransactionSynchronizationManager专门存放线程变量的类中 + // 的属性设置成被挂起事务的属性 + TransactionSynchronizationManager.setActualTransactionActive(resourcesHolder.wasActive); + TransactionSynchronizationManager.setCurrentTransactionIsolationLevel(resourcesHolder.isolationLevel); + TransactionSynchronizationManager.setCurrentTransactionReadOnly(resourcesHolder.readOnly); + TransactionSynchronizationManager.setCurrentTransactionName(resourcesHolder.name); + doResumeSynchronization(suspendedSynchronizations); + } + } +} +``` + +我们来看看doResume + +```java +@Override +protected void doResume(@Nullable Object transaction, Object suspendedResources) { + TransactionSynchronizationManager.bindResource(obtainDataSource(), suspendedResources); +} +``` + +这里恢复只是把`suspendedResources`重新绑定到线程中。 + + + +## 几种事务传播属性详解 + +我们先来看看七种传播属性 + +Spring事物传播特性表: + +| 传播特性名称 | 说明 | +| ------------------------- | ------------------------------------------------------------ | +| PROPAGATION_REQUIRED | 如果当前没有事物,则新建一个事物;如果已经存在一个事物,则加入到这个事物中 | +| PROPAGATION_SUPPORTS | 支持当前事物,如果当前没有事物,则以非事物方式执行 | +| PROPAGATION_MANDATORY | 使用当前事物,如果当前没有事物,则抛出异常 | +| PROPAGATION_REQUIRES_NEW | 新建事物,如果当前已经存在事物,则挂起当前事物 | +| PROPAGATION_NOT_SUPPORTED | 以非事物方式执行,如果当前存在事物,则挂起当前事物 | +| PROPAGATION_NEVER | 以非事物方式执行,如果当前存在事物,则抛出异常 | +| PROPAGATION_NESTED | 如果当前存在事物,则在嵌套事物内执行;如果当前没有事物,则与PROPAGATION_REQUIRED传播特性相同 | + + + +### 当前不存在事务的情况下 + +每次创建一个TransactionInfo的时候都会去new一个transaction,然后去线程变量Map中拿holder,当此时线程变量的Map中holder为空时,就视为当前情况下不存在事务,所以此时transaction中holder = null。 + +**1、PROPAGATION_MANDATORY** + +**使用当前事物,如果当前没有事物,则抛出异常** + +在上一篇博文中我们在getTransaction方法中可以看到如下代码,当前线程不存在事务时,如果传播属性为PROPAGATION_MANDATORY,直接抛出异常,因为PROPAGATION_MANDATORY必须要在事务中运行 + +```java +if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) { + throw new IllegalTransactionStateException( + "No existing transaction found for transaction marked with propagation 'mandatory'"); +} +``` + +**2、REQUIRED、REQUIRES_NEW、NESTED** + +我们继续看上一篇博文中的getTransaction方法 + +```java +else if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW || + definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + // PROPAGATION_REQUIRED、PROPAGATION_REQUIRES_NEW、PROPAGATION_NESTED都需要新建事务 + // 因为此时不存在事务,将null挂起 + SuspendedResourcesHolder suspendedResources = suspend(null); + if (debugEnabled) { + logger.debug("Creating new transaction with name [" + definition.getName() + "]: " + definition); + } + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status,存放刚刚创建的transaction,然后将其标记为新事务! + // 这里transaction后面一个参数决定是否是新事务! + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 新开一个连接的地方,非常重要 + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException | Error ex) { + resume(null, suspendedResources); + throw ex; + } +} +``` + + + +此时会讲null挂起,此时的status变量为: + +```java +DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); +``` + +此时的transaction中holder依然为null,标记为新事务,接着就会执行doBegin方法了: + +```java +@Override +protected void doBegin(Object transaction, TransactionDefinition definition) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + Connection con = null; + + // 此时会进入这个if语句块,因为此时的holder依然为null + if (!txObject.hasConnectionHolder() || + txObject.getConnectionHolder().isSynchronizedWithTransaction()) { + // 从dataSource从取得一个新的connection + Connection newCon = obtainDataSource().getConnection(); + if (logger.isDebugEnabled()) { + logger.debug("Acquired Connection [" + newCon + "] for JDBC transaction"); + } + // new一个新的holder放入新的连接,设置为新的holder + txObject.setConnectionHolder(new ConnectionHolder(newCon), true); + } + + // 略... + + prepareTransactionalConnection(con, definition); + // 将holder设置avtive = true + txObject.getConnectionHolder().setTransactionActive(true); + + // Bind the connection holder to the thread. + // 绑定到当前线程 + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.bindResource(obtainDataSource(), txObject.getConnectionHolder()); + } + } +} +``` + + + +所以,一切都是新的,新的事务,新的holder,新的连接,在当前不存在事务的时候一切都是新创建的。 + +这三种传播特性在当前不存在事务的情况下是没有区别的,此事务都为新创建的连接,在回滚和提交的时候都可以正常回滚或是提交,就像正常的事务操作那样。 + +**3、PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER** + +我们看看当传播属性为PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER这几种时的代码,getTransaction方法 + +```java +else { + //其他的传播特性一律返回一个空事务,transaction = null + //当前不存在事务,且传播机制=PROPAGATION_SUPPORTS/PROPAGATION_NOT_SUPPORTED/PROPAGATION_NEVER,这三种情况,创建“空”事务 + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + return prepareTransactionStatus(definition, null, true, newSynchronization, debugEnabled, null); +} +``` + + + +我们看到Status中第二个参数传的是**null**,表示一个空事务,意思是当前线程中并没有Connection,那如何进行数据库的操作呢?上一篇文章中我们有一个扩充的知识点,Mybaits中使用的数据库连接是从通过**`TransactionSynchronizationManager.getResource(Object key)获取spring增强方法中绑定到线程的connection,`**`如下代码,那当传播属性为PROPAGATION_SUPPORTS、PROPAGATION_NOT_SUPPORTED、PROPAGATION_NEVER这几种时,并没有创建新的Connection,当前线程中也没有绑定Connection,那Mybatis是如何获取Connecion的呢?这里留一个疑问,我们后期看Mybatis的源码的时候来解决这个疑问` + +```java +@Nullable +public static Object getResource(Object key) { + Object actualKey = TransactionSynchronizationUtils.unwrapResourceIfNecessary(key); + Object value = doGetResource(actualKey); + if (value != null && logger.isTraceEnabled()) { + logger.trace("Retrieved value [" + value + "] for key [" + actualKey + "] bound to thread [" + + Thread.currentThread().getName() + "]"); + } + return value; +} + + @Nullable +private static Object doGetResource(Object actualKey) { + Map map = resources.get(); + if (map == null) { + return null; + } + Object value = map.get(actualKey); + // Transparently remove ResourceHolder that was marked as void... + if (value instanceof ResourceHolder && ((ResourceHolder) value).isVoid()) { + map.remove(actualKey); + // Remove entire ThreadLocal if empty... + if (map.isEmpty()) { + resources.remove(); + } + value = null; + } + return value; +} +``` + + + +此时我们知道Status中的Transaction为null,在目标方法执行完毕后,进行回滚或提交的时候,会判断当前事务是否是新事务,代码如下 + +```java +@Override +public boolean isNewTransaction() { + return (hasTransaction() && this.newTransaction); +} +``` + +**此时transacion为null,回滚或提交的时候将什么也不做** + + + +### 当前存在事务情况下 + +上一篇文章中已经讲过,第一次事务开始时必会新创一个holder然后做绑定操作,此时线程变量是有holder的且avtive为true,如果第二个事务进来,去new一个transaction之后去线程变量中取holder,holder是不为空的且active是为true的,所以会进入**handleExistingTransaction**方法: + +```java + private TransactionStatus handleExistingTransaction( + TransactionDefinition definition, Object transaction, boolean debugEnabled) + throws TransactionException { +    // 1.NERVER(不支持当前事务;如果当前事务存在,抛出异常)报错 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); + } +    // 2.NOT_SUPPORTED(不支持当前事务,现有同步将被挂起)挂起当前事务,返回一个空事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { + if (debugEnabled) { + logger.debug("Suspending current transaction"); + } + // 这里会将原来的事务挂起,并返回被挂起的对象 + Object suspendedResources = suspend(transaction); + boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS); + // 这里可以看到,第二个参数transaction传了一个空事务,第三个参数false为旧标记 + // 最后一个参数就是将前面挂起的对象封装进新的Status中,当前事务执行完后,就恢复suspendedResources + return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); + } +    // 3.REQUIRES_NEW挂起当前事务,创建新事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) { + if (debugEnabled) { + logger.debug("Suspending current transaction, creating new transaction with name [" + + definition.getName() + "]"); + } + // 将原事务挂起,此时新建事务,不与原事务有关系 + // 会将transaction中的holder设置为null,然后解绑! + SuspendedResourcesHolder suspendedResources = suspend(transaction); + try { + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + // new一个status出来,传入transaction,并且为新事务标记,然后传入挂起事务 + DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); + // 这里也做了一次doBegin,此时的transaction中holer是为空的,因为之前的事务被挂起了 + // 所以这里会取一次新的连接,并且绑定! + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + catch (RuntimeException beginEx) { + resumeAfterBeginException(transaction, suspendedResources, beginEx); + throw beginEx; + } + catch (Error beginErr) { + resumeAfterBeginException(transaction, suspendedResources, beginErr); + throw beginErr; + } + } +   // 如果此时的传播特性是NESTED,不会挂起事务 + if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) { + if (!isNestedTransactionAllowed()) { + throw new NestedTransactionNotSupportedException( + "Transaction manager does not allow nested transactions by default - " + + "specify 'nestedTransactionAllowed' property with value 'true'"); + } + if (debugEnabled) { + logger.debug("Creating nested transaction with name [" + definition.getName() + "]"); + } + // 这里如果是JTA事务管理器,就不可以用savePoint了,将不会进入此方法 + if (useSavepointForNestedTransaction()) { + // 这里不会挂起事务,说明NESTED的特性是原事务的子事务而已 + // new一个status,传入transaction,传入旧事务标记,传入挂起对象=null + DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + // 这里是NESTED特性特殊的地方,在先前存在事务的情况下会建立一个savePoint + status.createAndHoldSavepoint(); + return status; + } + else { + // JTA事务走这个分支,创建新事务 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + DefaultTransactionStatus status = newTransactionStatus( + definition, transaction, true, newSynchronization, debugEnabled, null); + doBegin(transaction, definition); + prepareSynchronization(status, definition); + return status; + } + } + + // 到这里PROPAGATION_SUPPORTS 或 PROPAGATION_REQUIRED或PROPAGATION_MANDATORY,存在事务加入事务即可,标记为旧事务,空挂起 + boolean newSynchronization = (getTransactionSynchronization() != SYNCHRONIZATION_NEVER); + return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); + } +``` + + + +**1、NERVER** + +**不支持当前事务;如果当前事务存在,抛出异常** + +```java +// 1.NERVER(不支持当前事务;如果当前事务存在,抛出异常)报错 +if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) { + throw new IllegalTransactionStateException( + "Existing transaction found for transaction marked with propagation 'never'"); +} +``` + + + +我们看到如果当前线程中存在事务,传播属性为**PROPAGATION_NEVER,会直接抛出异常** + +**2、NOT_SUPPORTED** + +**以非事物方式执行,如果当前存在事物,则挂起当前事物** + +我们看上面代码第9行,如果传播属性为PROPAGATION_NOT_SUPPORTED,会先将原来的transaction挂起,此时status为: + +```java +return prepareTransactionStatus(definition, null, false, newSynchronization, debugEnabled, suspendedResources); +``` + +transaction为空,旧事务,挂起的对象存入status中。 + +**此时与外层事务隔离了,在这种传播特性下,是不进行事务的,当提交时,因为是旧事务,所以不会commit,失败时也不会回滚rollback** + +**3、REQUIRES_NEW** + +此时会先挂起,然后去执行doBegin方法,此时会创建一个新连接,新holder,新holder有什么用呢? + +如果是新holder,会在doBegin中做绑定操作,将新holder绑定到当前线程,其次,在提交或是回滚时finally语句块始终会执行清理方法时判断新holder会进行解绑操作。 + +```java +@Override +protected void doCleanupAfterCompletion(Object transaction) { + DataSourceTransactionObject txObject = (DataSourceTransactionObject) transaction; + + // Remove the connection holder from the thread, if exposed. + if (txObject.isNewConnectionHolder()) { + TransactionSynchronizationManager.unbindResource(obtainDataSource()); + } +} +``` + + + +符合传播特性,所以这里**REQUIRES_NEW**这个传播特性是与原事务相隔的,用的连接都是新new出来的。 + +此时返回的status是这样的: + +```java +DefaultTransactionStatus status = newTransactionStatus(definition, transaction, true, newSynchronization, debugEnabled, suspendedResources); +``` + +其中transaction中holder为新holder,连接都是新的。标记为新事务,在开头的回顾中提到,如果是新事务,提交时才能成功提交。并且在最后一个参数放入挂起的对象,之后将会恢复它。 + +**REQUIRES_NEW小结** + +会于前一个事务隔离,自己新开一个事务,与上一个事务无关,如果报错,上一个事务catch住异常,上一个事务是不会回滚的,这里要注意**(在invokeWithinTransaction方法中的catch代码块中,处理完异常后,还通过** **throw ex;将异常抛给了上层,所以上层要catch住子事务的异常,子事务回滚后,上层事务也会回滚),**而只要自己提交了之后,就算上一个事务后面的逻辑报错,自己是不会回滚的(因为被标记为新事务,所以在提交阶段已经提交了)。 + +**4、NESTED** + +不挂起事务,并且返回的status对象如下: + +```java +DefaultTransactionStatus status =prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null); + +status.createAndHoldSavepoint(); +``` + +不同于其他的就是此传播特性会创建savePoint,有什么用呢?前面说到,如果是旧事务的话回滚是不会执行的,但先看看它的status,虽然标记为旧事务,但它还有savePoint,如果有savePoint,会回滚到保存点去,提交的时候,会释放保存点,但是不提交!切记,这里就是NESTED与REQUIRES_NEW不同点之一了,NESTED只会在外层事务成功时才进行提交,实际提交点只是去释放保存点,外层事务失败,NESTED也将回滚,但如果是REQUIRES_NEW的话,不管外层事务是否成功,它都会提交不回滚。这就是savePoint的作用。 + +由于不挂起事务,可以看出来,此时transaction中的holder用的还是旧的,连接也是上一个事务的连接,可以看出来,这个传播特性会将原事务和自己当成一个事务来做。 + +**NESTED 小结** + +与前一个事务不隔离,没有新开事务,用的也是老transaction,老的holder,同样也是老的connection,没有挂起的事务。关键点在这个传播特性在存在事务情况下会创建savePoint,但不存在事务情况下是不会创建savePoint的。在提交时不真正提交,只是释放了保存点而已,在回滚时会回滚到保存点位置,如果上层事务catch住异常的话,是不会影响上层事务的提交的,外层事务提交时,会统一提交,外层事务回滚的话,会全部回滚 + +**5、REQUIRED 、PROPAGATION_REQUIRED或PROPAGATION_MANDATORY** + +**存在事务加入事务即可,标记为旧事务,空挂起** + +status为: + +```java +return prepareTransactionStatus(definition, transaction, false, newSynchronization, debugEnabled, null); +``` + +使用旧事务,标记为旧事务,挂起对象为空。 + +与前一个事务不隔离,没有新开事务,用的也是老transaction,老的connection,但此时被标记成旧事务,所以,在提交阶段不会真正提交的,在外层事务提交阶段,才会把事务提交。 + +如果此时这里出现了异常,内层事务执行回滚时,旧事务是不会去回滚的,而是进行回滚标记,我们看看文章开头处回滚的处理函数**processRollback中第39行,当前事务信息中表明是存在事务的,但是既没有保存点,又不是新事务,回滚的时候只做回滚标识,等到提交的时候再判断是否有回滚标识,commit的时候,如果有回滚标识,就进行回滚** + +```java +@Override +protected void doSetRollbackOnly(DefaultTransactionStatus status) { + // 将status中的transaction取出 + DataSourceTransactionObject txObject = (DataSourceTransactionObject) status.getTransaction(); + if (status.isDebug()) { + logger.debug("Setting JDBC transaction [" + txObject.getConnectionHolder().getConnection() + + "] rollback-only"); + } + // transaction执行标记回滚 + txObject.setRollbackOnly(); +} +public void setRollbackOnly() { + // 这里将transaction里面的connHolder标记回滚 + getConnectionHolder().setRollbackOnly(); +} +public void setRollbackOnly() { + // 将holder中的这个属性设置成true + this.rollbackOnly = true; +} +``` + +我们知道,在内层事务中transaction对象中的holder对象其实就是外层事务transaction里的holder,holder是一个对象,指向同一个地址,在这里设置holder标记,外层事务transaction中的holder也是会被设置到的,在外层事务提交的时候有这样一段代码: + +```java +@Override +public final void commit(TransactionStatus status) throws TransactionException { + // 略... + + // !shouldCommitOnGlobalRollbackOnly()只有JTA与JPA事务管理器才会返回false + // defStatus.isGlobalRollbackOnly()这里判断status是否被标记了 + if (!shouldCommitOnGlobalRollbackOnly() && defStatus.isGlobalRollbackOnly()) { + if (defStatus.isDebug()) { + logger.debug("Global transaction is marked as rollback-only but transactional code requested commit"); + } + // 如果内层事务抛异常,外层事务是会走到这个方法中的,而不是去提交 + processRollback(defStatus, true); + return; + } + + // 略... +} +``` + +在外层事务提交的时候是会去验证transaction中的holder里是否被标记rollback了,内层事务回滚,将会标记holder,而holder是线程变量,在此传播特性中holder是同一个对象,外层事务将无法正常提交而进入processRollback方法进行回滚,并抛出异常: + +```java +private void processRollback(DefaultTransactionStatus status, boolean unexpected) { + try { + // 此时这个值为true + boolean unexpectedRollback = unexpected; + + try { + triggerBeforeCompletion(status); + + if (status.hasSavepoint()) { + if (status.isDebug()) { + logger.debug("Rolling back transaction to savepoint"); + } + status.rollbackToHeldSavepoint(); + } + // 新事务,将进行回滚操作 + else if (status.isNewTransaction()) { + if (status.isDebug()) { + logger.debug("Initiating transaction rollback"); + } + // 回滚! + doRollback(status); + } + + // 略... + + // Raise UnexpectedRollbackException if we had a global rollback-only marker + // 抛出一个异常 + if (unexpectedRollback) { + // 这个就是上文说到的抛出的异常类型 + throw new UnexpectedRollbackException( + "Transaction rolled back because it has been marked as rollback-only"); + } + } + finally { + cleanupAfterCompletion(status); + } +} +``` + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247486208&idx=1&sn=dbeedf47c50b1be67b2ef31a901b8b56&chksm=ce98f646f9ef7f506a1f7d72fc9384ba1b518072b44d157f657a8d5495a1c78c3e5de0b41efd&token=1652861108&lang=zh_CN#rd \ No newline at end of file diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC \345\256\271\345\231\250\345\237\272\346\234\254\345\256\236\347\216\260.md" b/docs/source/spring/2-ioc-overview.md similarity index 99% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC \345\256\271\345\231\250\345\237\272\346\234\254\345\256\236\347\216\260.md" rename to docs/source/spring/2-ioc-overview.md index e8a89a8..08811f1 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC \345\256\271\345\231\250\345\237\272\346\234\254\345\256\236\347\216\260.md" +++ b/docs/source/spring/2-ioc-overview.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + ## 概述 上一篇我们了解了Spring的整体架构,这篇我们开始真正的阅读Spring源码。在分析spring的源码之前,我们先来简单回顾下spring核心功能的简单使用。 diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\212\357\274\211.md" b/docs/source/spring/3-ioc-tag-parse-1.md similarity index 98% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\212\357\274\211.md" rename to docs/source/spring/3-ioc-tag-parse-1.md index 45712c4..3edbb35 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\212\357\274\211.md" +++ b/docs/source/spring/3-ioc-tag-parse-1.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + ## 概述 本文主要研究Spring标签的解析,Spring的标签中有默认标签和自定义标签,两者的解析有着很大的不同,这次重点说默认标签的解析过程。 @@ -197,7 +212,7 @@ public AbstractBeanDefinition parseBeanDefinitionElement( ### 创建用于承载属性的BeanDefinition -BeanDefinition是一个接口,在spring中此接口有三种实现:RootBeanDefinition、ChildBeanDefinition已经GenericBeanDefinition。而三种实现都继承了AbstractBeanDefinition,其中BeanDefinition是配置文件元素标签在容器中的内部表示形式。元素标签拥有class、scope、lazy-init等属性,BeanDefinition则提供了相应的beanClass、scope、lazyInit属性,BeanDefinition和中的属性一一对应。其中RootBeanDefinition是最常用的实现类,他对应一般性的元素标签,GenericBeanDefinition是自2.5版本以后新加入的bean文件配置属性定义类,是一站式服务的。 +BeanDefinition是一个接口,在spring中此接口有三种实现:RootBeanDefinition、ChildBeanDefinition已经GenericBeanDefinition。而三种实现都继承了AbstractBeanDefinition,其中BeanDefinition是配置文件元素标签在容器中的内部表示形式。元素标签拥有class、scope、lazy-init等属性,BeanDefinition则提供了相应的beanClass、scope、lazyInit属性,BeanDefinition和``中的属性一一对应。其中RootBeanDefinition是最常用的实现类,他对应一般性的元素标签,GenericBeanDefinition是自2.5版本以后新加入的bean文件配置属性定义类,是一站式服务的。 在配置文件中可以定义父和字,父用RootBeanDefinition表示,而子用ChildBeanDefinition表示,而没有父的就使用RootBeanDefinition表示。AbstractBeanDefinition对两者共同的类信息进行抽象。 diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\213\357\274\211.md" b/docs/source/spring/4-ioc-tag-parse-2.md similarity index 98% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\213\357\274\211.md" rename to docs/source/spring/4-ioc-tag-parse-2.md index b92d7e7..4d5dcff 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224 IOC\351\273\230\350\256\244\346\240\207\347\255\276\350\247\243\346\236\220\357\274\210\344\270\213\357\274\211.md" +++ b/docs/source/spring/4-ioc-tag-parse-2.md @@ -1,3 +1,19 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + + **正文** 在上一篇我们已经完成了从xml配置文件到BeanDefinition的转换,转换后的实例是GenericBeanDefinition的实例。本文主要来看看标签解析剩余部分及BeanDefinition的注册。 diff --git "a/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276\350\247\243\346\236\220.md" b/docs/source/spring/5-ioc-tag-custom.md.md similarity index 98% rename from "docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276\350\247\243\346\236\220.md" rename to docs/source/spring/5-ioc-tag-custom.md.md index 1f7c14d..a902899 100644 --- "a/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\350\207\252\345\256\232\344\271\211\346\240\207\347\255\276\350\247\243\346\236\220.md" +++ b/docs/source/spring/5-ioc-tag-custom.md.md @@ -1,3 +1,19 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + + ## 概述 之前我们已经介绍了spring中默认标签的解析,解析来我们将分析自定义标签的解析,我们先回顾下自定义标签解析所使用的方法,如下图所示: diff --git "a/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC-\345\274\200\345\220\257 bean \347\232\204\345\212\240\350\275\275.md" b/docs/source/spring/6-bean-load.md similarity index 97% rename from "docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC-\345\274\200\345\220\257 bean \347\232\204\345\212\240\350\275\275.md" rename to docs/source/spring/6-bean-load.md index ecb36af..be0683e 100644 --- "a/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC-\345\274\200\345\220\257 bean \347\232\204\345\212\240\350\275\275.md" +++ b/docs/source/spring/6-bean-load.md @@ -1,3 +1,18 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Bean加载,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + ## 概述 前面我们已经分析了spring对于xml配置文件的解析,将分析的信息组装成 BeanDefinition,并将其保存注册到相应的 BeanDefinitionRegistry 中。至此,Spring IOC 的初始化工作完成。接下来我们将对bean的加载进行探索。 @@ -92,9 +107,9 @@ public interface BeanFactory { ## FactoryBean -一般情况下,Spring通过反射机制利`用`的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在``中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。它们隐藏了实例化一些复杂Bean的细节,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持泛型,即接口声明改为`FactoryBean`的形式。 +一般情况下,Spring通过反射机制利用``的class属性指定实现类实例化Bean,在某些情况下,实例化Bean过程比较复杂,如果按照传统的方式,则需要在``中提供大量的配置信息。配置方式的灵活性是受限的,这时采用编码的方式可能会得到一个简单的方案。Spring为此提供了一个org.springframework.bean.factory.FactoryBean的工厂类接口,用户可以通过实现该接口定制实例化Bean的逻辑。FactoryBean接口对于Spring框架来说占用重要的地位,Spring自身就提供了70多个FactoryBean的实现。它们隐藏了实例化一些复杂Bean的细节,给上层应用带来了便利。从Spring3.0开始,FactoryBean开始支持泛型,即接口声明改为`FactoryBean`的形式。 -以Bean结尾,表示它是一个Bean,不同于普通Bean的是:**它是实现了FactoryBean接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取**。 +以Bean结尾,表示它是一个Bean,不同于普通Bean的是:**它是实现了`FactoryBean`接口的Bean,根据该Bean的ID从BeanFactory中获取的实际上是FactoryBean的getObject()返回的对象,而不是FactoryBean本身,如果要获取FactoryBean对象,请在id前面加一个&符号来获取**。 ```java package org.springframework.beans.factory; @@ -109,7 +124,7 @@ public interface FactoryBean { - **T getObject()**:返回由FactoryBean创建的Bean实例,如果isSingleton()返回true,则该实例会放到Spring容器中单实例缓存池中; - **boolean isSingleton()**:返回由FactoryBean创建的Bean实例的作用域是singleton还是prototype; -- **Class getObjectType()**:返回FactoryBean创建的Bean类型。 +- `Class getObjectType()`:返回FactoryBean创建的Bean类型。 当配置文件中``的class属性配置的实现类是FactoryBean时,通过getBean()方法返回的不是FactoryBean本身,而是FactoryBean#getObject()方法所返回的对象,相当于FactoryBean#getObject()代理了getBean()方法。 例:如果使用传统方式配置下面Car的``时,Car的每个属性分别对应一个``元素标签。 diff --git "a/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean\345\210\233\345\273\272.md" b/docs/source/spring/7-bean-build.md similarity index 98% rename from "docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean\345\210\233\345\273\272.md" rename to docs/source/spring/7-bean-build.md index 259e050..6e37369 100644 --- "a/docs/source/spring/spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213bean\345\210\233\345\273\272.md" +++ b/docs/source/spring/7-bean-build.md @@ -1,3 +1,19 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,Spring设计模式,Bean创建,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + + **正文** 在 Spring 中存在着不同的 scope,默认是 singleton ,还有 prototype、request 等等其他的 scope,他们的初始化步骤是怎样的呢?这个答案在这篇博客中给出。 diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\345\261\236\346\200\247\345\241\253\345\205\205.md" b/docs/source/spring/8-ioc-attribute-fill.md similarity index 95% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\345\261\236\346\200\247\345\241\253\345\205\205.md" rename to docs/source/spring/8-ioc-attribute-fill.md index dc7b99b..3c7ac08 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\345\261\236\346\200\247\345\241\253\345\205\205.md" +++ b/docs/source/spring/8-ioc-attribute-fill.md @@ -1,7 +1,24 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,属性填充,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- + **正文** `doCreateBean()` 主要用于完成 bean 的创建和初始化工作,我们可以将其分为四个过程: +> [最全面的Java面试网站](https://topjavaer.cn) + - `createBeanInstance()` 实例化 bean - `populateBean()` 属性填充 - 循环依赖的处理 @@ -108,6 +125,16 @@ protected void populateBean(String beanName, RootBeanDefinition mbd, BeanWrapper 上面步骤中有几个地方是我们比较感兴趣的,它们分别是依赖注入(autowireByName/autowireByType)以及属性填充,接下来进一步分析这几个功能的实现细节 +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + ## 自动注入 diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\345\276\252\347\216\257\344\276\235\350\265\226\345\244\204\347\220\206.md" b/docs/source/spring/9-ioc-circular-dependency.md similarity index 90% rename from "docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\345\276\252\347\216\257\344\276\235\350\265\226\345\244\204\347\220\206.md" rename to docs/source/spring/9-ioc-circular-dependency.md index 328c93d..0d5e755 100644 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224IOC\344\271\213\345\276\252\347\216\257\344\276\235\350\265\226\345\244\204\347\220\206.md" +++ b/docs/source/spring/9-ioc-circular-dependency.md @@ -1,3 +1,17 @@ +--- +sidebar: heading +title: Spring源码分析 +category: 源码分析 +tag: + - Spring +head: + - - meta + - name: keywords + content: Spring源码,标签解析,源码分析,循环依赖,Spring设计模式,Spring AOP,Spring IOC,Bean,Bean生命周期 + - - meta + - name: description + content: 高质量的Spring源码分析总结 +--- ## **什么是循环依赖** 循环依赖其实就是循环引用,也就是两个或则两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于C,C又依赖于A。如下图所示: @@ -6,6 +20,8 @@ 注意,这里不是函数的循环调用,是对象的相互依赖关系。循环调用其实就是一个死循环,除非有终结条件。 +> [最全面的Java面试网站](https://topjavaer.cn) + Spring中循环依赖场景有: (1)构造器的循环依赖 @@ -56,6 +72,16 @@ private final Map> singletonFactories = new HashMap<>(1 private final Map earlySingletonObjects = new HashMap<>(16); ``` +> 分享一份大彬精心整理的**大厂面试手册**,包含**计算机基础、Java基础、多线程、JVM、数据库、Redis、Spring、Mybatis、SpringMVC、SpringBoot、分布式、微服务、设计模式、架构、校招社招分享**等高频面试题,非常实用,有小伙伴靠着这份手册拿过字节offer~ +> +> ![](http://img.topjavaer.cn/image/image-20211127150136157.png) +> +> ![](http://img.topjavaer.cn/image/image-20220316234337881.png) +> +> 需要的小伙伴可以自行**下载**: +> +> http://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247485445&idx=1&sn=1c6e224b9bb3da457f5ee03894493dbc&chksm=ce98f543f9ef7c55325e3bf336607a370935a6c78dbb68cf86e59f5d68f4c51d175365a189f8#rd + 这三级缓存分别指: (1)singletonFactories : 单例对象工厂的cache diff --git "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" "b/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" deleted file mode 100644 index ce9103c..0000000 --- "a/docs/source/spring/Spring\346\272\220\347\240\201\350\247\243\346\236\220\342\200\224\342\200\224\346\225\264\344\275\223\346\236\266\346\236\204.md" +++ /dev/null @@ -1,73 +0,0 @@ -## 概述 - -Spring是一个开放源代码的设计层面框架,他解决的是业务逻辑层和其他各层的松耦合问题,因此它将面向接口的编程思想贯穿整个系统应用。Spring是于2003 年兴起的一个轻量级的Java 开发框架,由Rod Johnson创建。简单来说,Spring是一个分层的JavaSE/EE full-stack(一站式) 轻量级开源框架。 - -## spring的整体架构 - -Spring框架是一个分层架构,它包含一系列的功能要素,并被分为大约20个模块,如下图所示: - -![](http://img.topjavaer.cn/img/202309161608576.png) - -从上图spring framework整体架构图可以看到,这些模块被总结为以下几个部分: - -### 1. Core Container - -Core Container(核心容器)包含有Core、Beans、Context和Expression Language模块 - -Core和Beans模块是框架的基础部分,提供IoC(转控制)和依赖注入特性。这里的基础概念是BeanFactory,它提供对Factory模式的经典实现来消除对程序性单例模式的需要,并真正地允许你从程序逻辑中分离出依赖关系和配置。 - -Core模块主要包含Spring框架基本的核心工具类 - -Beans模块是所有应用都要用到的,它包含访问配置文件、创建和管理bean以及进行Inversion of Control/Dependency Injection(Ioc/DI)操作相关的所有类 - -Context模块构建于Core和Beans模块基础之上,提供了一种类似于JNDI注册器的框架式的对象访问方法。Context模块继承了Beans的特性,为Spring核心提供了大量扩展,添加了对国际化(如资源绑定)、事件传播、资源加载和对Context的透明创建的支持。 - -ApplicationContext接口是Context模块的关键 - -Expression Language模块提供了一个强大的表达式语言用于在运行时查询和操纵对象,该语言支持设置/获取属性的值,属性的分配,方法的调用,访问数组上下文、容器和索引器、逻辑和算术运算符、命名变量以及从Spring的IoC容器中根据名称检索对象 - - - -### 2. Data Access/Integration - -JDBC模块提供了一个JDBC抽象层,它可以消除冗长的JDBC编码和解析数据库厂商特有的错误代码,这个模块包含了Spring对JDBC数据访问进行封装的所有类 - -ORM模块为流行的对象-关系映射API,如JPA、JDO、Hibernate、iBatis等,提供了一个交互层,利用ORM封装包,可以混合使用所有Spring提供的特性进行O/R映射,如前边提到的简单声明性事务管理 - -OXM模块提供了一个Object/XML映射实现的抽象层,Object/XML映射实现抽象层包括JAXB,Castor,XMLBeans,JiBX和XStream -JMS(java Message Service)模块主要包含了一些制造和消费消息的特性 - -Transaction模块支持编程和声明式事物管理,这些事务类必须实现特定的接口,并且对所有POJO都适用 - - - -### 3. Web - -Web上下文模块建立在应用程序上下文模块之上,为基于Web的应用程序提供了上下文,所以Spring框架支持与Jakarta Struts的集成。 - -Web模块还简化了处理多部分请求以及将请求参数绑定到域对象的工作。Web层包含了Web、Web-Servlet、Web-Struts和Web、Porlet模块 - -Web模块:提供了基础的面向Web的集成特性,例如,多文件上传、使用Servlet listeners初始化IoC容器以及一个面向Web的应用上下文,它还包含了Spring远程支持中Web的相关部分 - -Web-Servlet模块web.servlet.jar:该模块包含Spring的model-view-controller(MVC)实现,Spring的MVC框架使得模型范围内的代码和web forms之间能够清楚地分离开来,并与Spring框架的其他特性基础在一起 - -Web-Struts模块:该模块提供了对Struts的支持,使得类在Spring应用中能够与一个典型的Struts Web层集成在一起 - -Web-Porlet模块:提供了用于Portlet环境和Web-Servlet模块的MVC的实现 - - - -### 4. AOP - -AOP模块提供了一个符合AOP联盟标准的面向切面编程的实现,它让你可以定义例如方法拦截器和切点,从而将逻辑代码分开,降低它们之间的耦合性,利用source-level的元数据功能,还可以将各种行为信息合并到你的代码中 - -Spring AOP模块为基于Spring的应用程序中的对象提供了事务管理服务,通过使用Spring AOP,不用依赖EJB组件,就可以将声明性事务管理集成到应用程序中 - - - -### 5. Test - -Test模块支持使用Junit和TestNG对Spring组件进行测试 - - - diff --git a/docs/system-design/README.md b/docs/system-design/README.md index aef9e83..4ea61d7 100644 --- a/docs/system-design/README.md +++ b/docs/system-design/README.md @@ -24,4 +24,4 @@ [知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: -![](http://img.topjavaer.cn/img/星球优惠券.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202312280805419.png) \ No newline at end of file diff --git a/docs/zsxq/article/site-hack.md b/docs/zsxq/article/site-hack.md new file mode 100644 index 0000000..da92374 --- /dev/null +++ b/docs/zsxq/article/site-hack.md @@ -0,0 +1,84 @@ +你好,我是大彬。 + +昨天收到腾讯云的短信,一开始还以为是营销短信,仔细一看,好家伙,存在**恶意文件**等**安全风险**,看来是被入侵了。。 + +![](http://img.topjavaer.cn/img/202312290909237.png) + +吓得我赶紧上小破站看看,果然,502了。。 + +![](http://img.topjavaer.cn/img/202312290849848.png) + +登录腾讯云控制台,查看监控数据,发现服务器CPU资源持续**100**%。。 + +![](http://img.topjavaer.cn/img/202312290849903.png) + +作为优质八股文选手,这个难不倒我,上来就是**top**命令伺候。 + +![](http://img.topjavaer.cn/img/202312290751593.png) + +可以看到,一个叫 kswapd0 的进程跑的死死的。 + +那么这个 kswapd0 是啥?pid是 27343,通过命令 `ll /proc/27343`,可以看这个进程执行了 一个 `/root/.configrc5/a/kswapd0`的脚本 ,应该就是病毒程序。 + +![](http://img.topjavaer.cn/img/202312290819646.png) + +紧接着查看下这个进程是否有对外连接端口,`netstat -anltp|grep kswapd0`,显示IP是 `179.43.139.84`,是一个瑞士的IP地址。。 + +![](http://img.topjavaer.cn/img/202312290803351.png) + +分析`/var/log/secure` 日志,查找详细的入侵痕迹 `grep 'Accepted' /var/log/secure` + +![](http://img.topjavaer.cn/img/202312291022633.png) + +可以看到,有一个来自加拿大的IP地址 34.215.138.2 成功登录了,通过 publickey 密钥登录(可能是服务器上的恶意程序向.authorized_keys 写入了公钥),看来是被入侵无疑了。 + +然后习惯性看了下定时任务,`crontab -e`,好家伙,定时任务里面也有惊喜,入侵者还留下了几个定时任务。。 + +![](http://img.topjavaer.cn/img/202312290823467.png) + +## 处理措施 + +1、在腾讯云**安全组**限制了 SSH 的登录IP, 之前的安全组 SSH 是放行所有IP。 + +2、将 SSH ROOT 密码修改。 + +![](http://img.topjavaer.cn/img/202312290917431.png) + +3、将`/root/.ssh/authorized_keys`备份,然后删除。 + +4、直接杀掉27343进程,`kill -9 27343` + +5、**清理定时任务**: `crontab -e`(这个命令会清理所有的定时任务,慎用) + +6、删除/root/ 目录下的.configrc5文件夹:`rm -rf /root/.configrc5/` + + + +一番操作之后,CPU就降下来了,重启服务后小破站便恢复正常访问了。 + +![](http://img.topjavaer.cn/img/202312290840463.png) + + + +## 本次服务器被入侵的一些启示 + +1、用好云厂家的**安全组**。对一些关键端口,放行规则尽量最小 + +2、**封闭不使用的端口**,做到用一个开一个(通过防火墙和安全组策略) + +3、服务器相关的一些**密码**尽量增加**复杂性** + +4、检查开机启动 和 crontab 相关的内容 + +5、检查**异常**进程 + + + +以上就是这次服务器被入侵的排查处理过程和一些启示,希望大家以后没有机会用到~ + + + + + + + diff --git "a/docs/zsxq/article/\346\257\217\345\271\264\345\210\260\345\271\264\345\272\225\346\200\273\347\273\223\345\267\245\344\275\234\350\277\260\350\201\214\347\232\204\346\227\266\345\200\231\346\204\237\350\247\211\350\207\252\345\267\261\345\245\275\345\203\217\345\225\245\351\203\275\346\262\241\345\271\262.md" "b/docs/zsxq/article/\346\257\217\345\271\264\345\210\260\345\271\264\345\272\225\346\200\273\347\273\223\345\267\245\344\275\234\350\277\260\350\201\214\347\232\204\346\227\266\345\200\231\346\204\237\350\247\211\350\207\252\345\267\261\345\245\275\345\203\217\345\225\245\351\203\275\346\262\241\345\271\262.md" new file mode 100644 index 0000000..27752d0 --- /dev/null +++ "b/docs/zsxq/article/\346\257\217\345\271\264\345\210\260\345\271\264\345\272\225\346\200\273\347\273\223\345\267\245\344\275\234\350\277\260\350\201\214\347\232\204\346\227\266\345\200\231\346\204\237\350\247\211\350\207\252\345\267\261\345\245\275\345\203\217\345\225\245\351\203\275\346\262\241\345\271\262.md" @@ -0,0 +1,95 @@ +# 每年到年底总结工作述职的时候感觉自己好像啥都没干 + +# 背景 + +年底了,上周老板说要求大家写个述职报告(年底工作总结),然后大家开个会讲一下自己过去这一年时间都做了什么,并且互相之间都看看述职报告,对彼此的工作做个评价 + +周末我大概花了整整一天时间在那憋述职报告,算上标点符号总共就憋出来几十个字儿-_-|| + +述职报告实在是太难写了,明明这一年辛辛苦苦肝了很多事情,工作量也不比别人少,但是写的时候总感觉好像也没干多少重要的事情,写出来总觉得不是那么回事儿 + +和同事朋友交流了一下,发现 ta 们也跟我一样,全都在那现场直憋呢 + +# 为什么述职报告写的这么难受 + +那么问题来了,为什么辛辛苦苦干了一年,过程中都感觉自己挺充实的,但写述职报告的时候就这么难受,感觉写不出来多少东西呢? + +我思考了一下,总结原因大概是以下这几个: + +## 不系统 + +首先是做的事情不系统,用大白话说就是这一年干事情东一榔头西一棒的,很难把它们结合在一起来说,用互联网黑话来说就是很难形成组合拳 + +我以程序员的工作内容来举例子,比如说你的同事今年干的事情都是聚焦在性能优化这块,那么 ta 在写述职报告的时候就很好写了 + +首先是性能优化基于什么背景,性能优化做了什么调研,拆分了哪些阶段去推进的,在这个过程中做了什么技术相关的事情,做完之后对整个项目产生了哪些正面的影响 + +这是一个完整的做事流程,写出来就言之有物 + +相反的,如果你做的事情比较琐碎,做的都是到处补锅或者打杂的事情的话,那么你的述职报告就真的很不好写,因为不完整,不是从头到尾的流程,那你写出来的东西就很散,做的事情无法聚焦起来来体现出更大的价值 + +打个比方就是,你工作了一年是积累了一堆芝麻,而有的人是一直在培育一个西瓜,差不多就是这个意思 + +## 平时没记录 + +还有一个重要原因就是平时没有做好记录 + +首先我是非常反对有的公司要求员工写日报来汇报工作的,我认为这是一种非常低级的管理方式,体现了公司管理者的无能 + +不过我认为我们确实应该定期对自己的工作做一个记录,不需要非常频繁,只要在某个阶段记录好当前阶段做的几个大事情就行了 + +比如说在一个重要的项目上线之后,我们应该自己给自己写一个记录总结,写清楚这个事情的背景、阶段、重点事项、收获以及不足等等 + +注意这个记录总结是给我们自己看的,不是给老板看的,我们除了给老板打工谋生以外,我们也是自己生活的主人,所以很有必要为自己写总结 + +这些阶段性的记录总结,就为年底的述职准备好了素材,到时候需要述职的时候只需要把这些素材汇总起来润色一下即可 + +## 没有主动和 leader 沟通 + +最后一个额外的原因是我觉得平时没有和 leader 做好沟通,据我的观察,很多人在职场里面主动找 leader 沟通的时间太少了,其实这样子不太好 + +一个团队里面,leader 很难对任何事情的细枝末节都了解清楚,ta 一般只会抓重点的一两件事情 + +所以如果你某段时间感觉做事特别不得劲,或是做的事情你感觉特别散不够系统,想做点大块儿的事情,那你完全可以主动和 leader 沟通,表达你的诉求 + +退一万步讲,如果你述职报告写的很难受,这个事情其实也可以找 leader 沟通,也许 leader 会给你点启发,再不济也许可以安排你明年的工作,让你明年写述职报告的时候可以轻松一些,有东西可写 + +# 如何写述职报告 + +刚刚聊的是一些宏观的问题,具体说的是感觉述职报告没什么东西可写的原因,接下来我说一下微观的东西,也就是如何来写述职报告 + +## 做好分类 + +首先是做好分类,我还是以程序员的工作来举例,比如说你这一年大大小小做过无数事情,加过无数班,它们分别归属于什么类目呢?什么事情是业务类的,什么事情是技术优化类的,什么事情是业务方产品方发起的,什么事情是技术团队自己发起的,什么事情是外部合作的,等等 + +首先你得分好了类,你的报告才能结构清晰,就像一个人首先得筋骨强健,才能站的稳,这是身体的基础 + +做好了分类,你做的事情才能一点一点的往每个类目里面加,你也不会越写越乱 + +## 善用 star 模型 + +做好了分类,接下来具体就是每个事情该怎么写了,一般我们是使用 star 模型来写 + +star 模型是个被说烂了的写作套路,但是确实好用 + +啥是 star 模型,我再啰嗦几句,star 就是四个英文单词的缩写: + +1. situation:场景 +2. task:任务 +3. action:行动 +4. result:结果 + +看起来挺难的,其实大白话来讲它很简单 + +比如说你在述职报告上这样写:我今年做了 xx 优化 + +这句话就是很多人会常犯的错误,让人感觉话说了一半 + +用 star 模型来优化一下就是: + +1. situation:我是基于什么样的背景来做 xx 优化的,比如说我在接手项目时,发现了 xx 问题,受限于 xx 条件,我打算做 xx 优化,这部分主要是讲清楚你做事情的原因和出发点,不要让别人觉得很突兀 +2. task:我把 xx 优化拆分为几个任务,这部分是你要做的事情具体有哪些任务,比如说调研优化方案、PC 端优化、移动端优化等等 +3. action:我把每个任务里做了什么,这部分就该说细节了,不管是什么方案、技术工具、优化细节等等,都可以在这里面说 +4. result:最终达到了什么样的效果,这部分是总结做的事情达到的效果,主要是对结果做一个说明,这里需要强调的是有点事情可以定量来说,比如说某某指标提升了 xx%,但是有的事情不太好定量,那就只能定性,比如说用户反馈评价等等,之前我见过某些业务方给技术团队送锦旗的 + +这篇笔记要写的就这么多,最后祝大家年底都能拿个好绩效,开开心心拿满年终奖过年😸 \ No newline at end of file diff --git a/docs/zsxq/inner-material.md b/docs/zsxq/inner-material.md index dad99b2..536c880 100644 --- a/docs/zsxq/inner-material.md +++ b/docs/zsxq/inner-material.md @@ -22,4 +22,4 @@ [知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: -![](http://img.topjavaer.cn/img/星球优惠券.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202312280805419.png) \ No newline at end of file diff --git a/docs/zsxq/introduce.md b/docs/zsxq/introduce.md index edc181d..7e2d967 100644 --- a/docs/zsxq/introduce.md +++ b/docs/zsxq/introduce.md @@ -26,18 +26,38 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, ![](http://img.topjavaer.cn/img/202305180010872.png) -目前大彬的学习圈已经积累了很多优质内容了,像**Java面试手册完整版、高频场景设计题目、LeetCode刷题笔记**等。 +目前大彬的学习圈已经积累了很多优质内容了,像**Java面试手册完整版、面试真题手册(300多家公司面试真题)、高频场景设计题目、LeetCode刷题笔记**等。 ![](http://img.topjavaer.cn/img/202305180013526.png) ![](http://img.topjavaer.cn/img/image-20230102151744058.png) +![](http://img.topjavaer.cn/img/202404091744332.png) + 此外学习圈还积累了很多的**优质学习资源**,包括**计算机基础、Java项目、进阶知识、实战经验总结、优质书籍、笔试面试资源**等等,可以说非常全面了。 ![](http://img.topjavaer.cn/img/20230116133239.png) 在这里可以找到大部分你想要的**学习资源**,而且这里有一群和你一样志同道合的小伙伴,可以一起提升编程能力、交流学习心得、分享经验、拿更好的Offer! +## 球友感言 + +学习圈目前已经运营一年多了,也收到了很多球友的**好评**。 + +![](http://img.topjavaer.cn/img/202310200757694.png) + +![](http://img.topjavaer.cn/img/202310200757131.png) + +也有挺多星球的小伙伴拿到了**大厂offer**! + +![](http://img.topjavaer.cn/img/202404091745315.png) + +![](http://img.topjavaer.cn/img/202310200756703.png) + +![](http://img.topjavaer.cn/img/202310200756575.png) + +![](http://img.topjavaer.cn/img/202404091745397.png) + ## 谁适合加入的学习圈 1. **非科班转码**或者计算机小白,没有一个完善的学习路线规划; @@ -53,13 +73,13 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, 如果你加入了,希望你也能跟像球友们一样**每天坚持打卡学习,为未来奋斗**~ -![](http://img.topjavaer.cn/img/202304212238396.png) +![](http://img.topjavaer.cn/img/202312280800515.png) ## 学习圈能提供什么? 学习圈能给你带来的帮助有: -### 1、最新的面试手册(星球专属) +### 1、最新的面试手册(星球专属)+面试真题手册 **精心整理的面试手册最新版**。目前已经更新迭代了**19**个版本,持续在更新中,**面试手册完整版**是星球球友专享,**不会对外**提供下载。 @@ -67,6 +87,10 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, ![](http://img.topjavaer.cn/img/202305180033154.png) +![](http://img.topjavaer.cn/img/202404091745512.png) + +![](http://img.topjavaer.cn/img/202404091745321.png) + 面试手册最新版本增加补充了**微服务、分布式、系统设计、场景题目**等高频面试题,同样也是星球球友**专享**的。 这份面试手册已经**帮助好多位读者拿到offer**了,其中也有拿了字节、平安等大厂offer的。 @@ -181,10 +205,10 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, ## 怎么进入星球? -如果你下定决心要加入的话,可以直接扫下面这个二维码。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元(**拒绝割韭菜**)的价格就可以加入,服务期一年,**每天只要三毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务**远超**门票价格了。 +如果你下定决心要加入的话,可以直接扫下面这个二维码。星球定价**218**元,减去**50**元的优惠券,等于说只需要**168**元(**拒绝割韭菜**)的价格就可以加入,服务期一年,**每天只要三毛钱**(0.46元),相比培训班几万块的学费,非常值了,星球提供的服务**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**218**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款**~ -![](http://img.topjavaer.cn/img/202304212238396.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202312280801529.png) \ No newline at end of file diff --git a/docs/zsxq/mianshishouce.md b/docs/zsxq/mianshishouce.md index 54a4fd5..287488c 100644 --- a/docs/zsxq/mianshishouce.md +++ b/docs/zsxq/mianshishouce.md @@ -82,4 +82,4 @@ PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款**~ -![](http://img.topjavaer.cn/img/202304212238396.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202304212233017.png) \ No newline at end of file diff --git "a/docs/zsxq/question/offer\351\200\211\346\213\251\357\274\232\345\260\217\347\272\242\344\271\246vs\351\230\277\351\207\214.md" "b/docs/zsxq/question/offer\351\200\211\346\213\251\357\274\232\345\260\217\347\272\242\344\271\246vs\351\230\277\351\207\214.md" new file mode 100644 index 0000000..debf2c5 --- /dev/null +++ "b/docs/zsxq/question/offer\351\200\211\346\213\251\357\274\232\345\260\217\347\272\242\344\271\246vs\351\230\277\351\207\214.md" @@ -0,0 +1,63 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495573&idx=1&sn=353036594c08354ce96631e34fc97082&chksm=ce9b12d3f9ec9bc531d8b2e590e2b8e61f73daf32a9e2e8fab89f1bc417b6309e14edbb05d85#rd)小伙伴关于【**offer选择**】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [放弃大厂去外包...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495584&idx=1&sn=55ea6cba584d0e2d0a585ccdb0f437a2&chksm=ce9b12e6f9ec9bf02105572f7a4d432edf8c5c635fc876001d6590cd2b67e321932dbc0d9a20&token=1557067692&lang=zh_CN#rd) +> +> [秋招还没有offer,打算去上培训班了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495475&idx=1&sn=c9604e8310e674ceed78228373ab21b6&chksm=ce9b1275f9ec9b633e27aaf0c83abba8a7d8c25c8a84b2bf031238d3d41aa1b8cc5af7d0e3fa#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,您好,有个问题咨询您,offer选择,都是 Java 开发岗。 + +**小红书**,**直播技术**,北京,试用期后定级, base+大小周双倍加班费+1.5km 房补,年包差不多50w。作息 10:30~21:30 ,大小周。argue 了一下 base 、签字费和期权,可能会有一点点上涨空间。 + +**淘天**,**交易技术**,杭州,p4, 薪资组成( base+车补和两年的无距离限制房补+两年杭州市的补贴+签字费),算补贴不算签字费年包 40w+。作息为 10105 。 + +业务方面,我和两边的leader都聊过,淘天那边是架构很成熟了,最近组里是在用 DDD 领域驱动设计**重构旧代码**,负责预售、价保、赠品等产品,技术这块我是放心的。 + +小红书那边是处于业务的**增长期**,虽然 ld 知道成熟的架构是什么样的,表示“没有壁垒”,但是现有的架构还需要迭代;组里的 20+开发大多来自抖音快手,校招社招会再招人。有三个方向可选,tob 的商家-主播推荐平台、toc 的直播间互动和直播架构。 + +**跳槽**方面的担忧 本人背景是**双一流本**,在小红书可能会有更多崭露头角的机会,但是去淘天能有大厂光环。考虑到晋升速度,若干年后如果需要从淘天跳槽,与从小红书跳槽相比,哪边会更吃香一些呢? + +**技术**方面的担忧 我算是自驱力比较强的,在开源社区也很活跃。我担心小红书的技术不如淘天,直播的流量目前也不大,学到的技术会比淘天少很多? + +关于**提前实习** 如果要选择小红书,那么有没有必要在毕业前去淘天实习镀金?怎么对 HR 开口比较好呢? 另外 + +--- + +**大彬的回答**: + +师弟你好,恭喜恭喜,能拿到这两个offer很优秀啊! + +我认为新人,**一开始在成长期的中厂比单纯的大厂更好**,主要是几个方面的原因: + +**职业发展**来说,大厂基本上工作内容、技术架构和晋升通道已经**固化**了,大部分情况都是按部就班堆工作量;中厂还有很多“不完善”的地方可以优化并且做出成绩,有快速脱颖而出的机会。 + +**技术成长**来说,对于新人,用一个已有的技术可以快速学到一些东西,但也只能让你从初级工成长为熟练工;只有自己设计或者优化了某个技术,才能晋升到职业成长的下个阶段,比如技术负责人或架构师。 + +大厂的流程体系更完善,但是有一定体量的中厂也不会差到哪里去,这部分不会有质的区别。 + +总的来说,大厂和中厂的头衔其实不是特别重要,新人找工作其实更多应该看重成长速度,而技术人员的成长绝大部分都依赖于解决问题。在目前的环境下,大厂的增长相对停滞,工作中面对的更多是细碎的**存量优化**问题,或者无效内卷;少数还有增长空间的中厂无疑对于技术成长的价值更大一些。 + +除了公司,还有业务方向的问题也需要考虑一下,“直播技术”的范围很大,从编解码器到流媒体工程到前后端工程都有可能,如果没有特殊教育背景或者偏好,我建议新人选一个更通用的业务方向,尽量去**核心业务**的核心流程,见到的东西会更多一些。 + +第三,**城市**也需要考虑,虽然年轻人换个城市的代价不大,但是一旦有了对象或者想要常住某个城市之后,那么之前不是问题的问题就都是问题。比如购房成本北京会高一些等 + +最后,工资这东西当然也需要考虑,但也**不是追求越高越好**。一般来说,现在这个行业内工资水平都是相对稳定的,如果某一家工资特别高,那么要么是赶在业务扩张前急招(后面有可能会裁)、要么是工作强度明显更大;当然,相对的,在裁员的时候,也更偏向于裁工资更高的人。 + + diff --git "a/docs/zsxq/question/\344\272\214\346\234\254\345\255\246\345\216\206\357\274\214\346\203\263\345\207\272\345\233\275\350\257\273\347\240\224.md" "b/docs/zsxq/question/\344\272\214\346\234\254\345\255\246\345\216\206\357\274\214\346\203\263\345\207\272\345\233\275\350\257\273\347\240\224.md" new file mode 100644 index 0000000..e76be5f --- /dev/null +++ "b/docs/zsxq/question/\344\272\214\346\234\254\345\255\246\345\216\206\357\274\214\346\203\263\345\207\272\345\233\275\350\257\273\347\240\224.md" @@ -0,0 +1,40 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**出国读研**】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,有个问题咨询你 + +基本情况: 25岁,**二本非科班**程序员,在上海,最近想**出国读研** + +本科读的工商管理,还挂过科,辅修了计算机,现在工资税前 17k 吧,感觉到瓶颈期了,想出去见见世面,回来有个应届生的 buff ,**冲冲大厂**。 + +不知道这个决定是否可靠?有没有建议? + +--- + +**大彬的回答**: + +你好。按照目前互联网的行情,我**不建议**你这样做。 + +除非你出国是为了**留在国外**,国外文凭拿到国内**认可度**不见得比国内的高,或者你能够得上那种国内都认识的,不然 HR 最多参考一下 QS 排名,不然不认识的可能就被当成野鸡大学了。 + +还有一种为了拿**上海户口**,海归有优惠政策,可以去读个水硕回来就业落户 + +留学回来冲大厂最好就不要想了,**只有留学背景是不够的**,就现在应届生很多很多,如果你**本科学历和专业技能**都不够硬,现在再投校招岗位弄不好连简历筛选都过不去,连面试的机会都没有,而且出国读研是没法随时回来实习的,一些企业要求学生**实习**,如果你无法提前到岗位进行实习,可能直接被淘汰。对留学生而言在这方面没有任何优势。 + +唯一优势就是你留学后提升了你的**英语水平**,回来可以冲一些外企,英语好特别是口语好是有一定优势。 + +结论:如果出国留学是为了回国发展,你需要了解国内对于外国文凭的**认可度**问题,同时还要注重本科学历和专业技能的提高。出国留学的经历可以帮助你提升英语水平,学习不同的文化和思维方式,但并不一定就能为你在就业市场上带来直接的**竞争力**的。 + diff --git "a/docs/zsxq/question/\345\246\202\344\275\225\350\260\210\350\226\252.md" "b/docs/zsxq/question/\345\246\202\344\275\225\350\260\210\350\226\252.md" new file mode 100644 index 0000000..54129bb --- /dev/null +++ "b/docs/zsxq/question/\345\246\202\344\275\225\350\260\210\350\226\252.md" @@ -0,0 +1,42 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**如何谈薪**】的提问。 + +> 往期**星球提问整理**: +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥好,本人是今年参与校招的一名应届硕士生,目前已经拿到一些 offer 意向,到10月底可能就要陆陆续续谈薪了,请问彬哥在与 hr 沟通谈薪的时候有没有什么技巧或者要注意的呢?因为之前都注意的是面试方面,真正沟通谈薪的发现网上也很少有教,希望彬哥可以分享一下 + +--- + +**大彬的回答**: + +你好,建议从下面三点去考虑 + +1. 你可以到 OfferShow 上查一下你面试的这家公司对应岗位的薪资水平就知道你拿到的是高还是低了。一般校招都是企业单方面开价,除非你觉得你面试中水平远超其他参与该岗位竞争的人,或已经拿到其他公司比该公司开出的薪资更高的 offer ,你就可以跟 HR argue 薪资了。 +2. 还可以到牛客上去打探下消息今年校招薪资的情况,看看其他人拿到的 package 是多少,但在网上给别人透露你的薪资时注意保持隐藏身份,以免被介意公开薪资的 HR 抓到收回 offer +3. 想要在谈薪中掌握主动权,首先你要足够优秀,面试评价很高,部门很看好你,这样才有议价的余地。如果你的面试结果一般,和其他候选人相比没有什么优势,这样是基本不会给你 argue 的机会的,毕竟 HR 开完价后再重新调整,公司内部也要走很多流程,甚至大领导点头才可能给你调,手上备胎又多,根本就不关心你接不接这个 offer + + + +总之,argue 薪资的前提是你手上有多个备胎可以选择,如果 HR 不接受,就放弃该 offer 吧,选择你更满意的 + + + + + + + diff --git "a/docs/zsxq/question/\346\203\263\350\267\263\346\247\275\350\277\233\345\244\247\345\216\202\357\274\214\346\200\216\344\271\210\346\217\220\345\215\207\346\212\200\346\234\257.md" "b/docs/zsxq/question/\346\203\263\350\267\263\346\247\275\350\277\233\345\244\247\345\216\202\357\274\214\346\200\216\344\271\210\346\217\220\345\215\207\346\212\200\346\234\257.md" new file mode 100644 index 0000000..861f0a8 --- /dev/null +++ "b/docs/zsxq/question/\346\203\263\350\267\263\346\247\275\350\277\233\345\244\247\345\216\202\357\274\214\346\200\216\344\271\210\346\217\220\345\215\207\346\212\200\346\234\257.md" @@ -0,0 +1,38 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**如何提升技术能力**】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,我学历是**普通一本**,工作**一年多**,想跳槽进大厂做java后端,平时应该练什么才能**提升技术**?感谢! + +--- + +**大彬的回答**: + +你好,挺多同学都有这样的问题,分享一下我的建议: + +显然刚开始工作的新人是很难接触到一些比较大的锻炼机会的,比如写一些**底层框架**,或者**项目重构**等一些0-1的设计等等,这种一般是交给有一定经验的人去负责。 + +我觉得你这个阶段要做的是多**注重平时的积累**。主要是**实战和理论**两方面并行,比如**代码扩展、复用性、架构方面的理论**等。通过书籍或者课程学习相关的理论,然后在日常工作中,有意识地运用这些理论进行实际锻炼。 + +即使你一开始没机会设计一个大工程,那么就把一个小功能当成一个架构去做设计考量。代码的**设计模式、扩展、复用性**等,平时积累锻炼的机会还是有的,自己多去思考,平时写的业务代码,有没有优化空间之类的。 + +这样,比较理想的路线是 : 通过平时积累锻炼 -> leader 发现你相关能力不错 -> 尝试派给你更大更有挑战的任务 ->得到更好地锻炼。 + +当然如果你觉得自己可以承担一些比较复杂的任务了,也可以自己**主动**去争取一些锻炼机会。别会怕搞砸,开发的时候方案让组里的大佬帮你把把关,这个过程会是非常好的**成长**机会。 + + + diff --git "a/docs/zsxq/question/\346\224\276\345\274\203\345\244\247\345\216\202\345\216\273\345\244\226\345\214\205.md" "b/docs/zsxq/question/\346\224\276\345\274\203\345\244\247\345\216\202\345\216\273\345\244\226\345\214\205.md" new file mode 100644 index 0000000..60ab0d6 --- /dev/null +++ "b/docs/zsxq/question/\346\224\276\345\274\203\345\244\247\345\216\202\345\216\273\345\244\226\345\214\205.md" @@ -0,0 +1,70 @@ +大家好,我是大彬~ + +最近陆续有同学找我报喜了,有些同学拿了好几个offer,但是不知道该如何**抉****择。**这确实是个值得认真考虑的问题。 + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495573&idx=1&sn=353036594c08354ce96631e34fc97082&chksm=ce9b12d3f9ec9bc531d8b2e590e2b8e61f73daf32a9e2e8fab89f1bc417b6309e14edbb05d85#rd)小伙伴关于【**offer选择**】的提问。 + +> 往期**星球提问整理**: +> +> [秋招还没有offer,打算去上培训班了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495475&idx=1&sn=c9604e8310e674ceed78228373ab21b6&chksm=ce9b1275f9ec9b633e27aaf0c83abba8a7d8c25c8a84b2bf031238d3d41aa1b8cc5af7d0e3fa#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +大彬哥好,向您咨询**offer选择**的问题 + +最近找工作有两家 offer + +一家是**外派**给传统行业外企的岗位(公司1) + +a. 工作时间:早 9 晚 6 **不加班**,可居家办公 + +b. 薪资年包:五险一金按**最低**的交,到手比目前降了**10%** + + + +另一家是规模不错的**知名互联网公司**(公司2) + +a. 工作时间:比较**卷**,暂且按每天 11 个小时来算 + +b. 薪资年包:到手基本持平,**五险一金正常交** + +公司 1 有充足的属于自己的时间,工作上有一定的**主导权**,技术栈也比较**符合**自己 + +公司 2 会让自己的履历好看一些,能接触到技术方面更多的业务场景,有助于学习提高,但是技术栈**不太符合**自己,也比较**卷** + +两家公司如果算到手薪资时薪的话,外派的反而稍微高一点,想问问彬哥有什么建议没 + +(内心里是更**倾向**于去 1 ,第一想要主导权和自己的时间,其实就是花钱买时间,第二比较烦大厂的官僚风,讲鬼话,现在最主要是怕不稳定,但是公司 1 合同里边还分基础薪资和绩效薪资,基础薪资压的老低,感觉太不保险了,风险太大) + +--- + +**大彬的回答**: + +你好,不知道你是不是理解错了,外派外企就不是外企,是外企**外包**。五险一金按最低交基本就不用考虑了,真外企不会克扣这个。项目结束合作就结束了,到时候你就会派到下一个公司了。 + +所谓的早 9 晚 6 不加班,有可能只是hr一家之言,加不加班都是项目组说了算,不是外派公司能决定的。 + +虽然大厂这两年裁员很凶,不过再怎么说**稳定性**也好过外包的。这两者其实没有可比性。 + +如果从长远职业发展考虑,可见的将来还是一直干这行,家里没资本,**果断选 2** 就行了;后续可能改行,或者家庭条件比较好,哪里舒服选哪。 + +再有一点,比工资也不能光看到手,**公积金**完全可以算自己的钱,社保虽然是将来的事但多总比少要好的。 + +最后,建议你要么找个**真外企**,要么就选 2 ,降薪去外包不值得。 + + + + + diff --git "a/docs/zsxq/question/\346\230\237\347\220\203\346\212\200\346\234\257\351\227\256\351\242\230\346\261\207\346\200\273.md" "b/docs/zsxq/question/\346\230\237\347\220\203\346\212\200\346\234\257\351\227\256\351\242\230\346\261\207\346\200\273.md" new file mode 100644 index 0000000..12ac7c0 --- /dev/null +++ "b/docs/zsxq/question/\346\230\237\347\220\203\346\212\200\346\234\257\351\227\256\351\242\230\346\261\207\346\200\273.md" @@ -0,0 +1,116 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴提出的一些技术问题和解答。 + + + +**球友提问**: + +彬哥,我们项目组要求用户敏感数据加密存储,主要的问题: + +1 、加密后如何查询才能击中**索引**; + +2 、用户身份如何验证(手机号、身份证); + +大佬有什么建议吗? + +**大彬的回答**: + +首先需要一个专用于加密解密的服务。 + +所有加密解密操作都访问该服务。 可访问数据库的人, 秘钥管理人,加密服务管理人,三者不能有任何一方有办法获取到所有信息。 + +接下来回答你的两个问题: + +1、加密后如何查询才能击中索引? + +使用 **hash** 查询。加密字段只允许精准查询,不允许模糊。如果一定要模糊,那也是有限制的模糊,提前定好**模糊规则**,根据模糊规则提前预留相关数据,再进行加密。 + +2 、用户身份如何验证(手机号、身份证); 验证后再加密,或者允许解密。 + + + +需要注意的是: + +1. 不同业务场景对脱敏的要求是不一样的。我们公司的要求是,任何人任何时候都不能看到明文,明文只出现在内存中。 +2. 解密后,明文只能存在**内存**中,调用解密必须要有日志。 + +--- + +**球友提问**: + +大彬哥,请教您技术方案,我们项目有个场景,需要在一段时间内发了**n个相同的mq消息**,去更新数据,只需要**保留最后一个消息**就可以,前面的都可以不要,应该怎么实现? + +**大彬的回答**: + +收到消息后先往redis写,如果存在就直接**覆盖**,如果不存在就先写入,并且发送一条延迟消费的消息到mq,具体延迟多久可以是大于等于你说的一段时间内这个值。然后收到延迟消费的消息时直接去取redis的消息,这条消息基本就是这段时间内的最后一条消息了 + +--- + +**球友提问**: + +大佬你好,请教一个问题。 有两个百万条数据的表以及三张万以内数据表,取数据的时候不可避免要 **join**。 客户端可以自由调整日期,来查看数据。 因为表数据十分钟同步更新一次以及用户自由调整日期,感觉没办法做缓存。 现在每次查询都要 join 一次,导致查询数据很慢,要 10s 多,请问一下要怎么优化? + +**大彬的回答**: + +如果能改表结构的话,把索引和内容分开存,其实百万级的数据全放内存里应该是没问题的。如果非要 join,要么降低 join 的数据规模,要么提前算好数据,要么把能缓存的数据(比如那 3 张表)**缓存**起来,尽量降低查询时的磁盘消耗。 + +--- + +> 往期**星球提问整理**: + +**球友提问**: + +大彬哥,请教一个问题(之前面试问到的),怎么做到**增加机器线性增加性能**的? + +**大彬的回答**: + +你好。**线性扩容**有两种情况,一种是“无状态”服务,比如常见的 web 后端;另一种是“有状态服务”,比如 MySQL 数据库。 + +对于无状态服务,只要解决了服务发现功能,在服务启动之后能够把请求量引到新服务上,就可以做到线性扩容。常见的服务发现有 DNS 调度(通过域名解析到不同机器)、负载均衡调度(通过反向代理服务转发到不同机器)或者动态服务发现(比如 dubbo ),等等。 + +--- + +**球友提问**: + +彬哥,请教你两个问题: + +1. 新功能提测之类的,流程是怎样的,是各个功能特性分开提测还是一起合并了提测?另外比较偏向业务的,单元测试集成测试这些要求怎样,还是会在合入的时候进行限制,不到一定比率不允许合入? +2. 一般线上出事故了,就是开会总结、发邮件改进流程之类的。彬哥有没啥经验之类的可以分享? + +**大彬的回答**: + +1.像我们项目组的话,服务有一定程度的拆分,每周都会上线,冲突的概率不高。如果冲突了,那就合并测。单元测试有要求,但是最高就要求到40%,再高维护成本太高。 + +2.**总结发邮件**肯定是少不了的,我分享一下我们这边的流程,你可以参考一下。 + +a.发现线上事件后,需第一时间报告运营经理,项目经理。 + +b.判断线上事件的性质,对外报告处理故障进展,按流程要求通知相关负责人。 + +c.问题处理。如果问题比较紧急(故障分为A/B/C三个等级),需要立即通知相关部门的领导,由各部门领导协调事故处理,一般要在2小时内给予解决,保证系统恢复正常。 + +d.线上故障处理后都需要测试人员进行跟进,问题修复后第一时间验证,然后按照上线管理流程进行程序发布 + +e.通知业务方 + + + +持续更新中... + +> 往期**星球提问整理**: +> +> [秋招还没有offer,打算去上培训班了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495475&idx=1&sn=c9604e8310e674ceed78228373ab21b6&chksm=ce9b1275f9ec9b633e27aaf0c83abba8a7d8c25c8a84b2bf031238d3d41aa1b8cc5af7d0e3fa&token=1033350733&lang=zh_CN#rd) +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + diff --git "a/docs/zsxq/question/\347\247\213\346\213\2330offer\357\274\214\346\211\223\347\256\227\345\216\273\344\270\212\345\237\271\350\256\255\347\217\255\344\272\206....md" "b/docs/zsxq/question/\347\247\213\346\213\2330offer\357\274\214\346\211\223\347\256\227\345\216\273\344\270\212\345\237\271\350\256\255\347\217\255\344\272\206....md" new file mode 100644 index 0000000..d9e7718 --- /dev/null +++ "b/docs/zsxq/question/\347\247\213\346\213\2330offer\357\274\214\346\211\223\347\256\227\345\216\273\344\270\212\345\237\271\350\256\255\347\217\255\344\272\206....md" @@ -0,0 +1,44 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【**是否有必要参加培训班**】的提问。 + +> 往期**星球提问整理**: +> +> [非科班,如何补基础?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495380&idx=1&sn=b0555daa04aed83ea848d4b0b188dc18&chksm=ce9b1392f9ec9a84244572822a67c98fc7ac9d9f12124f779d7f98acf889f53c3db9f86c26b0#rd) +> +> [想出国读研了...](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495427&idx=1&sn=a481a4916494ed10cd9f633da55f6831&chksm=ce9b1245f9ec9b5332c02d8d98a6172dddea89e72405c537afe5a59cae2d367ae33d876a636d#rd) +> +> [今年这情况,读非全值得吗?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495462&idx=1&sn=7418dca042d395df62efef8b9bd46474&chksm=ce9b1260f9ec9b764d0b6c66d95151a5b10cfe8ec9c84d945abc54912f8262fddf1bc24e01c2&token=1033350733&lang=zh_CN#rd) +> +> [读博还是找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +彬哥,向您请教 + +我的基本情况: 本科**末流211**,**非计算机专业**,打算找Java方向的工作,目前**秋招还没拿到offer**,面试官反馈是基础比较差,打算**去上培训班**了。 + +我去打听了,周围一家培训机构收费是**15800**,5800可以分期付款,另外10000找到工作后再给,时间是 6-8 个月。听了两天试听课感觉那个老师讲的还不错。不知道彬哥是否建议我去上培训班?穷学生有点怕翻车 + +--- + +**大彬的回答**: + +你好,建议从下面三点去考虑 + +1. **金钱成本和时间成本**,一般培训班周期在6个月以上,而且学员水平参差不齐,机构老师没办法根据每个学员的能力**定制化学习路线**,只能跟着大部队走。另外**培训学费**也比较贵,对很多学生来说都是一笔很大的开销。 +2. **目的是否明确**,培训班的作用是快速入门,注意只是**入门**。培训班老师会告诉你最常用的技术是什么,怎么用。至于深度就不用想了,很难培训出高级程序员的 +3. **自学能力**怎么样。培训班仅仅是帮助你将知识整理出一个体系,这个体系给到你了,至于能不能学会还是两码事。建议你可以去 B 站先看黑马或者尚学堂的视频,一般都有二三百集,先自己看几十集,看看自己是否**适合** + +ps: 我的经历跟你类似,非科班转行,当初没有选择进培训班学习,一方面**学费很贵**,对于穷学生来说,真的贵,另一方面我的**自律能力和自学能力**还算可以,感觉也没有参加培训班的必要。 + +另外如果想在计算机行业拿到高薪的话,需要多学很多东西,培训班教的那些是远远不够的。 + + + + + diff --git "a/docs/zsxq/question/\351\235\236\347\247\221\347\217\255\357\274\214\346\203\263\350\241\245\345\237\272\347\241\200.md" "b/docs/zsxq/question/\351\235\236\347\247\221\347\217\255\357\274\214\346\203\263\350\241\245\345\237\272\347\241\200.md" new file mode 100644 index 0000000..23e4bb2 --- /dev/null +++ "b/docs/zsxq/question/\351\235\236\347\247\221\347\217\255\357\274\214\346\203\263\350\241\245\345\237\272\347\241\200.md" @@ -0,0 +1,40 @@ +大家好,我是大彬~ + +今天跟大家分享[知识星球](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)小伙伴关于【非科班转码如何补基础】的提问。 + +> 往期星球[提问](https://mp.weixin.qq.com/s/yzy-40-xeEOY34qrdIXJ4Q)整理: +> +> [读博?找工作?](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495184&idx=1&sn=f22989dda1d725583f516d07682fc9bf&chksm=ce9b1356f9ec9a4061b70f38f6b76a5af4f079f93bed8269ee744f27f2f3cac6f7ecf98325a0&token=1194034453&lang=zh_CN#rd) +> +> [性格测试真的很重要](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247494301&idx=1&sn=89c095a1feb8f29a0e87c4b7835280fc&chksm=ce9b17dbf9ec9ecde1720b74e83b4c862a31d60139662592ba8967cf291f4244e9b9bf9738e9&token=1194034453&lang=zh_CN#rd) +> +> [想找一份实习工作,需要准备什么](https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247495304&idx=1&sn=54163de5ee4da27373048dea6f41344a&chksm=ce9b13cef9ec9ad8b28635cfd2d22c723ae0731babbadb9456a35464a747d19fc6e20bebe448#rd) + +**球友提问**: + +大彬大佬,想问下非科班要补哪些基础? 求推荐视频,国内国外都行。 + +--- + +**大彬的回答**: + +你好,我也是非科班转码的,Java方向,不知道你打算想往哪个方向发展。不过没关系,无论哪个方向,计算机基础都是相通的,下面分享一下我的经验: + +1. 数据结构:程序员可以不关注硬件,软件部分就是代码的逻辑实现,其中数据结构是基础,推荐橘黄色的算法书,想进中大厂就刷 leetcode ;这部分我觉得熟悉常见数据结构,了解常见算法就够了。 +2. 操作系统:推荐电子科技大学的蒲晓蓉老师的操作系统课程,看完觉得意犹未尽再去翻翻现代操作系统或者 csapp 吧,这部分主要看下进程、内存、文件系统。 +3. 计算机网络:推荐自顶向下,重点看两章就够了,应用层和传输层,更下层的说实话用不到。这里工作用到的更多的是 http,看下图解 http 之类的,有需要的可以看下图解密码学。 +4. 数据库:推荐伯克利的 CS168 课程。国内的推荐中国人民大学王珊老师的《数据库系统概论》 +5. 编译原理:不推荐太早看,代码写多了再来看,前期直接跳过。如果你是前端程序员,至少接触过 babel 这一类工具,了解过原理之后再来学习,这门课太早接触我觉得真的没用,晦涩难懂 +6. 最后补充下个人理解:这个阶段最重要的不是深入细节,熟悉原理这一类的,看到不懂的部分直接跳过就行了,先大概过一遍建立计算机的一些基本思想和概念,比如分层和抽象、时间和空间、接口和实现、分治等等等等,先悟到这一层,再回头看书能快很多,接下来再去深入一些感兴趣的细节部分,我觉得就差不多了 + + + + + +最后给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: + +![](http://img.topjavaer.cn/image/Image.png) + +![](http://img.topjavaer.cn/image/image-20221030094126118.png) + +https://mp.weixin.qq.com/s?__biz=Mzg2OTY1NzY0MQ==&mid=2247486208&idx=1&sn=dbeedf47c50b1be67b2ef31a901b8b56&chksm=ce98f646f9ef7f506a1f7d72fc9384ba1b518072b44d157f657a8d5495a1c78c3e5de0b41efd&token=1652861108&lang=zh_CN#rd \ No newline at end of file diff --git "a/docs/zsxq/\346\230\245\346\213\233\346\235\245\344\272\206\357\274\214\345\244\247\345\256\266\351\203\275\345\234\250\345\201\267\345\201\267\345\215\267\357\274\201.md" "b/docs/zsxq/\346\230\245\346\213\233\346\235\245\344\272\206\357\274\214\345\244\247\345\256\266\351\203\275\345\234\250\345\201\267\345\201\267\345\215\267\357\274\201.md" new file mode 100644 index 0000000..72208d6 --- /dev/null +++ "b/docs/zsxq/\346\230\245\346\213\233\346\235\245\344\272\206\357\274\214\345\244\247\345\256\266\351\203\275\345\234\250\345\201\267\345\201\267\345\215\267\357\274\201.md" @@ -0,0 +1,95 @@ +你好,我是大彬~ + +这段时间春招陆续开始了,很多大厂(美团、阿里等)开启了24届春招补录,也有公司开始暑期实习招聘流程了。 + +看了小破站访问量也是嘎嘎上涨,看来大家都在偷偷卷啊。。 + +![](http://img.topjavaer.cn/img/202403020934945.png) + +最近也有挺多同学私信我,我总结了一些比较常见的问题,比如有没有内推渠道?怎么写好简历?面试八股文应该怎么准备等等。在这里分享一下~ + +## 1、有没有内推渠道? + +内推其实是换一种方式投简历,好处是可以**免简历筛选**,有时还能免笔试(比较少)。 + +为了感谢大家一直以来的支持,我特地新建了各个城市的**内推群**。 + +你可以在群里发布内推信息、招聘信息,也可以在群里交流面试、工作问题。 + +![](http://img.topjavaer.cn/img/202403021124419.png) + +**进群方式** + +添加大彬的个人微信 dabinjava 或者 i_am_dabin,备注:**内推+工作城市**,我拉你进群 + +![](http://img.topjavaer.cn/img/202403021107571.png) + +> **被添加频繁的话,请半个小时之后重试** + +## 2、怎么写好简历 + +分成以下几个方面来编写: + +- **简历模板** +- **基本信息** +- **教育经历** +- **专业技能** +- **项目经验(实习经历、工作经历)** +- **奖项荣誉** + +其中最重要的应该是项目经验这一模块,要写清楚你在项目中的角色,以及个人职责,具体负责哪一块业务等。可以重点突出**攻克的技术问题**,或者参与过的**性能优化**。 + +最好能有**数据支撑**,**数据是最有说服力的**,比如SQL执行时间从xx秒到xx毫秒、xx优化给公司节省了多少万的服务器成本等,这些在面试中很加分,比较能体现个人的实战能力。 + +在这里也吐槽一下,我帮[知识星球](https://mp.weixin.qq.com/s/VdKnOvkQa5rrqgdllSJtUw)小伙伴修改了大概**220**多份简历,很多同学简历上的项目经验,真的写的跟流水账一样。比如: + +1. 实现了xx功能 +2. 做了xx页面 +3. ... + +说实话,看完简历印象分就大打折扣了,简历关可能就被pass了。面试机会不多,特别是现在大环境不好,更要认真对待,尽力把每一个环节做好。 + +建议大家在写项目经验的时候,可以按照我整理的**模板**(出于 xx 考虑,使用 xx 解决了 xx 问题 ,达到了 xxx 效果等),这样去写,效果会好很多。 + +如果你确实不知道该如何写自己的简历,可以看看我星球的文章——**手把手教你写好一份简历** + +> 链接:https://t.zsxq.com/09wZgDO7v + +如果你还是觉得自己简历写的不够好,可以找我帮你**修改**简历,需要的可以加我微信 i_am_dabin,备注【**简历修改**】(**付费**的,介意的勿扰)。 + +![](http://img.topjavaer.cn/img/202403021121570.png) + +下面是我修改简历的一些案例。 + +![](http://img.topjavaer.cn/img/202403031119562.png) + +![](http://img.topjavaer.cn/img/202403031120321.png) + +![](http://img.topjavaer.cn/img/image-20230111224753836.png) + +## 3、先面试大公司还是小公司? + +大彬建议把自己比较**心仪的大厂放在后面**,拿一些小公司或者意愿不大的中大公司试手,积攒经验,把面试技术问题范围记录下来,查漏补缺,最后冲自己的目标公司。面试前期就是广撒网,捞小鱼积累经验, 把想捞的大鱼放在后面。 + +## 4、面试八股文应该怎么准备? + +在这里当然要推荐一下我的小破站了。 + +> 地址:topjavaer.cn + +这是一个优质的八股文网站,内容涵盖**Java基础、并发、JVM、MySQL、Springboot、MyBatis、Redis、消息队列、微服务**等,非常全面,建议面试前刷上两遍! + +![](http://img.topjavaer.cn/img/202403021025198.png) + +不少同学靠着网站上面整理的面试题找到了工作,质量还是不错的。 + +建议大家面试前先把网站上面的面试题刷上2-3遍~ + +![](http://img.topjavaer.cn/img/202403021036763.png) + +![](http://img.topjavaer.cn/img/202403021113383.png) + + + +最后祝大家春招顺利~ + diff --git "a/docs/zsxq/\350\277\231\345\217\257\350\203\275\346\230\257\346\234\200\345\205\250\346\260\221\347\232\204\351\235\242\350\257\225\351\242\230\345\272\223\344\272\206.md" "b/docs/zsxq/\350\277\231\345\217\257\350\203\275\346\230\257\346\234\200\345\205\250\346\260\221\347\232\204\351\235\242\350\257\225\351\242\230\345\272\223\344\272\206.md" new file mode 100644 index 0000000..db9492d --- /dev/null +++ "b/docs/zsxq/\350\277\231\345\217\257\350\203\275\346\230\257\346\234\200\345\205\250\346\260\221\347\232\204\351\235\242\350\257\225\351\242\230\345\272\223\344\272\206.md" @@ -0,0 +1,34 @@ +大家好,我是大彬。 + +最近是面试高峰期,有挺多读者来咨询我有没有一些**大厂的面试题目**,想在面试前重点复习一下,“抱佛腿”~ + +![](http://img.topjavaer.cn/img/202403161558009.png) + +![](http://img.topjavaer.cn/img/202403161559190.png) + +刚好我这几天有空,整理了近**300**家公司的面试题目,按照**社招、校招、实习**分类。 + +![](http://img.topjavaer.cn/img/202403161603239.png) + +**社招面经**部分截图: + +![](http://img.topjavaer.cn/img/202403161616030.png) + +![](http://img.topjavaer.cn/img/202403161618740.png) + +![](http://img.topjavaer.cn/img/202403161620835.png) + +![](http://img.topjavaer.cn/img/202403161624295.png) + +![](http://img.topjavaer.cn/img/202403161649106.png) + +**校招、实习面经**部分截图: + +![](http://img.topjavaer.cn/img/202403161626248.png) + +![](http://img.topjavaer.cn/img/202403161628679.png) + +面试题目**获取方式**:添加我微信 dabinjava 或者 i_am_dabin,**备注**【**面试题目**】。PS:有偿,白嫖勿扰(花了挺多时间整理的,希望可以理解) + +![](http://img.topjavaer.cn/img/202403161641601.png) + diff --git "a/docs/zsxq/\351\235\242\350\257\225\347\234\237\351\242\230\345\205\261\344\272\253\347\276\244.md" "b/docs/zsxq/\351\235\242\350\257\225\347\234\237\351\242\230\345\205\261\344\272\253\347\276\244.md" new file mode 100644 index 0000000..a4850d8 --- /dev/null +++ "b/docs/zsxq/\351\235\242\350\257\225\347\234\237\351\242\230\345\205\261\344\272\253\347\276\244.md" @@ -0,0 +1,59 @@ +为方便大家交流,大彬新建了**面试真题共享群,**群里可以讨论面试问题、发布内推信息,群成员可以共享各大公司的面试真题,同时可以**永久**查看大彬整理的**面试真题VIP**手册(目前整理了超过**300**家公司的面试题目)。 + +群友**评价**: + +![](http://img.topjavaer.cn/img/202404221801755.png) + +![](http://img.topjavaer.cn/img/202404221801618.png) + +![](http://img.topjavaer.cn/img/202404221801810.png) + +面试真题分享群截图: + +![](http://img.topjavaer.cn/img/202404221802888.png) + +![](http://img.topjavaer.cn/img/202404221802316.png) + +![](http://img.topjavaer.cn/img/202404221802739.png) + +![](http://img.topjavaer.cn/img/202404221802506.png) + +![](http://img.topjavaer.cn/img/202404221802781.png) + +![](http://img.topjavaer.cn/img/202404221802200.png) + +![](http://img.topjavaer.cn/img/202404221803753.png) + +![](http://img.topjavaer.cn/img/202404221817070.png) + + + +**面试真题VIP**手册截图: + +![](http://img.topjavaer.cn/img/202404221808531.png) + +**社招面经**部分截图: + +![](http://img.topjavaer.cn/img/202404221807102.png) + +![](http://img.topjavaer.cn/img/202404221807282.png) + +![](http://img.topjavaer.cn/img/202404221806214.png) + +![](http://img.topjavaer.cn/img/202404221806220.png) + +![](http://img.topjavaer.cn/img/202404221806613.png) + +**校招、实习面经**部分截图: + +![](http://img.topjavaer.cn/img/202404221806258.png) + +![](http://img.topjavaer.cn/img/202404221806239.png) + + + +面试共享群**进群方式**:添加我微信 dabinjava 或者 i_am_dabin,**备注**【**面试真题**】。 + +PS:**有偿**,白嫖勿扰(花了挺多时间整理的,希望可以理解) + +![](http://img.topjavaer.cn/img/202404221805547.png) \ No newline at end of file From 899256d8d7a47e5c1a9f3d546b894f47e2e22ee5 Mon Sep 17 00:00:00 2001 From: dabin <1713476357@qq.com> Date: Thu, 13 Jun 2024 11:27:54 +0800 Subject: [PATCH 5/8] update0613 --- ...04\345\245\275\344\271\240\346\203\257.md" | 91 +++++++++++++++++++ docs/database/mysql.md | 2 + ...25\346\211\247\350\241\214\347\232\204.md" | 67 ++++++++++++++ docs/java/java-concurrent.md | 22 ++++- docs/other/site-diary.md | 6 ++ docs/redis/redis.md | 89 ++++++++++++++++++ 6 files changed, 274 insertions(+), 3 deletions(-) create mode 100644 "docs/advance/excellent-article/MySQL\344\270\255N\344\270\252\345\206\231SQL\347\232\204\345\245\275\344\271\240\346\203\257.md" create mode 100644 "docs/database/\344\270\200\346\235\241 SQL \346\237\245\350\257\242\350\257\255\345\217\245\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" diff --git "a/docs/advance/excellent-article/MySQL\344\270\255N\344\270\252\345\206\231SQL\347\232\204\345\245\275\344\271\240\346\203\257.md" "b/docs/advance/excellent-article/MySQL\344\270\255N\344\270\252\345\206\231SQL\347\232\204\345\245\275\344\271\240\346\203\257.md" new file mode 100644 index 0000000..9a2241a --- /dev/null +++ "b/docs/advance/excellent-article/MySQL\344\270\255N\344\270\252\345\206\231SQL\347\232\204\345\245\275\344\271\240\346\203\257.md" @@ -0,0 +1,91 @@ +MySQL中编写SQL时,遵循良好的习惯能够提高查询性能、保障数据一致性、提升代码可读性和维护性。以下列举了多个编写SQL的好习惯 + +#### 1.使用EXPLAIN分析查询计划 + +在编写或优化复杂查询时,先使用EXPLAIN命令查看查询执行计划,理解MySQL如何执行查询、访问哪些表、使用哪种类型的联接以及索引的使用情况。 + +好处:有助于识别潜在的性能瓶颈,如全表扫描、错误的索引选择、过多的临时表或文件排序等,从而针对性地优化查询或调整索引结构。 + +#### 2.避免全表扫描 + +2. 习惯:尽可能利用索引来避免全表扫描,尤其是在处理大表时。确保在WHERE、JOIN条件和ORDER BY、GROUP BY子句中使用的列有适当的索引。 + +好处:极大地减少数据访问量,提高查询性能,减轻I/O压力。 + +#### 3. 为表和字段添加注释 + +3. 习惯:在创建表时,为表和每个字段添加有意义的注释,描述其用途、数据格式、业务规则等信息。 + +好处:提高代码可读性和可维护性,帮助其他开发人员快速理解表结构和字段含义,减少沟通成本和误解。 + +#### 4. 明确指定INSERT语句的列名 + +习惯:在INSERT语句中显式列出要插入数据的列名,即使插入所有列也应如此。 + +好处:避免因表结构变化导致的插入错误,增强代码的健壮性,同时也提高了语句的清晰度。 + +#### 5. 格式化SQL语句 + +习惯:保持SQL语句的格式整洁,使用一致的大小写(如关键词大写、表名和列名小写),合理缩进,避免过长的单行语句。 + +好处:提高代码可读性,便于审查、调试和团队协作。 + +#### 6. 使用LIMIT限制结果集大小 + +习惯:在执行SELECT、DELETE或UPDATE操作时,若不需要处理全部数据,务必使用LIMIT子句限制结果集大小,特别是在生产环境中。 + +好处:防止因误操作导致大量数据被修改或删除,降低风险,同时也能提高查询性能。 + +#### 7.使用JOIN语句代替子查询 + +习惯:在可能的情况下,优先使用JOIN操作代替嵌套的子查询,特别是在处理多表关联查询时。 + +好处:许多情况下JOIN的执行效率高于子查询,而且JOIN语句通常更易于理解和优化。 + +#### 8.避免在WHERE子句中对NULL进行比较 + +习惯:使用IS NULL和IS NOT NULL来检查字段是否为NULL,而不是直接与NULL进行等值或不等值比较。 + +好处:正确处理NULL值,避免逻辑错误和未预期的结果。 + +#### 9.避免在查询中使用SELECT + +习惯:明确列出需要的列名,而不是使用SELECT *从表中获取所有列。 + +好处:减少网络传输的数据量,降低I/O开销,提高查询性能,同时也有利于代码的清晰性和可维护性。 + +#### 10. 数据库对象命名规范 + +习惯:遵循一致且有意义的命名约定,如使用小写字母、下划线分隔单词,避免使用MySQL保留字,保持表名、列名、索引名等的简洁性和一致性。 + +好处:提高代码可读性,减少命名冲突,便于团队协作和维护。 + +#### 11. 事务管理 + +习惯:对一系列需要保持原子性的操作使用事务管理,确保数据的一致性。 + +好处:在发生异常时能够回滚未完成的操作,避免数据处于不一致状态。 + +#### 12.适时使用索引覆盖 + +习惯:对于只查询索引列且不需要访问数据行的查询(如计数、统计),创建覆盖索引以避免回表操作。 + +好处:极大提升查询性能,减少I/O开销。 + +#### 13.遵循第三范式或适当反范式 + +习惯:根据业务需求和查询模式,合理设计表结构,遵循第三范式以减少数据冗余和更新异常,或适当反范式以优化查询性能。 + +好处:保持数据一致性,减少数据维护成本,或提高查询效率。 + +#### 14.使用预编译语句(PreparedStatement) + +习惯:在应用程序中使用预编译语句(如Java中的PreparedStatement)执行SQL,特别是对于动态拼接SQL语句的情况。 + +好处:避免SQL注入攻击,提高查询性能,减少数据库服务器的解析开销。 + +#### 15.定期分析与优化表和索引 + +习惯:定期运行ANALYZE TABLE收集统计信息,以便MySQL优化器做出更准确的查询计划决策。根据查询性能监控结果,适时调整索引或重构表结构。 + +好处:确保数据库持续高效运行,适应不断变化的业务需求和数据分布。 \ No newline at end of file diff --git a/docs/database/mysql.md b/docs/database/mysql.md index b7946d9..c8b0032 100644 --- a/docs/database/mysql.md +++ b/docs/database/mysql.md @@ -22,6 +22,8 @@ head: ## 更新记录 +- 2024.06.05,更新[MySQL查询 limit 1000,10 和limit 10 速度一样快吗?](###MySQL查询 limit 1000,10 和limit 10 速度一样快吗?) + - 2024.5.15,新增[B树和B+树的区别?](###B树和B+树的区别?) ## 什么是MySQL diff --git "a/docs/database/\344\270\200\346\235\241 SQL \346\237\245\350\257\242\350\257\255\345\217\245\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" "b/docs/database/\344\270\200\346\235\241 SQL \346\237\245\350\257\242\350\257\255\345\217\245\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" new file mode 100644 index 0000000..94d82b5 --- /dev/null +++ "b/docs/database/\344\270\200\346\235\241 SQL \346\237\245\350\257\242\350\257\255\345\217\245\345\246\202\344\275\225\346\211\247\350\241\214\347\232\204.md" @@ -0,0 +1,67 @@ +一条 SQL 查询语句如何执行的 + +在平常的开发中,可能很多人都是 CRUD,对 SQL 语句的语法很熟练,但是说起一条 SQL 语句在 MySQL 中是怎么执行的却浑然不知,今天大彬就由浅入深,带大家一点点剖析一条 SQL 语句在 MySQL 中是怎么执行的。 + +比如你执行下面这个 SQL 语句时,我们看到的只是输入一条语句,返回一个结果,却不知道 MySQL 内部的执行过程: + +```mysql +mysql> select * from T where ID=10; +``` + +在剖析这个语句怎么执行之前,我们先看一下 MySQL 的基本架构示意图,能更清楚的看到 SQL 语句在 MySQL 的各个功能模块中的执行过程。 + +![](http://img.topjavaer.cn/img/202406030841887.png) + +整体来说,MySQL 可以分为 Server 层和存储引擎两部分。 + +Server 层包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。 + +### 连接器 + +如果要操作 MySQL 数据库,我们必须使用 MySQL 客户端来连接 MySQL 服务器,这时候就是服务器中的连接器来负责根客户端建立连接、获取权限、维持和管理连接。 + +在和服务端完成 TCP 连接后,连接器就要认证身份,需要用到用户名和密码,确保用户有足够的权限执行该SQL语句。 + +### 查询缓存 + +建立完连接后,就可以执行查询语句了,来到第二步:查询缓存。 + +MySQL 拿到第一个查询请求后,会先到查询缓存看看,之前是不是执行过这条语句。之前执行过的语句及其结果可能会以 key-value 对的形式,被直接缓存在内存中,如果你的查询能够直接在这个缓存中找到 key,那么这个 value 就会被直接返回给客户端。 + +如果语句不在查询缓存中,就会继续后面的执行阶段。执行完成后,执行结果会被存入查询缓存中。你可以看到,如果查询命中缓存,MySQL 不需要执行后面的复杂操作,就可以直接返回结果,这个效率会很高。 + +### 分析器 + +如果没有命中缓存,就要开始真正执行语句了,MySQL 首先会对 SQL 语句做解析。 + +分析器会先做 “词法分析”,MySQL 需要识别出 SQL 里面的字符串分别是什么,代表什么。 + +做完之后就要做“语法分析”,根据词法分析的结果,语法分析器会根据语法规则,判断你输入的这个 SQL 语句是否满足 MySQL 语法。若果语句不对,就会收到错误提醒。 + +### 优化器 + +经过了分析器,MySQL 就知道要做什么了,但是在开始执行之前,要先经过优化器的处理。 + +比如:优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序。 + +MySQL 会帮我去使用他自己认为的最好的方式去优化这条 SQL 语句,并生成一条条的执行计划,比如你创建了多个索引,MySQL 会依据**成本最小原则**来选择使用对应的索引,这里的成本主要包括两个方面, IO 成本和 CPU 成本。 + +### 执行器 + +执行优化之后的执行计划,在开始执行之前,先判断一下用户对这个表有没有执行查询的权限,如果没有,就会返回没有权限的错误;如果有权限,就打开表继续执行。打开表的时候,执行器就会根据表的引擎定义,去使用这个引擎提供的接口。 + +### 存储引擎 + +执行器将查询请求发送给存储引擎组件。 + +存储引擎组件负责具体的数据存储、检索和修改操作。 + +存储引擎根据执行器的请求,从磁盘或内存中读取或写入相关数据。 + +### 返回结果 + +存储引擎将查询结果返回给执行器。 + +执行器将结果返回给连接器。 + +最后,连接器将结果发送回客户端,完成整个执行过程。 \ No newline at end of file diff --git a/docs/java/java-concurrent.md b/docs/java/java-concurrent.md index e1c611a..09c408f 100644 --- a/docs/java/java-concurrent.md +++ b/docs/java/java-concurrent.md @@ -22,7 +22,9 @@ head: ## 线程池 -线程池:一个管理线程的池子。 +### 什么是线程池,如何使用?为什么要使用线程池? + +线程池就是事先将多个线程对象放到一个容器中,使用的时候就不用new线程而是直接去池中拿线程即可,节省了开辟子线程的时间,提高了代码执行效率。 ### 为什么平时都是使用线程池创建线程,直接new一个线程不好吗? @@ -35,7 +37,7 @@ head: 系统资源有限,每个人针对不同业务都可以手动创建线程,并且创建线程没有统一标准,比如创建的线程有没有名字等。当系统运行起来,所有线程都在抢占资源,毫无规则,混乱场面可想而知,不好管控。 -**频繁手动创建线程为什么开销会大?跟new Object() 有什么差别?** +### 频繁手动创建线程为什么开销会大?跟new Object() 有什么差别? 虽然Java中万物皆对象,但是new Thread() 创建一个线程和 new Object()还是有区别的。 @@ -733,9 +735,23 @@ class SeasonThreadTask implements Runnable{ ## ThreadLocal +### ThreadLocal是什么 + 线程本地变量。当使用`ThreadLocal`维护变量时,`ThreadLocal`为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。 -### ThreadLocal原理 +### 为什么要使用ThreadLocal? + +并发场景下,会存在多个线程同时修改一个共享变量的场景。这就可能会出现**线性安全问题**。 + +为了解决线性安全问题,可以用加锁的方式,比如使用`synchronized` 或者`Lock`。但是加锁的方式,可能会导致系统变慢。 + +还有另外一种方案,就是使用空间换时间的方式,即使用`ThreadLocal`。使用`ThreadLocal`类访问共享变量时,会在每个线程的本地,都保存一份共享变量的拷贝副本。多线程对共享变量修改时,实际上操作的是这个变量副本,从而保证线性安全。 + +### Thread和ThreadLocal有什么联系呢? + +Thread和ThreadLocal是绑定的, ThreadLocal依赖于Thread去执行, Thread将需要隔离的数据存放到ThreadLocal(准确的讲是ThreadLocalMap)中,来实现多线程处理。 + +### 说说ThreadLocal的原理? 每个线程都有一个`ThreadLocalMap`(`ThreadLocal`内部类),Map中元素的键为`ThreadLocal`,而值对应线程的变量副本。 diff --git a/docs/other/site-diary.md b/docs/other/site-diary.md index 426ce57..0620c06 100644 --- a/docs/other/site-diary.md +++ b/docs/other/site-diary.md @@ -8,6 +8,12 @@ sidebar: heading ## 更新记录 +- 2024.06.11,新增-聊聊如何用Redis 实现分布式锁? + +- 2024.06.07,更新-为什么Redis单线程还这么快? + +- 2024.05.21,新增一条SQL是怎么执行的 + - 2023.12.28,[增加源码解析模块,包含Sprign/SpringMVC/MyBatis(更新中)](/source/mybatis/1-overview.html)。 - 2023.12.28,[遭受黑客攻击,植入木马程序](/zsxq/article/site-hack.html)。 diff --git a/docs/redis/redis.md b/docs/redis/redis.md index 0f2a11c..0890449 100644 --- a/docs/redis/redis.md +++ b/docs/redis/redis.md @@ -824,6 +824,95 @@ Redis 的哈希表使用链地址法(separate chaining)来解决键冲突: 原理跟 Java 的 HashMap 类似,都是数组+链表的结构。当发生 hash 碰撞时将会把元素追加到链表上。 +## Redis实现分布式锁有哪些方案? + +在这里分享六种Redis分布式锁的正确使用方式,由易到难。 + +方案一:SETNX+EXPIRE + +方案二:SETNX+value值(系统时间+过期时间) + +方案三:使用Lua脚本(包含SETNX+EXPIRE两条指令) + +方案四::ET的扩展命令(SETEXPXNX) + +方案五:开源框架~Redisson + +方案六:多机实现的分布式锁Redlock + +**首先什么是分布式锁**? + +分布式锁是一种机制,用于确保在分布式系统中,多个节点在同一时刻只能有一个节点对共享资源进行操作。它是解决分布式环境下并发控制和数据一致性问题的关键技术之一。 + +分布式锁的特征: + +1、「互斥性」:任意时刻,只有一个客户端能持有锁。 + +2、「锁超时释放」:持有锁超时,可以释放,防止不必要的资源浪费,也可以防止死锁。 + +3、「可重入性」“一个线程如果获取了锁之后,可以再次对其请求加锁。 + +4、「安全性」:锁只能被持有的客户端删除,不能被其他客户端删除 + +5、「高性能和高可用」:加锁和解锁需要开销尽可能低,同时也要保证高可用,避免分布式锁失效。 + + + +**Redis分布式锁方案一:SETNX+EXPIRE** + +提到Redis的分布式锁,很多朋友可能就会想到setnx+expire命令。即先用setnx来抢锁,如果抢到之后,再用expire给锁设置一个过期时间,防止锁忘记了释放。SETNX是SETIF NOT EXISTS的简写。日常命令格式是SETNXkey value,如果 key不存在,则SETNX成功返回1,如果这个key已经存在了,则返回0。假设某电商网站的某商品做秒杀活动,key可以设置为key_resource_id,value设置任意值,伪代码如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718076327854-c75a4b72-4a8a-4afb-87fe-378082b36046.png) + +缺陷:加锁与设置过期时间是非原子操作,如果加锁后未来得及设置过期时间系统异常等,会导致其他线程永远获取不到锁。 + +**Redis分布式锁方案二**:SETNX+value值(系统时间+过期时间) + +为了解决方案一,「发生异常锁得不到释放的场景」,有小伙伴认为,可以把过期时间放到setnx的value值里面。如果加锁失败,再拿出value值校验一下即可。 + +这个方案的优点是,避免了expire 单独设置过期时间的操作,把「过期时间放到setnx的value值」里面来。解决了方案一发生异常,锁得不到释放的问题。 + +但是这个方案有别的缺点:过期时间是客户端自己生成的(System.currentTimeMillis()是当前系统的时间),必须要求分布式环境下,每个客户端的时间必须同步。如果锁过期的时候,并发多个客户端同时请求过来,都执行jedis.get()和set(),最终只能有一个客户端加锁成功,但是该客户端锁的过期时间,可能被别的客户端覆盖。该锁没有保存持有者的唯一标识,可能坡别的客户端释放/解锁 + +**分布式锁方案三:使用Lua脚本(包含SETNX+EXPIRE两条指令)** + +实际上,我们还可以使用Lua脚本来保证原子性(包含setnx和expire两条指令),lua脚本如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718075869527-a3704805-53a6-4bd4-be07-2558cff533a2.png) + +加锁代码如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718075859795-a0cfcfe0-7c56-49ac-9182-6b203739a99e.png) + +**Redis分布式锁方案四:SET的扩展命令(SET EX PX NX)** + +除了使用,使用Lua脚本,保证SETNX+EXPIRE两条指令的原子性,我们还可以巧用Redis的SET指令扩展参数。(`SET key value[EX seconds]`PX milliseconds][NX|XX]`),它也是原子性的 + +`SET key value[EX seconds][PX milliseconds][NX|XX]` + +1. NX:表示key不存在的时候,才能set成功,也即保证只有第一个客户端请求才能获得锁,而其他客户端请求只能等其释放锁, 才能获取。 +2. EXseconds:设定key的过期时间,时间单位是秒。 +3. PX milliseconds:设定key的过期时间,单位为毫秒。 +4. XX:仅当key存在时设置值。 + +伪代码如下: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718075985907-86dd8066-001a-4957-a998-897cdc27c831.png) + +**Redis分布式锁方案五:Redisson框架** + +方案四还是可能存在「锁过期释放,业务没执行完」的问题。设想一下,是否可以给获得锁的线程,开启一个定时守护线程,每隔一段时间检查锁是否还存在,存在则对锁的过期时间延长,防止锁过期提前释放。当前开源框架Redisson解决了这个问题。一起来看下Redisson底层原理图: + +![img](https://cdn.nlark.com/yuque/0/2024/png/28848830/1718076061807-8b2419dd-13ff-441e-a238-30bf402b07fb.png) + +只要线程一加锁成功,就会启动一个watchdog看门狗,它是一个后台线程,会每隔10秒检查一下,如果线程1还持有锁,那么就会不断的延长锁key的生存时间。因此,Redisson就是使用Redisson解决了「锁过期释放,业务没执行完」问题。 + +**分布式锁方案六:多机实现的分布式锁Redlock+Redisson** + +前面五种方案都是基于单机版的讨论,那么集群部署该怎么处理? + +答案是多机实现的分布式锁Redlock+Redisson + ![](http://img.topjavaer.cn/img/20220612101342.png) From 0d261007d550249de771fe9add5db808efbc599f Mon Sep 17 00:00:00 2001 From: dabin <1713476357@qq.com> Date: Fri, 20 Dec 2024 17:14:13 +0800 Subject: [PATCH 6/8] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E7=BD=91=E7=AB=99title?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/.vuepress/config.ts | 2 +- docs/advance/system-design/README.md | 4 +- docs/computer-basic/network.md | 4 +- docs/framework/springmvc.md | 4 +- docs/java/jvm.md | 4 +- docs/other/site-diary.md | 2 + docs/zsxq/introduce.md | 6 +- docs/zsxq/mianshishouce.md | 4 +- ...56\351\242\230\346\216\222\346\237\245.md" | 95 +++++++++++++++++++ 9 files changed, 111 insertions(+), 14 deletions(-) create mode 100644 "docs/zsxq/\347\272\277\344\270\212CPU\351\243\231\345\215\207100%\351\227\256\351\242\230\346\216\222\346\237\245.md" diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index 8fa92a8..63869db 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -8,7 +8,7 @@ import { gitPlugin } from '@vuepress/plugin-git' export default defineUserConfig({ lang: "zh-CN", - title: "Java学习&面试指南-程序员大彬", + title: "大彬", description: "Java学习、面试指南,涵盖大部分 Java 程序员所需要掌握的核心知识", base: "/", dest: './public', diff --git a/docs/advance/system-design/README.md b/docs/advance/system-design/README.md index 515b3d5..dace54d 100644 --- a/docs/advance/system-design/README.md +++ b/docs/advance/system-design/README.md @@ -26,8 +26,8 @@ 怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -**扫描以下二维码**领取50元的优惠券即可加入。星球定价**218**元,减去**50**元的优惠券,等于说只需要**168**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.46元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**218**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 ![](http://img.topjavaer.cn/img/202312280808000.png) \ No newline at end of file diff --git a/docs/computer-basic/network.md b/docs/computer-basic/network.md index c407f6d..fb5b6a8 100644 --- a/docs/computer-basic/network.md +++ b/docs/computer-basic/network.md @@ -41,8 +41,8 @@ head: 怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -**扫描以下二维码**领取50元的优惠券即可加入。星球定价**218**元,减去**50**元的优惠券,等于说只需要**168**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.46元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**218**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 ![](http://img.topjavaer.cn/img/202304212233017.png) diff --git a/docs/framework/springmvc.md b/docs/framework/springmvc.md index f0bc2ae..44f29a7 100644 --- a/docs/framework/springmvc.md +++ b/docs/framework/springmvc.md @@ -43,8 +43,8 @@ head: 怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -**扫描以下二维码**领取50元的优惠券即可加入。星球定价**218**元,减去**50**元的优惠券,等于说只需要**168**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.46元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**218**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 ![](http://img.topjavaer.cn/img/202304212233017.png) \ No newline at end of file diff --git a/docs/java/jvm.md b/docs/java/jvm.md index 9120ef0..532c75b 100644 --- a/docs/java/jvm.md +++ b/docs/java/jvm.md @@ -26,8 +26,8 @@ 怎么加入[知识星球](https://topjavaer.cn/zsxq/introduce.html)? -**扫描以下二维码**领取50元的优惠券即可加入。星球定价**218**元,减去**50**元的优惠券,等于说只需要**168**元的价格就可以加入,服务期一年,**每天只要三毛钱**(0.46元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**218**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 ![](http://img.topjavaer.cn/img/202312280805419.png) \ No newline at end of file diff --git a/docs/other/site-diary.md b/docs/other/site-diary.md index 0620c06..477801a 100644 --- a/docs/other/site-diary.md +++ b/docs/other/site-diary.md @@ -8,6 +8,8 @@ sidebar: heading ## 更新记录 +- 2024.06.11,更新-Redis大key怎么处理? + - 2024.06.11,新增-聊聊如何用Redis 实现分布式锁? - 2024.06.07,更新-为什么Redis单线程还这么快? diff --git a/docs/zsxq/introduce.md b/docs/zsxq/introduce.md index 7e2d967..4dad691 100644 --- a/docs/zsxq/introduce.md +++ b/docs/zsxq/introduce.md @@ -205,10 +205,10 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, ## 怎么进入星球? -如果你下定决心要加入的话,可以直接扫下面这个二维码。星球定价**218**元,减去**50**元的优惠券,等于说只需要**168**元(**拒绝割韭菜**)的价格就可以加入,服务期一年,**每天只要三毛钱**(0.46元),相比培训班几万块的学费,非常值了,星球提供的服务**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**218**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款**~ +PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款** ![](http://img.topjavaer.cn/img/202312280801529.png) \ No newline at end of file diff --git a/docs/zsxq/mianshishouce.md b/docs/zsxq/mianshishouce.md index 287488c..a7cfbd9 100644 --- a/docs/zsxq/mianshishouce.md +++ b/docs/zsxq/mianshishouce.md @@ -76,9 +76,9 @@ ## 怎么进入星球? -如果你下定决心要加入的话,可以直接扫下面这个二维码。星球定价**158**元,减去**50**元的优惠券,等于说只需要**108**元(**拒绝割韭菜**)的价格就可以加入,服务期一年,**每天不到三毛钱**(0.29元),相比培训班几万块的学费,非常值了,星球提供的服务**远超**门票价格了。 +**扫描以下二维码**领取50元的优惠券即可加入。星球定价**188**元,减去**50**元的优惠券,等于说只需要**138**元的价格就可以加入,服务期一年,**每天只要4毛钱**(0.37元),相比培训班几万块的学费,非常值了,星球提供的服务可以说**远超**门票价格了。 -随着星球内容不断积累,星球定价也会不断**上涨**,所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 +随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款**~ diff --git "a/docs/zsxq/\347\272\277\344\270\212CPU\351\243\231\345\215\207100%\351\227\256\351\242\230\346\216\222\346\237\245.md" "b/docs/zsxq/\347\272\277\344\270\212CPU\351\243\231\345\215\207100%\351\227\256\351\242\230\346\216\222\346\237\245.md" new file mode 100644 index 0000000..57a099a --- /dev/null +++ "b/docs/zsxq/\347\272\277\344\270\212CPU\351\243\231\345\215\207100%\351\227\256\351\242\230\346\216\222\346\237\245.md" @@ -0,0 +1,95 @@ +线上CPU飙升100%问题排查 + +对于互联网公司,线上CPU飙升的问题很常见(例如某个活动开始,流量突然飙升时),特此整理排查方法一篇,供大家参考讨论提高。 + +## 二、问题复现 + +线上系统突然运行缓慢,CPU飙升,甚至到100%,以及Full GC次数过多,接着就是各种报警:例如接口超时报警等。此时急需快速线上排查问题。 + +## 三、问题排查 + +不管什么问题,既然是CPU飙升,肯定是查一下耗CPU的线程,然后看看GC。 + +### 3.1 核心排查步骤 + +1.执行“top”命令``:查看所有进程占系统CPU的排序。极大可能排第一个的就是咱们的java进程(COMMAND列)。PID那一列就是进程号。`` + +2.执行“top -Hp 进程号”命令:查看java进程下的所有线程占CPU的情况。 + +3.执行“printf "%x\n 10"命令 :后续查看线程堆栈信息展示的都是十六进制,为了找到咱们的线程堆栈信息,咱们需要把线程号转成16进制。例如,printf "%x\n 10-》打印:a,那么在jstack中线程号就是0xa. + +4.执行 “jstack 进程号 | grep 线程ID” 查找某进程下-》线程ID(jstack堆栈信息中的nid)=0xa的线程状态。如果“"VM Thread" os_prio=0 tid=0x00007f871806e000 nid=0xa runnable”,第一个双引号圈起来的就是线程名,如果是“VM Thread”这就是虚拟机GC回收线程了 + +5.执行“jstat -gcutil 进程号 统计间隔毫秒 统计次数(缺省代表一致统计)”,查看某进程GC持续变化情况,如果发现返回中FGC很大且一直增大-》确认Full GC! 也可以使用“jmap -heap 进程ID”查看一下进程的堆内从是不是要溢出了,特别是老年代内从使用情况一般是达到阈值(具体看垃圾回收器和启动时配置的阈值)就会进程Full GC。 + +6.执行“jmap -dump:format=b,file=filename 进程ID”,导出某进程下内存heap输出到文件中。可以通过eclipse的mat工具查看内存中有哪些对象比较多 + +### 3.2 原因分析 + +#### 1.内存消耗过大,导致Full GC次数过多 + +执行步骤1-5: + +- 多个线程的CPU都超过了100%,通过jstack命令可以看到这些线程主要是垃圾回收线程-》上一节步骤2 +- 通过jstat命令监控GC情况,可以看到Full GC次数非常多,并且次数在不断增加。--》上一节步骤5 + +确定是Full GC,接下来找到**具体原因**: + +- 生成大量的对象,导致内存溢出-》执行步骤6,查看具体内存对象占用情况。 +- 内存占用不高,但是Full GC次数还是比较多,此时可能是代码中手动调用 System.gc()导致GC次数过多,这可以通过添加 -XX:+DisableExplicitGC来禁用JVM对显示GC的响应。 + +#### 2.代码中有大量消耗CPU的操作,导致CPU过高,系统运行缓慢; + +执行步骤1-4:在步骤4jstack,可直接定位到代码行。例如某些复杂算法,甚至算法BUG,无限循环递归等等。 + +#### 3.由于锁使用不当,导致死锁。 + +执行步骤1-4: 如果有死锁,会直接提示。关键字:deadlock.步骤四,会打印出业务死锁的位置。 + +造成死锁的原因:最典型的就是2个线程互相等待对方持有的锁。 + +#### 4.随机出现大量线程访问接口缓慢。 + +代码某个位置有阻塞性的操作,导致该功能调用整体比较耗时,但出现是比较随机的;平时消耗的CPU不多,而且占用的内存也不高。 + +思路: + +首先找到该接口,通过压测工具不断加大访问力度,大量线程将阻塞于该阻塞点。 + +执行步骤1-4: + +``` +"http-nio-8080-exec-4" #31 daemon prio=5 os_prio=31 tid=0x00007fd08d0fa000 nid=0x6403 waiting on condition [0x00007000033db000] + + java.lang.Thread.State: TIMED_WAITING (sleeping)-》期限等待 + + at java.lang.Thread.sleep(Native Method) + + at java.lang.Thread.sleep(Thread.java:340) + + at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386) + + at com.*.user.controller.UserController.detail(UserController.java:18)-》业务代码阻塞点 +``` + +如上图,找到业务代码阻塞点,这里业务代码使用了TimeUnit.sleep()方法,使线程进入了TIMED_WAITING(期限等待)状态。 + +#### 5.某个线程由于某种原因而进入WAITING状态,此时该功能整体不可用,但是无法复现; + +执行步骤1-4:jstack多查询几次,每次间隔30秒,对比一直停留在parking 导致的WAITING状态的线程。例如CountDownLatch倒计时器,使得相关线程等待->AQS->LockSupport.park()。 + +``` +"Thread-0" #11 prio=5 os_prio=31 tid=0x00007f9de08c7000 nid=0x5603 waiting on condition [0x0000700001f89000] +java.lang.Thread.State: WAITING (parking) ->无期限等待 +at sun.misc.Unsafe.park(Native Method) +at java.util.concurrent.locks.LockSupport.park(LockSupport.java:304) +at com.*.SyncTask.lambda$main$0(SyncTask.java:8)-》业务代码阻塞点 +at com.*.SyncTask$$Lambda$1/1791741888.run(Unknown Source) +at java.lang.Thread.run(Thread.java:748) +``` + + + +## 四、总结 + +按照3.1节的6个步骤走下来,基本都能找到问题所在。 \ No newline at end of file From 822f77e406a4313afee01df659430250924b4c94 Mon Sep 17 00:00:00 2001 From: dabin <1713476357@qq.com> Date: Sat, 1 Mar 2025 13:15:09 +0800 Subject: [PATCH 7/8] =?UTF-8?q?=E6=9B=B4=E6=96=B0=E6=98=9F=E7=90=83?= =?UTF-8?q?=E4=BC=98=E6=83=A0=E5=88=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/advance/system-design/README.md | 2 +- docs/campus-recruit/share/2-years-tech-upgrade.md | 2 +- docs/career-plan/3-years-reflect.md | 2 +- docs/career-plan/how-to-prepare-job-hopping.md | 2 +- docs/career-plan/java-or-bigdata.md | 2 +- docs/computer-basic/network.md | 2 +- docs/framework/springboot/springboot-cross-domain.md | 2 +- docs/framework/springmvc.md | 2 +- docs/java/jvm.md | 2 +- docs/system-design/README.md | 2 +- docs/zsxq/inner-material.md | 2 +- docs/zsxq/introduce.md | 4 ++-- docs/zsxq/mianshishouce.md | 2 +- docs/zsxq/question/2-years-tech-no-upgrade.md | 2 +- docs/zsxq/question/3-years-confusion.md | 2 +- docs/zsxq/question/frontend-or-backend.md | 2 +- docs/zsxq/question/how-to-prepare-job-hopping.md | 2 +- docs/zsxq/question/java-or-bigdata.md | 2 +- 18 files changed, 19 insertions(+), 19 deletions(-) diff --git a/docs/advance/system-design/README.md b/docs/advance/system-design/README.md index dace54d..15ef840 100644 --- a/docs/advance/system-design/README.md +++ b/docs/advance/system-design/README.md @@ -30,4 +30,4 @@ 随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -![](http://img.topjavaer.cn/img/202312280808000.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/campus-recruit/share/2-years-tech-upgrade.md b/docs/campus-recruit/share/2-years-tech-upgrade.md index b2aa471..5322d98 100644 --- a/docs/campus-recruit/share/2-years-tech-upgrade.md +++ b/docs/campus-recruit/share/2-years-tech-upgrade.md @@ -58,4 +58,4 @@ head: **加入方式**:**扫描二维码**领取优惠券即可加入~ -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/career-plan/3-years-reflect.md b/docs/career-plan/3-years-reflect.md index 8c4c12c..21e4cfc 100644 --- a/docs/career-plan/3-years-reflect.md +++ b/docs/career-plan/3-years-reflect.md @@ -95,5 +95,5 @@ head: **加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/career-plan/how-to-prepare-job-hopping.md b/docs/career-plan/how-to-prepare-job-hopping.md index 0dea290..59aee75 100644 --- a/docs/career-plan/how-to-prepare-job-hopping.md +++ b/docs/career-plan/how-to-prepare-job-hopping.md @@ -85,4 +85,4 @@ head: **加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/career-plan/java-or-bigdata.md b/docs/career-plan/java-or-bigdata.md index d3bbf32..cf98ec1 100644 --- a/docs/career-plan/java-or-bigdata.md +++ b/docs/career-plan/java-or-bigdata.md @@ -59,4 +59,4 @@ head: **加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/computer-basic/network.md b/docs/computer-basic/network.md index fb5b6a8..831488a 100644 --- a/docs/computer-basic/network.md +++ b/docs/computer-basic/network.md @@ -45,4 +45,4 @@ head: 随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -![](http://img.topjavaer.cn/img/202304212233017.png) +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/framework/springboot/springboot-cross-domain.md b/docs/framework/springboot/springboot-cross-domain.md index fa7dac3..b93a909 100644 --- a/docs/framework/springboot/springboot-cross-domain.md +++ b/docs/framework/springboot/springboot-cross-domain.md @@ -90,4 +90,4 @@ public class AccountController { 6、打卡学习,**大学自习室的氛围**,一起蜕变成长 -![](http://img.topjavaer.cn/img/星球优惠券-学习网站.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/framework/springmvc.md b/docs/framework/springmvc.md index 44f29a7..6bf2369 100644 --- a/docs/framework/springmvc.md +++ b/docs/framework/springmvc.md @@ -47,4 +47,4 @@ head: 随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -![](http://img.topjavaer.cn/img/202304212233017.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/java/jvm.md b/docs/java/jvm.md index 532c75b..5400aac 100644 --- a/docs/java/jvm.md +++ b/docs/java/jvm.md @@ -30,4 +30,4 @@ 随着星球内容不断积累,星球定价也会不断**上涨**(最初原价**68**元,现在涨到**188**元了,后面还会持续**上涨**),所以,想提升自己的小伙伴要趁早加入,**早就是优势**(优惠券只有50个名额,用完就恢复**原价**了)。 -![](http://img.topjavaer.cn/img/202312280805419.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/system-design/README.md b/docs/system-design/README.md index 4ea61d7..dac1d6d 100644 --- a/docs/system-design/README.md +++ b/docs/system-design/README.md @@ -24,4 +24,4 @@ [知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: -![](http://img.topjavaer.cn/img/202312280805419.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/inner-material.md b/docs/zsxq/inner-material.md index 536c880..613a61e 100644 --- a/docs/zsxq/inner-material.md +++ b/docs/zsxq/inner-material.md @@ -22,4 +22,4 @@ [知识星球](https://topjavaer.cn/zsxq/introduce.html)**加入方式**: -![](http://img.topjavaer.cn/img/202312280805419.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/introduce.md b/docs/zsxq/introduce.md index 4dad691..a72a60d 100644 --- a/docs/zsxq/introduce.md +++ b/docs/zsxq/introduce.md @@ -73,7 +73,7 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, 如果你加入了,希望你也能跟像球友们一样**每天坚持打卡学习,为未来奋斗**~ -![](http://img.topjavaer.cn/img/202312280800515.png) +![](http://img.topjavaer.cn/img/202412271108286.png) ## 学习圈能提供什么? @@ -211,4 +211,4 @@ APP端页面如下(建议大家**使用APP**,因为APP布局更加美观, PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款** -![](http://img.topjavaer.cn/img/202312280801529.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/mianshishouce.md b/docs/zsxq/mianshishouce.md index a7cfbd9..24d1368 100644 --- a/docs/zsxq/mianshishouce.md +++ b/docs/zsxq/mianshishouce.md @@ -82,4 +82,4 @@ PS:如果加入学习圈之后觉得不合适,**支持3天内全额退款**~ -![](http://img.topjavaer.cn/img/202304212233017.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/question/2-years-tech-no-upgrade.md b/docs/zsxq/question/2-years-tech-no-upgrade.md index 3bf789f..b4eccf3 100644 --- a/docs/zsxq/question/2-years-tech-no-upgrade.md +++ b/docs/zsxq/question/2-years-tech-no-upgrade.md @@ -58,4 +58,4 @@ head: **加入方式**:**扫描二维码**领取优惠券即可加入~ -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/question/3-years-confusion.md b/docs/zsxq/question/3-years-confusion.md index 7fde282..1888eda 100644 --- a/docs/zsxq/question/3-years-confusion.md +++ b/docs/zsxq/question/3-years-confusion.md @@ -89,5 +89,5 @@ head: 6、打卡学习,**大学自习室的氛围**,一起蜕变成长 -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/zsxq/question/frontend-or-backend.md b/docs/zsxq/question/frontend-or-backend.md index b33ccf4..35e6f0c 100644 --- a/docs/zsxq/question/frontend-or-backend.md +++ b/docs/zsxq/question/frontend-or-backend.md @@ -31,4 +31,4 @@ 最后,给大家送福利啦,限时发放10张[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)60元的优惠券,先到先得!目前[知识星球](https://mp.weixin.qq.com/s/6eAOmYiNhEjIvhhXoXm9QQ)已经有**300**多位成员了,想加入的小伙伴不要错过这一波优惠活动,**扫描下方二维码**领取优惠券即可加入。 -![](http://img.topjavaer.cn/img/星球优惠券0317-减60.png) \ No newline at end of file +![](http://img.topjavaer.cn/img/202412271108286.png) \ No newline at end of file diff --git a/docs/zsxq/question/how-to-prepare-job-hopping.md b/docs/zsxq/question/how-to-prepare-job-hopping.md index 0dea290..59aee75 100644 --- a/docs/zsxq/question/how-to-prepare-job-hopping.md +++ b/docs/zsxq/question/how-to-prepare-job-hopping.md @@ -85,4 +85,4 @@ head: **加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) +![](http://img.topjavaer.cn/img/202412271108286.png) diff --git a/docs/zsxq/question/java-or-bigdata.md b/docs/zsxq/question/java-or-bigdata.md index d3bbf32..cf98ec1 100644 --- a/docs/zsxq/question/java-or-bigdata.md +++ b/docs/zsxq/question/java-or-bigdata.md @@ -59,4 +59,4 @@ head: **加入方式**:**扫描二维码**领取优惠券加入(**即将恢复原价**)~ -![](http://img.topjavaer.cn/img/星球优惠券-b站.png) +![](http://img.topjavaer.cn/img/202412271108286.png) From 9690a43241c7d21a77faa0928a27499d46d47c6d Mon Sep 17 00:00:00 2001 From: dabin <1713476357@qq.com> Date: Sat, 1 Mar 2025 21:40:24 +0800 Subject: [PATCH 8/8] =?UTF-8?q?=E7=9F=A5=E8=AF=86=E6=98=9F=E7=90=83?= =?UTF-8?q?=E9=A6=96=E9=A1=B5=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/README.md | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/docs/README.md b/docs/README.md index 4b49ed1..9f8ba39 100644 --- a/docs/README.md +++ b/docs/README.md @@ -47,26 +47,10 @@ projects: [](https://www.zhihu.com/people/dai-shu-bin-13) [](https://github.com/Tyson0314/java-books) -## 秋招提前批信息汇总 - -[秋招提前批及正式批信息汇总(含内推)](https://docs.qq.com/sheet/DYW9ObnpobXNRTXpq?tab=BB08J2) - ## 面试手册电子版 本网站所有内容已经汇总成**PDF电子版**,**PDF电子版**在我的[**学习圈**](zsxq/introduce.md)可以获取~ -## 计算机经典书籍PDF - -给大家分享**200多本计算机经典书籍PDF电子书**,包括**C语言、C++、Java、Python、前端、数据库、操作系统、计算机网络、数据结构和算法、机器学习、编程人生**等,感兴趣的小伙伴可以自取: - -![](http://img.topjavaer.cn/image/Image.png) - -![](http://img.topjavaer.cn/image/image-20221030094126118.png) - -**200多本计算机经典书籍PDF电子书**:https://pan.xunlei.com/s/VNlmlh9jBl42w0QH2l4AJaWGA1?pwd=j8eq# - -备用链接:https://pan.quark.cn/s/3f1321952a16 - ## 学习路线 ![](http://img.topjavaer.cn/img/20220530232715.png)