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

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


  • 首页

  • 归档

  • 搜索

妙啊,阻塞到底是个啥?黄袍加身,亦能谈古说今

发表于 2020-08-16

不羡鸳鸯不羡仙,一行代码调半天。原创:小姐姐味道(ID:xjjdog),欢迎分享,转载请保留出处。

现在,请记住你的身份!从进入本篇文章开始,你就是皇帝!三宫六院七十二妃,任君品尝。

人有亲疏远近,事有轻重缓急。作为万岁,你的时间非常宝贵。整个王朝都在你手中运算,方能国泰民安。

为了讨论方便,我们把场景界限在单核CPU上。你就是CPU,当然是仅仅是一颗单核的CPU。

为了让你更好的安排自己的时间,我将你的时间切割成了八九七十二份,每一份都弥足珍贵。

就凭我画的这些密密麻麻的小方块,你就应该给xjjdog点下赞。

现实的CPU,时间片分的会更细,但作为人类你是理解不了那么小的时间间隔的:你可能每天都要花很多时间在吃喝拉撒上,但后宫里总有大部分希望得到你宠幸的妃子,你一点时间片都不留给她。

实在是忙不过来呀!需要一个太监!

  1. 中断就是从中断掉

不是让太监来帮你干活的,他没有那个能力。太监是用来给你调度工作的。

比如,有反叛的军队攻到了城外,太监慌慌张张来报告,你就不得不暂停后宫的活动,提着裤子处理首要的问题;再比如,有刚来的妃子频频抛媚眼,但你还有一大堆公文要批,心有余而力不足。

这种处理问题的方式,就是中断(从中断掉就是太监)。中断是指在CPU正常运行期间,由于内外部事件或由程序预先安排的事件引起的 CPU 暂时停止正在运行的程序,转而为该内部或外部事件或预先安排的事件服务的程序中去,服务完毕后再返回去继续运行被暂时中断的程序。

我们来看下底层的中断处理程序。

1
2
c复制代码request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,
const char *name, void *dev)

可以看到,太监只需要给皇帝要做的事情,都编码备案,并固定下处理流程,调整好优先级,皇帝的时间片就可以有效的轮转起来。不至于江山都丢了,还在后宫里风花雪月。

拿网络传输来说,当有了网络数据包,就需要及时处理,否则客户端会超时。这个时候,网卡会立马发出中断请求,CPU就会通过网卡的中断程序去处理这些缓冲区。这都是非常重要的工作。

中断又有硬中断和软中断之分。硬中断是由硬件产生的,比如,像磁盘,网卡,键盘,时钟等。软中断是由当前正在运行的进程所产生的,通常优先级比硬中断低一些。

  1. 阻塞会占用CPU么?

代入了皇帝这个身份,我们就可以解释一些平常遇到的,令人疑惑的问题。

我们都见过在Concurent包下面,有一个叫做LinkedBlockingQeque的类。从它的名字就可以看出,这是一个阻塞队列。实际上,它也并不是挂着羊头卖狗肉。

如下面的代码,我们通常把它放在循环中。我对while(true)这种东西是有心理阴影的,因为它有可能会跑满你的CPU。

1
2
3
java复制代码while(true){
Object o = linkedBlockingQeque.poll();
}

但实际上,并不会。因为人家都说了,这是个阻塞队列。

相似的,还有NIO中的select。把逻辑放在while循环里,不怕得报应么?

1
2
3
4
5
6
7
JAVA复制代码 while (!stop) {
int num = selector.select();
if (num == 0) {
continue;
}
Iterator<SelectionKey> events = selector.selectedKeys().iterator();
}

这还真不怕。因为阻塞并不会占用任何资源。

比如,小太监上报了一个折子,是关于吕嫔妃的舅舅的贪污问题处理。但是这个问题,需要等待司法调查的结果,还需要听听爱妃的意见,就先可以把它搁置在一旁。

把问题记录在一个其他的小册子里,等这些依赖的事办的差不多了,同时你又有龙时,那就可以继续处理。

可以看到,这种阻塞性的问题,虽然是个任务,但并不会占用你的任何时间,这在计算机中是一样的。

我们来看一下常见的Java阻塞方式。

sleep和wait

睡和等。用词很巧妙,到底妙在哪呢?因为它是现实中的场景。

sleep

sleep函数会让线程在一定的时间内进入阻塞状态,不能得到cpu时间,但不会释放锁资源。指定的时间一过,线程重新进入可执行状态。

注意我们这里说的是线程,并不是CPU本身。线程不活动了,并不代表CPU不能干其他事情。

比如,今天是接见大臣的黄道吉日,王天师得到了接见的机会,其他的大臣们就得在外面等着被传唤。结果王天师的谈话又臭又长,勾不起你的任何兴趣。正好小太监急匆匆跑来,在你耳边悄悄说: 李贵妃生了个儿子!

这是让人振奋的事情,因为其他儿子都在宫斗中被KO了。于是你装模作样的对王天师说: 我现在有点头痛,需要小憩一会儿。” 其实你已经偷偷去探望李贵妃了。

注意,这个时候,王天师只能唯唯诺诺的等着。对于“接见”这个主线来说,其他的大臣也只能在外面等着被传唤。它们都没有拿到“接见”这把锁,王天师也一直占用着这把锁,直到你看完了儿子归来。

这就是sleep不释放锁的意思,因为sleep后,在sleep那一瞬间的任何东西都没有改变。

wait

wait( ) 使线程进入阻塞状态,同时释放自己占有的锁资源,和notify( )搭配使用。

对于wait来说,就完全不一样了。

如图,每个监视器(Monitor)在某个时刻,只能被一个线程拥有,该线程就是 “Active Thread”。而其它线程都是 “Waiting Thread”,分别在两个队列 “ Entry Set”和 “Wait Set”里面等候。在 “Entry Set”中等待的线程状态是 “Waiting for monitor entry”,而在 “Wait Set”中等待的线程状态是 “in Object.wait()”。

术语难以理解,还是以皇帝的身份来潇洒一下。

这个时候,你还打算接见大臣。不过,现在不想再one by one了,因为这太低效太枯燥了。某个大臣在你的书房里待得长了些,就有可能有大臣怀疑你在搞gay,这种副作用让人心里不悦。

p2p不行,那就聚在一块谈谈心吧。

正在和你谈话的是王天师,因为这货话比较多,你也比较喜欢他。

王天师说: 小太子出生在三伏天,就叫史三伏吧!。

你这才想起自己姓史。作为熟读文章的皇帝,你对此嗤之以鼻,听着这不入流的名字,还隐隐有点生气。

王爱卿,你还是先wait一下吧,听听别人意见。

这个时候,一大堆等着拍马屁的大臣开始举手,跃跃欲试。刘道长抢到了 谈话主线 这把锁。

刘道长: 天地长久,人有终时,北冥有鱼,其名为鲲,可活亿年。我看,就叫史鲲吧。

你听后微微颔首,果然仙人嘴下口水香,但总感觉有点怪异。

注意注意。等着发言的这群大臣,就叫做Entry Set,谁举手举得快,就可以回答这个问题。

像王天师这种被喊停的大臣,就属于Wait Set,只有你重新让他说话,他才有机会。

这整个过程,谈话是可以继续的,并不因为王天师被禁言了谈话就无法进行下去。我们就可以说,wait操作是释放了对象锁的。

计算机中各种所谓的阻塞,都是通过划分不同的队列资源进行处理。比如epoll就是围绕着工作队列和等待队列进行编程的。虽然底层的数据结构有些不同,但思想都是一样的。

线程如何获取时间片?

这个不容易回答,因为你需要知道一个事实:Java中的线程,在Linux上本质是一个轻量级进程,它的调度都是操作系统来完成的。

可以看一下我们最上面那一副让人容易产生密集恐惧症的图片。我们的CPU时间,就划分为多个CPU时间片。你的程序虽然在执行while(true),但不代表它总能够得到CPU资源,所以其他的进程也有机会去执行。

JVM采用抢占式调度模型,指的是让优先级高的线程占用比较多的CPU,如果线程优先级相同,那么就随机选择一个线程,使其占用CPU。

注意“随机”这两个字,就非常的有魔性。它可以让你每天都中100万的彩票,也可能每天喝水都被呛着。

可怜的计算机系统,也参与到大千世界让人无奈的随机命运而来。

但有一种很霸道的任务,对CPU一抢一个准,那就是我们上面提到的硬中断–那些不得不优先处理的事情。

下辈子投胎,就当个硬中断吧(囧)。

快来点赞累加你的幸运值吧 :)。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的GZH。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。

本文转载自: 掘金

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

MySQL常用备份策略详解 —— mysqldump、mys

发表于 2020-08-15

一、备份简介

2.1 备份分类

按照不同的维度,通常将数据库的备份分为以下几类:

物理备份 与 逻辑备份

  • 物理备份:备份的是完整的数据库目录和数据文件。采用该模式会进行大量的 IO 操作,但不含任何逻辑转换,因此备份和恢复速度通常都比较快。
  • 逻辑备份:通过数据库结构和内容信息来进行备份。因为要执行逻辑转换,因此其速度较慢,并且在以文本格式保存时,其输出文件的大小大于物理备份。逻辑备份的还原的粒度可以从服务器级别(所有数据库)精确到具体表,但备份不会包括日志文件、配置文件等与数据库无关的内容。

全量备份 与 增量备份

  • 全量备份:备份服务器在给定时间点上的所有数据。
  • 增量备份:备份在给定时间跨度内(从一个时间点到另一个时间点)对数据所做的更改。

在线备份 与 离线备份

  • 在线备份:数据库服务在运行状态下进行备份。此时其他客户端依旧可以连接到数据库,但为了保证数据的一致性,在备份期间可能会对数据进行加锁,此时客户端的访问依然会受限。
  • 离线备份:在数据库服务停机状态下进行备份。此备份过程简单,但由于无法提供对外服务,通常会对业务造成比较大的影响。

2.2 备份工具

MySQL 支持的备份工具有很多种,这里列出常用的三种:

  • mysqldump:这是 MySQL 自带的备份工具,其采用的备份方式是逻辑备份,支持全库备份、单库备份、单表备份。由于其采用的是逻辑备份,所以生成的备份文件比物理备份的大,且所需恢复时间也比较长。
  • mysqlpump:这是 MySQL 5.7 之后新增的备份工具,在 mysqldump 的基础上进行了功能的扩展,支持多线程备份,支持对备份文件进行压缩,能够提高备份的速度和降低备份文件所需的储存空间。
  • Xtrabackup:这是 Percona 公司开发的实时热备工具,能够在不停机的情况下进行快速可靠的热备份,并且备份期间不会间断数据库事务的处理。它支持数据的全备和增备,并且由于其采用的是物理备份的方式,所以恢复速度比较快。

二、mysqldump

2.1 常用参数

mysqldump 的基本语法如下:

1
2
3
4
5
6
shell复制代码# 备份数据库或数据库中的指定表
mysqldump [options] db_name [tbl_name ...]
# 备份多个指定的数据库
mysqldump [options] --databases db_name ...
# 备份当前数据库实例中的所有表
mysqldump [options] --all-databases

options 代表可选操作,常用的可选参数如下:

  • –host=host_name, -h host_name

指定服务器地址。

  • –user=user_name, -u user_name

指定用户名。

  • –password[=password], -p[password]

指定密码。通常无需在命令行中明文指定,按照提示输入即可。

  • –default-character-set=charset_name

导出文本使用的字符集,默认为 utf8。

  • –events, -E

备份包含数据库中的事件。

  • –ignore-table=db_name.tbl_name

不需要进行备份的表,必须使用数据库和表名来共同指定。也可以作用于视图。

  • –routines, -R

备份包含数据库中的存储过程和自定义函数。

  • –triggers

备份包含数据库中的触发器。

  • –where=’where_condition’, -w ‘where_condition’

在对单表进行导出时候,可以指定过滤条件,例如指定用户名 --where="user='jimf'" 或用户范围 -w"userid>1" 。

  • –lock-all-tables, -x

锁定所有数据库中的所有表,从而保证备份数据的一致性。此选项自动关闭 --single-transaction 和 --lock-tables。

  • –lock-tables, -l

锁定当前数据库中所有表,能够保证当前数据库中表的一致性,但不能保证全局的一致性。

  • –single-transaction

此选项会将事务隔离模式设置为 REPEATABLE READ 并开启一个事务,从而保证备份数据的一致性。主要用于事务表,如 InnoDB 表。 但是此时仍然不能在备份表上执行 ALTER TABLE, CREATE TABLE, DROP TABLE, RENAME TABLE, TRUNCATE TABLE 等操作,因为 REPEATABLE READ 并不能隔离这些操作。

另外需要注意的是 --single-transaction 选项与 --lock-tables 选项是互斥的,因为 LOCK TABLES 会导致任何正在挂起的事务被隐式提交。转储大表时,可以将 --single-transaction 选项与 --quick 选项组合使用 。

  • –quick, -q

主要用于备份大表。它强制 mysqldump 一次只从服务器检索一行数据,避免一次检索所有行而导致缓存溢出。

  • –flush-logs, -F

在开始备份前刷新 MySQL 的日志文件。此选项需要 RELOAD 权限。如果此选项与 --all-databases 配合使用,则会在每个数据库开始备份前都刷新一次日志。如果配合 --lock-all-tables,--master-data 或 --single-transaction 使用,则只会在锁定所有表或者开启事务时刷新一次。

  • –master-data[=value]

可以通过配置此参数来控制生成的备份文件是否包含 CHANGE MASTER 语句,该语句中包含了当前时间点二进制日志的信息。该选项有两个可选值:1 和 2 ,设置为 1 时 CHANGE MASTER 语句正常生成,设置为 2 时以注释的方式生成。--master-data 选项还会自动关闭 --lock-tables 选项,而且如果你没有指定 --single-transaction 选项,那么它还会启用 --lock-all-tables 选项,在这种情况下,会在备份开始时短暂内获取全局读锁。

2.2 全量备份

mysqldump 的全量备份与恢复的操作比较简单,示例如下:

1
2
3
4
5
shell复制代码# 备份雇员库
mysqldump -uroot -p --databases employees > employees_bak.sql

# 恢复雇员库
mysql -uroot -p < employees_bak.sql

单表备份:

1
2
3
4
5
6
shell复制代码# 备份雇员库中的职位表
mysqldump -uroot -p --single-transaction employees titles > titles_bak.sql

# 恢复雇员库中的职位表
mysql> use employees;
mysql> source /root/mysqldata/titles_bak.sql;

2.3 增量备份

mysqldump 本身并不能直接进行增量备份,需要通过分析二进制日志的方式来完成。具体示例如下:

1. 基础全备

1.先执行一次全备作为基础,这里以单表备份为例,需要用到上文提到的 --master-data 参数,语句如下:

1
shell复制代码mysqldump -uroot -p --master-data=2 --flush-logs employees titles > titles_bak.sql

使用 more 命令查看备份文件,此时可以在文件开头看到 CHANGE MASTER 语句,语句中包含了二进制日志的名称和偏移量信息,具体如下:

1
sql复制代码-- CHANGE MASTER TO MASTER_LOG_FILE='mysql-bin.000004', MASTER_LOG_POS=155;

2. 增量恢复

对表内容进行任意修改,然后通过分析二进制日志文件来生成增量备份的脚本文件,示例如下:

1
2
shell复制代码mysqlbinlog --start-position=155 \
--database=employees ${MYSQL_HOME}/data/mysql-bin.000004 > titles_inr_bak_01.sql

需要注意的是,在实际生产环境中,可能在全量备份后与增量备份前的时间间隔里生成了多份二进制文件,此时需要对每一个二进制文件都执行相同的命令:

1
2
3
shell复制代码mysqlbinlog --database=employees  ${MYSQL_HOME}/data/mysql-bin.000005 > titles_inr_bak_02.sql
mysqlbinlog --database=employees ${MYSQL_HOME}/data/mysql-bin.000006 > titles_inr_bak_03.sql
.....

之后将全备脚本 ( titles_bak.sql ),以及所有的增备脚本 ( inr_01.sql,inr_02.sql …. ) 通过 source 命令导入即可,这样就完成了全量 + 增量的恢复。

三、mysqlpump

3.1 功能优势

mysqlpump 在 mysqldump 的基础上进行了扩展增强,其主要的优点如下:

  • 能够并行处理数据库及其中的对象,从而可以加快备份进程;
  • 能够更好地控制数据库及数据库对象(表,存储过程,用户帐户等);
  • 能够直接对备份文件进行压缩;
  • 备份时能够显示进度指标(估计值);
  • 备份用户时生成的是 CREATE USER 与 GRANT 语句,而不是像 mysqldump 一样备份成数据,可以方便用户按需恢复。

3.2 常用参数

mysqlpump 的使用和 mysqldump 基本一致,这里不再进行赘述。以下主要介绍部分新增的可选项,具体如下:

  • –default-parallelism=N

每个并行处理队列的默认线程数。默认值为 2。

  • –parallel-schemas=[N:]db_list

用于并行备份多个数据库:db_list 是一个或多个以逗号分隔的数据库名称列表;N 为使用的线程数,如果没有设置,则使用 --default-parallelism 参数的值。

  • –users

将用户信息备份为 CREATE USER 语句和 GRANT 语句 。如果想要只备份用户信息,则可以使用下面的命令:

1
shell复制代码mysqlpump --exclude-databases=% --users
  • –compress-output=algorithm

默认情况下,mysqlpump 不对备份文件进行压缩。可以使用该选项指定压缩格式,当前支持 LZ4 和 ZLIB 两种格式。需要注意的是压缩后的文件可以占用更少的存储空间,但是却不能直接用于备份恢复,需要先进行解压,具体如下:

1
2
3
4
5
6
7
8
shell复制代码# 采用lz4算法进行压缩
mysqlpump --compress-output=LZ4 > dump.lz4
# 恢复前需要先进行解压
lz4_decompress input_file output_file

# 采用ZLIB算法进行压缩
mysqlpump --compress-output=ZLIB > dump.zlib
zlib_decompress input_file output_file

MySQL 发行版自带了上面两个压缩工具,不需要进行额外安装。以上就是 mysqlpump 新增的部分常用参数,完整参数可以参考官方文档:mysqlpump — A Database Backup Program

四、Xtrabackup

4.1 在线安装

Xtrabackup 可以直接使用 yum 命令进行安装,这里我的 MySQL 为 8.0 ,对应安装的 Xtrabackup 也为 8.0,命令如下:

1
2
3
4
5
shell复制代码# 安装Percona yum 源
yum install https://repo.percona.com/yum/percona-release-latest.noarch.rpm

# 安装
yum install percona-xtrabackup-80

4.2 全量备份

全量备份的具体步骤如下:

1. 创建备份

Xtrabackup 全量备份的基本语句如下,可以使用 target-dir 指明备份文件的存储位置,parallel 则是指明操作的并行度:

1
shell复制代码xtrabackup --backup  --user=root --password --parallel=3  --target-dir=/data/backups/

以上进行的是整个数据库实例的备份,如果需要备份指定数据库,则可以使用 –databases 进行指定。

另外一个容易出现的异常是:Xtrabackup 在进行备份时,默认会去 /var/lib/mysql/mysql.sock 文件里获取数据库的 socket 信息,如果你修改了数据库的 socket 配置,则需要使用 –socket 参数进行重新指定,否则会抛出找不到连接的异常。备份完整后需要立即执行的另外一个操作是 prepare (准备备份)。

2. 准备备份

由于备份是将所有物理库表等文件复制到备份目录,而整个过程需要持续一段时间,此时备份的数据中就可能会包含尚未提交的事务或已经提交但尚未同步至数据文件中的事务,最终导致备份结果处于不一致状态。此时需要进行 prepare 操作来回滚未提交的事务及同步已经提交的事务至数据文件,从而使得整体达到一致性状态。命令如下:

1
shell复制代码xtrabackup --prepare --target-dir=/data/backups/

需要特别注意的在该阶段不要随意中断 xtrabackup 进程,因为这可能会导致数据文件损坏,备份将无法使用。

3. 恢复备份

由于 xtrabackup 执行的是物理备份,所以想要进行恢复,必须先要停止 MySQL 服务。同时这里我们可以删除 MySQL 的数据目录来模拟数据丢失的情况,之后使用以下命令将备份文件拷贝到 MySQL 的数据目录下:

1
2
3
4
5
shell复制代码# 模拟数据异常丢失
rm -rf /usr/app/mysql-8.0.17/data/*

# 将备份文件拷贝到 data 目录下
xtrabackup --copy-back --target-dir=/data/backups/

copy-back 命令只需要指定备份文件的位置,不需要指定 MySQL 数据目录的位置,因为 Xtrabackup 会自动从 /etc/my.cnf 上获取 MySQL 的相关信息,包括数据目录的位置。如果不需要保留备份文件,可以直接使用 --move-back 命令,代表直接将备份文件移动到数据目录下。此时数据目录的所有者通常为执行命令的用户,需要更改为 mysql 用户,命令如下:

1
shell复制代码chown -R mysql:mysql /usr/app/mysql-8.0.17/data

再次启动即可完成备份恢复。

4.3 增量备份

使用 Xtrabackup 进行增量备份时,每一次增量备份都需要以上一次的备份为基础,之后再将增量备份运用到第一次全备之上,从而完成备份。具体操作如下:

1. 创建备份

这里首先创建一个全备作为基础:

1
shell复制代码xtrabackup  --user=root --password --backup  --target-dir=/data/backups/base/

之后修改库中任意数据,然后进行第一次增量备份,此时需要使用 incremental-basedir 指定基础目录为全备目录:

1
2
shell复制代码xtrabackup  --user=root --password --backup  --target-dir=/data/backups/inc1 \
--incremental-basedir=/data/backups/base

再修改库中任意数据,然后进行第二次增量备份,此时需要使用 incremental-basedir 指定基础目录为上一次增备目录:

1
2
shell复制代码xtrabackup  --user=root --password --backup  --target-dir=/data/backups/inc2 \
--incremental-basedir=/data/backups/inc1

2. 准备备份

准备基础备份:

1
shell复制代码xtrabackup --prepare --apply-log-only --target-dir=/data/backups/base

将第一次备份作用于全备数据:

1
2
shell复制代码xtrabackup --prepare --apply-log-only --target-dir=/data/backups/base \
--incremental-dir=/data/backups/inc1

将第二次备份作用于全备数据:

1
2
shell复制代码xtrabackup --prepare --target-dir=/data/backups/base \
--incremental-dir=/data/backups/inc2

在准备备份时候,除了最后一次增备外,其余的准备命令都需要加上 --apply-log-only 选项来阻止事务的回滚,因为备份时未提交的事务可能正在进行,并可能在下一次增量备份中提交,如果不进行阻止,那么增量备份将没有任何意义。

3. 恢复备份

恢复备份和全量备份时相同,只需要最终准备好的全备数据复制到 MySQL 的数据目录下即可:

1
2
3
shell复制代码xtrabackup --copy-back --target-dir=/data/backups/base
# 必须修改文件权限,否则无法启动
chown -R mysql:mysql /usr/app/mysql-8.0.17/data

此时增量备份就已经完成。需要说明的是:按照上面的情况,如果第二次备份之后发生了宕机,那么第二次备份后到宕机前的数据依然没法通过 Xtrabackup 进行恢复,此时就只能采用上面介绍的分析二进制日志的恢复方法。由此可以看出,无论是采用何种备份方式,二进制日志都是非常重要的,因此最好对其进行实时备份。

五、二进制日志的备份

想要备份二进制日志文件,可以通过定时执行 cp 或 scp 等命令来实现,也可以通过 mysqlbinlog 自带的功能来实现远程备份,将远程服务器上的二进制日志文件复制到本机,命令如下:

1
2
3
shell复制代码mysqlbinlog --read-from-remote-server --raw --stop-never \
--host=主机名 --port=3306 \
--user=用户名 --password=密码 初始复制时的日志文件名

需要注意的是这里的用户必须具有 replication slave 权限,因为上述命令本质上是模拟主从复制架构下,从节点通过 IO 线程不断去获取主节点的二进制日志,从而达到备份的目的。

参考资料

  • Chapter 7 Backup and Recovery
  • mysqldump — A Database Backup Program
  • mysqlpump — A Database Backup Program
  • Percona XtraBackup - Documentation

更多文章,欢迎访问 [全栈工程师手册] ,GitHub 地址:github.com/heibaiying/…

本文转载自: 掘金

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

Spark 系列(十六)—— Spark Streaming

发表于 2020-08-15

一、版本说明

Spark 针对 Kafka 的不同版本,提供了两套整合方案:spark-streaming-kafka-0-8 和 spark-streaming-kafka-0-10,其主要区别如下:

spark-streaming-kafka-0-8 spark-streaming-kafka-0-10
Kafka 版本 0.8.2.1 or higher 0.10.0 or higher
AP 状态 Deprecated从 Spark 2.3.0 版本开始,Kafka 0.8 支持已被弃用 Stable(稳定版)
语言支持 Scala, Java, Python Scala, Java
Receiver DStream Yes No
Direct DStream Yes Yes
SSL / TLS Support No Yes
Offset Commit API(偏移量提交) No Yes
Dynamic Topic Subscription(动态主题订阅) No Yes

本文使用的 Kafka 版本为 kafka_2.12-2.2.0,故采用第二种方式进行整合。

二、项目依赖

项目采用 Maven 进行构建,主要依赖如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xml复制代码<properties>
<scala.version>2.12</scala.version>
</properties>

<dependencies>
<!-- Spark Streaming-->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming_${scala.version}</artifactId>
<version>${spark.version}</version>
</dependency>
<!-- Spark Streaming 整合 Kafka 依赖-->
<dependency>
<groupId>org.apache.spark</groupId>
<artifactId>spark-streaming-kafka-0-10_${scala.version}</artifactId>
<version>2.4.3</version>
</dependency>
</dependencies>

完整源码见本仓库:spark-streaming-kafka

三、整合Kafka

通过调用 KafkaUtils 对象的 createDirectStream 方法来创建输入流,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
java复制代码import org.apache.kafka.common.serialization.StringDeserializer
import org.apache.spark.SparkConf
import org.apache.spark.streaming.kafka010.ConsumerStrategies.Subscribe
import org.apache.spark.streaming.kafka010.LocationStrategies.PreferConsistent
import org.apache.spark.streaming.kafka010._
import org.apache.spark.streaming.{Seconds, StreamingContext}

/**
* spark streaming 整合 kafka
*/
object KafkaDirectStream {

def main(args: Array[String]): Unit = {

val sparkConf = new SparkConf().setAppName("KafkaDirectStream").setMaster("local[2]")
val streamingContext = new StreamingContext(sparkConf, Seconds(5))

val kafkaParams = Map[String, Object](
/*
* 指定 broker 的地址清单,清单里不需要包含所有的 broker 地址,生产者会从给定的 broker 里查找其他 broker 的信息。
* 不过建议至少提供两个 broker 的信息作为容错。
*/
"bootstrap.servers" -> "hadoop001:9092",
/*键的序列化器*/
"key.deserializer" -> classOf[StringDeserializer],
/*值的序列化器*/
"value.deserializer" -> classOf[StringDeserializer],
/*消费者所在分组的 ID*/
"group.id" -> "spark-streaming-group",
/*
* 该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:
* latest: 在偏移量无效的情况下,消费者将从最新的记录开始读取数据(在消费者启动之后生成的记录)
* earliest: 在偏移量无效的情况下,消费者将从起始位置读取分区的记录
*/
"auto.offset.reset" -> "latest",
/*是否自动提交*/
"enable.auto.commit" -> (true: java.lang.Boolean)
)

/*可以同时订阅多个主题*/
val topics = Array("spark-streaming-topic")
val stream = KafkaUtils.createDirectStream[String, String](
streamingContext,
/*位置策略*/
PreferConsistent,
/*订阅主题*/
Subscribe[String, String](topics, kafkaParams)
)

/*打印输入流*/
stream.map(record => (record.key, record.value)).print()

streamingContext.start()
streamingContext.awaitTermination()
}
}

3.1 ConsumerRecord

这里获得的输入流中每一个 Record 实际上是 ConsumerRecord<K, V> 的实例,其包含了 Record 的所有可用信息,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码public class ConsumerRecord<K, V> {

public static final long NO_TIMESTAMP = RecordBatch.NO_TIMESTAMP;
public static final int NULL_SIZE = -1;
public static final int NULL_CHECKSUM = -1;

/*主题名称*/
private final String topic;
/*分区编号*/
private final int partition;
/*偏移量*/
private final long offset;
/*时间戳*/
private final long timestamp;
/*时间戳代表的含义*/
private final TimestampType timestampType;
/*键序列化器*/
private final int serializedKeySize;
/*值序列化器*/
private final int serializedValueSize;
/*值序列化器*/
private final Headers headers;
/*键*/
private final K key;
/*值*/
private final V value;
.....
}

3.2 生产者属性

在示例代码中 kafkaParams 封装了 Kafka 消费者的属性,这些属性和 Spark Streaming 无关,是 Kafka 原生 API 中就有定义的。其中服务器地址、键序列化器和值序列化器是必选的,其他配置是可选的。其余可选的配置项如下:

1. fetch.min.byte

消费者从服务器获取记录的最小字节数。如果可用的数据量小于设置值,broker 会等待有足够的可用数据时才会把它返回给消费者。

2. fetch.max.wait.ms

broker 返回给消费者数据的等待时间。

3. max.partition.fetch.bytes

分区返回给消费者的最大字节数。

4. session.timeout.ms

消费者在被认为死亡之前可以与服务器断开连接的时间。

5. auto.offset.reset

该属性指定了消费者在读取一个没有偏移量的分区或者偏移量无效的情况下该作何处理:

  • latest(默认值) :在偏移量无效的情况下,消费者将从其启动之后生成的最新的记录开始读取数据;
  • earliest :在偏移量无效的情况下,消费者将从起始位置读取分区的记录。

6. enable.auto.commit

是否自动提交偏移量,默认值是 true,为了避免出现重复数据和数据丢失,可以把它设置为 false。

7. client.id

客户端 id,服务器用来识别消息的来源。

8. max.poll.records

单次调用 poll() 方法能够返回的记录数量。

9. receive.buffer.bytes 和 send.buffer.byte

这两个参数分别指定 TCP socket 接收和发送数据包缓冲区的大小,-1 代表使用操作系统的默认值。

3.3 位置策略

Spark Streaming 中提供了如下三种位置策略,用于指定 Kafka 主题分区与 Spark 执行程序 Executors 之间的分配关系:

  • PreferConsistent : 它将在所有的 Executors 上均匀分配分区;
  • PreferBrokers : 当 Spark 的 Executor 与 Kafka Broker 在同一机器上时可以选择该选项,它优先将该 Broker 上的首领分区分配给该机器上的 Executor;
  • PreferFixed : 可以指定主题分区与特定主机的映射关系,显示地将分区分配到特定的主机,其构造器如下:
1
2
3
4
5
6
7
java复制代码@Experimental
def PreferFixed(hostMap: collection.Map[TopicPartition, String]): LocationStrategy =
new PreferFixed(new ju.HashMap[TopicPartition, String](hostMap.asJava))

@Experimental
def PreferFixed(hostMap: ju.Map[TopicPartition, String]): LocationStrategy =
new PreferFixed(hostMap)

3.4 订阅方式

Spark Streaming 提供了两种主题订阅方式,分别为 Subscribe 和 SubscribePattern。后者可以使用正则匹配订阅主题的名称。其构造器分别如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码/**
* @param 需要订阅的主题的集合
* @param Kafka 消费者参数
* @param offsets(可选): 在初始启动时开始的偏移量。如果没有,则将使用保存的偏移量或 auto.offset.reset 属性的值
*/
def Subscribe[K, V](
topics: ju.Collection[jl.String],
kafkaParams: ju.Map[String, Object],
offsets: ju.Map[TopicPartition, jl.Long]): ConsumerStrategy[K, V] = { ... }

/**
* @param 需要订阅的正则
* @param Kafka 消费者参数
* @param offsets(可选): 在初始启动时开始的偏移量。如果没有,则将使用保存的偏移量或 auto.offset.reset 属性的值
*/
def SubscribePattern[K, V](
pattern: ju.regex.Pattern,
kafkaParams: collection.Map[String, Object],
offsets: collection.Map[TopicPartition, Long]): ConsumerStrategy[K, V] = { ... }

在示例代码中,我们实际上并没有指定第三个参数 offsets,所以程序默认采用的是配置的 auto.offset.reset 属性的值 latest,即在偏移量无效的情况下,消费者将从其启动之后生成的最新的记录开始读取数据。

3.5 提交偏移量

在示例代码中,我们将 enable.auto.commit 设置为 true,代表自动提交。在某些情况下,你可能需要更高的可靠性,如在业务完全处理完成后再提交偏移量,这时候可以使用手动提交。想要进行手动提交,需要调用 Kafka 原生的 API :

  • commitSync: 用于异步提交;
  • commitAsync:用于同步提交。

具体提交方式可以参见:Kafka 消费者详解 消费者详解.md)

四、启动测试

4.1 创建主题

1. 启动Kakfa

Kafka 的运行依赖于 zookeeper,需要预先启动,可以启动 Kafka 内置的 zookeeper,也可以启动自己安装的:

1
2
3
4
5
shell复制代码# zookeeper启动命令
bin/zkServer.sh start

# 内置zookeeper启动命令
bin/zookeeper-server-start.sh config/zookeeper.properties

启动单节点 kafka 用于测试:

1
shell复制代码# bin/kafka-server-start.sh config/server.properties

2. 创建topic

1
2
3
4
5
6
7
8
9
shell复制代码# 创建用于测试主题
bin/kafka-topics.sh --create \
--bootstrap-server hadoop001:9092 \
--replication-factor 1 \
--partitions 1 \
--topic spark-streaming-topic

# 查看所有主题
bin/kafka-topics.sh --list --bootstrap-server hadoop001:9092

3. 创建生产者

这里创建一个 Kafka 生产者,用于发送测试数据:

1
shell复制代码bin/kafka-console-producer.sh --broker-list hadoop001:9092 --topic spark-streaming-topic

4.2 本地模式测试

这里我直接使用本地模式启动 Spark Streaming 程序。启动后使用生产者发送数据,从控制台查看结果。

从控制台输出中可以看到数据流已经被成功接收,由于采用 kafka-console-producer.sh 发送的数据默认是没有 key 的,所以 key 值为 null。同时从输出中也可以看到在程序中指定的 groupId 和程序自动分配的 clientId。

https://github.com/heibaiying

参考资料

  1. spark.apache.org/docs/latest…

https://github.com/heibaiying

本文转载自: 掘金

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

面试官:知道时间轮算法吗?在Netty和Kafka中如何应用

发表于 2020-08-15

大家好,我是yes。

最近看 Kafka 看到了时间轮算法,记得以前看 Netty 也看到过这玩意,没太过关注。今天就来看看时间轮到底是什么东西。

为什么要用时间轮算法来实现延迟操作?

延时操作 Java 不是提供了 Timer 么?

还有 DelayQueue 配合线程池或者 ScheduledThreadPool 不香吗?

我们先来简单看看 Timer、DelayQueue 和 ScheduledThreadPool 的相关实现,看看它们是如何实现延时任务的,源码之下无秘密。再来剖析下为何 Netty 和 Kafka 特意实现了时间轮来处理延迟任务。

如果在手机上阅读其实纯看字也行,不用看代码,我都会先用文字描述清楚。不过电脑上看效果更佳。

Timer

Timer 可以实现延时任务,也可以实现周期性任务。我们先来看看 Timer 核心属性和构造器。

核心就是一个优先队列和封装的执行任务的线程,从这我们也可以看到一个 Timer 只有一个线程执行任务。

再来看看如何实现延时和周期性任务的。我先简单的概括一下,首先维持一个小顶堆,即最快需要执行的任务排在优先队列的第一个,根据堆的特性我们知道插入和删除的时间复杂度都是 O(logn)。

然后 TimerThread 不断地拿排着的第一个任务的执行时间和当前时间做对比。如果时间到了先看看这个任务是不是周期性执行的任务,如果是则修改当前任务时间为下次执行的时间,如果不是周期性任务则将任务从优先队列中移除。最后执行任务。如果时间还未到则调用 wait() 等待。

再看下图,整理下流程。

流程知道了再对着看下代码,这块就差不多了。看代码不爽的可以跳过代码部分,影响不大。

先来看下 TaskQueue,就简单看下插入任务的过程,就是个普通的堆插入操作。

再来看看 TimerThread 的 run 操作。

小结一下

可以看出 Timer 实际就是根据任务的执行时间维护了一个优先队列,并且起了一个线程不断地拉取任务执行。

有什么弊端呢?

首先优先队列的插入和删除的时间复杂度是O(logn),当数据量大的时候,频繁的入堆出堆性能有待考虑。

并且是单线程执行,那么如果一个任务执行的时间过久则会影响下一个任务的执行时间(当然你任务的run要是异步执行也行)。

并且从代码可以看到对异常没有做什么处理,那么一个任务出错的时候会导致之后的任务都无法执行。

ScheduledThreadPoolExecutor

在说 ScheduledThreadPoolExecutor 之前我们再看下 Timer 的注释,注释可都是干货千万不要错过。我做了点修改,突出了下重点。

Java 5.0 introduced ScheduledThreadPoolExecutor, It is effectively a more versatile replacement for the Timer, it allows multiple service threads. Configuring with one thread makes it equivalent to Timer。

简单翻译下:1.5 引入了 ScheduledThreadPoolExecutor,它是一个具有更多功能的 Timer 的替代品,允许多个服务线程。如果设置一个服务线程和 Timer 没啥差别。

从注释看出相对于 Timer ,可能就是单线程跑任务和多线程跑任务的区别。我们来看下。

继承了 ThreadPoolExecutor,实现了 ScheduledExecutorService。可以定性操作就是正常线程池差不多了。区别就在于两点,一个是 ScheduledFutureTask ,一个是 DelayedWorkQueue。

其实 DelayedWorkQueue 就是优先队列,也是利用数组实现的小顶堆。而 ScheduledFutureTask 继承自 FutureTask 重写了 run 方法,实现了周期性任务的需求。

小结一下

ScheduledThreadPoolExecutor 大致的流程和 Timer 差不多,也是维护一个优先队列,然后通过重写 task 的 run 方法来实现周期性任务,主要差别在于能多线程运行任务,不会单线程阻塞。

并且 Java 线程池的设定是 task 出错会把错误吃了,无声无息的。因此一个任务出错也不会影响之后的任务。

DelayQueue

Java 中还有个延迟队列 DelayQueue,加入延迟队列的元素都必须实现 Delayed 接口。延迟队列内部是利用 PriorityQueue 实现的,所以还是利用优先队列!Delayed 接口继承了Comparable 因此优先队列是通过 delay 来排序的。

然后我们再来看下延迟队列是如何获取元素的。

小结一下

也是利用优先队列实现的,元素通过实现 Delayed 接口来返回延迟的时间。不过延迟队列就是个容器,需要其他线程来获取和执行任务。

这下是搞明白了 Timer 、ScheduledThreadPool 和 DelayQueue,总结的说下它们都是通过优先队列来获取最早需要执行的任务,因此插入和删除任务的时间复杂度都为O(logn),并且 Timer 、ScheduledThreadPool 的周期性任务是通过重置任务的下一次执行时间来完成的。

问题就出在时间复杂度上,插入删除时间复杂度是O(logn),那么假设频繁插入删除次数为 m,总的时间复杂度就是O(mlogn),这种时间复杂度满足不了 Kafka 这类中间件对性能的要求,而时间轮算法的插入删除时间复杂度是O(1)。我们来看看时间轮算法是如何实现的。

时间轮算法

俗话说艺术源于生活,技术也能从日常生活中找到灵感。咱们先来看块表,嗯金色的表。

都看清楚了吧,时间轮就是和手表时钟很相似的存在。时间轮用环形数组实现,数组的每个元素可以称为槽,和 HashMap一样称呼。

槽的内部用双向链表存着待执行的任务,添加和删除的链表操作时间复杂度都是 O(1),槽位本身也指代时间精度,比如一秒扫一个槽,那么这个时间轮的最高精度就是 1 秒。

也就是说延迟 1.2 秒的任务和 1.5 秒的任务会被加入到同一个槽中,然后在 1 秒的时候遍历这个槽中的链表执行任务。

从图中可以看到此时指针指向的是第一个槽,一共有八个槽0~7,假设槽的时间单位为 1 秒,现在要加入一个延时 5 秒的任务,计算方式就是 5 % 8 + 1 = 6,即放在槽位为 6,下标为 5 的那个槽中。更具体的就是拼到槽的双向链表的尾部。

然后每秒指针顺时针移动一格,这样就扫到了下一格,遍历这格中的双向链表执行任务。然后再循环继续。

可以看到插入任务从计算槽位到插入链表,时间复杂度都是O(1)。那假设现在要加入一个50秒后执行的任务怎么办?这槽好像不够啊?难道要加槽嘛?和HashMap一样扩容?

不是的,常见有两种方式,一种是通过增加轮次的概念。50 % 8 + 1 = 3,即应该放在槽位是 3,下标是 2 的位置。然后 (50 - 1) / 8 = 6,即轮数记为 6。也就是说当循环 6 轮之后扫到下标的 2 的这个槽位会触发这个任务。Netty 中的 HashedWheelTimer 使用的就是这种方式。

还有一种是通过多层次的时间轮,这个和我们的手表就更像了,像我们秒针走一圈,分针走一格,分针走一圈,时针走一格。

多层次时间轮就是这样实现的。假设上图就是第一层,那么第一层走了一圈,第二层就走一格,可以得知第二层的一格就是8秒,假设第二层也是 8 个槽,那么第二层走一圈,第三层走一格,可以得知第三层一格就是 64 秒。那么一格三层,每层8个槽,一共 24 个槽时间轮就可以处理最多延迟 512 秒的任务。

而多层次时间轮还会有降级的操作,假设一个任务延迟 500 秒执行,那么刚开始加进来肯定是放在第三层的,当时间过了 436 秒后,此时还需要 64 秒就会触发任务的执行,而此时相对而言它就是个延迟 64 秒后的任务,因此它会被降低放在第二层中,第一层还放不下它。

再过个 56 秒,相对而言它就是个延迟 8 秒后执行的任务,因此它会再被降级放在第一层中,等待执行。

降级是为了保证时间精度一致性。Kafka内部用的就是多层次的时间轮算法。

Netty中的时间轮

在 Netty 中时间轮的实现类是 HashedWheelTimer,代码中的 wheel 就是上图画的循环数组,mask 的设计和HashMap一样,通过限制数组的大小为2的次方,利用位运算来替代取模运算,提高性能。tickDuration 就是每格的时间即精度。可以看到配备了一个工作线程来处理任务的执行。

接下来我们再来看看任务是如何添加的。

可以看到任务并没有直接添加到时间轮中,而是先入了一个 mpsc 队列,我简单说下 mpsc 是 JCTools 中的并发队列,用在多个生产者可同时访问队列,但只有一个消费者会访问队列的情况。篇幅有限,有兴趣的朋友自行了解实现。

然后我们再来看看工作线程是如何运作的。

很直观没什么花头,我们先来看看 waitForNextTick,是如何得到下一次执行时间的。

简单的说就是通过 tickDuration 和此时已经滴答的次数算出下一次需要检查的时间,时候未到就sleep等着。

再来看下任务如何入槽的。

注释的很清楚了,实现也和上述分析的一致。

最后再来看下如何执行的。

就是通过轮数和时间双重判断,执行完了移除任务。

小结一下

总体上看 Netty 的实现就是上文说的时间轮通过轮数的实现,完全一致。可以看出时间精度由 TickDuration 把控,并且工作线程的除了处理执行到时的任务还做了其他操作,因此任务不一定会被精准的执行。

而且任务的执行如果不是新起一个线程,或者将任务扔到线程池执行,那么耗时的任务会阻塞下个任务的执行。

并且会有很多无用的 tick 推进,例如 TickDuration 为1秒,此时就一个延迟350秒的任务,那就是有349次无用的操作。

但是从另一面来看,如果任务都执行很快(当然你也可以异步执行),并且任务数很多,通过分批执行,并且增删任务的时间复杂度都是O(1)来说。时间轮还是比通过优先队列实现的延时任务来的合适些。

Kafka 中的时间轮

上面我们说到 Kafka 中的时间轮是多层次时间轮实现,总的而言实现和上述说的思路一致。不过细节有些不同,并且做了点优化。

先看看添加任务的方法。在添加的时候就设置任务执行的绝对时间。

那么时间轮是如何推动的呢?Netty 中是通过固定的时间间隔扫描,时候未到就等待来进行时间轮的推动。上面我们分析到这样会有空推进的情况。

而 Kafka 就利用了空间换时间的思想,通过 DelayQueue,来保存每个槽,通过每个槽的过期时间排序。这样拥有最早需要执行任务的槽会有优先获取。如果时候未到,那么 delayQueue.poll 就会阻塞着,这样就不会有空推进的情况发送。

我们来看下推进的方法。

从上面的 add 方法我们知道每次对比都是根据expiration < currentTime + interval 来进行对比的,而advanceClock 就是用来推进更新 currentTime 的。

小结一下

Kafka 用了多层次时间轮来实现,并且是按需创建时间轮,采用任务的绝对时间来判断延期,并且对于每个槽(槽内存放的也是任务的双向链表)都会维护一个过期时间,利用 DelayQueue 来对每个槽的过期时间排序,来进行时间的推进,防止空推进的存在。

每次推进都会更新 currentTime 为当前时间戳,当然做了点微调使得 currentTime 是 tickMs 的整数倍。并且每次推进都会把能降级的任务重新插入降级。

可以看到这里的 DelayQueue 的元素是每个槽,而不是任务,因此数量就少很多了,这应该是权衡了对于槽操作的延时队列的时间复杂度与空推进的影响。

总结

首先介绍了 Timer、DelayQueue 和 ScheduledThreadPool,它们都是基于优先队列实现的,O(logn) 的时间复杂度在任务数多的情况下频繁的入队出队对性能来说有损耗。因此适合于任务数不多的情况。

Timer 是单线程的会有阻塞的风险,并且对异常没有做处理,一个任务出错 Timer 就挂了。而 ScheduledThreadPool 相比于 Timer 首先可以多线程来执行任务,并且线程池对异常做了处理,使得任务之间不会有影响。

并且 Timer 和 ScheduledThreadPool 可以周期性执行任务。 而 DelayQueue 就是个具有优先级的阻塞队列。

对比而言时间轮更适合任务数很大的延时场景,它的任务插入和删除时间复杂度都为O(1)。对于延迟超过时间轮所能表示的范围有两种处理方式,一是通过增加一个字段-轮数,Netty 就是这样实现的。二是多层次时间轮,Kakfa 是这样实现的。

相比而言 Netty 的实现会有空推进的问题,而 Kafka 采用 DelayQueue 以槽为单位,利用空间换时间的思想解决了空推进的问题。

可以看出延迟任务的实现都不是很精确的,并且或多或少都会有阻塞的情况,即使你异步执行,线程不够的情况下还是会阻塞。

巨人的肩膀

《深入理解Kafka:核心设计与实践原理》

www.cnblogs.com/luozhiyun/p…


我是 yes,从一点点到亿点点,我们下篇见。

往期推荐:

Kafka和RocketMQ底层存储之那些你不知道的事

消息队列面试热点一锅端

图解+代码|常见限流算法以及限流在单机分布式场景下的思考

表弟面试被虐我教他缓存连招

面试官:说说Kafka处理请求的全流程

Kafka索引设计的亮点

Kafka日志段读写分析

本文转载自: 掘金

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

逐行解读Spring(五)- 没人比我更懂循环依赖!

发表于 2020-08-14

创作不易,转载请篇首注明 作者:掘金@小希子 + 来源链接~

如果想了解更多Spring源码知识,点击前往其余逐行解析Spring系列

一、前言

这一篇博文主要讲一下我们spring是怎么解决循环依赖的问题的。

二、什么是循环依赖

首先我们需要明确,什么是循环依赖呢?这里举一个简单的例子:

1
2
3
4
5
6
7
8
9
10
java复制代码@Service
public class A {
@Autowired
private B b;
}
@Service
public class B {
@Autowired
private A a;
}

以这个例子来看,我们声明了a、b两个bean,且a中需要注入一个b,b中需要注入一个a。

结合我们上篇博文的bean生命周期的知识,我们来模拟一下这两个bean创建的流程:

循环依赖问题

如果没有缓存的设计,我们的虚线所示的分支将永远无法到达,导致出现无法解决的循环依赖问题….

三、三级缓存设计

1. 自己解决循环依赖问题

现在,假如我们是spring的架构师,我们应该怎么解决这个循环依赖问题呢?

1.1. 流程设计

首先如果要解决这个问题,我们的目标应该是要把之前的级联的无限创建流程切到,也就是说我们的流程要变为如下所示:

循环依赖如何解决

也就是说,我们需要在B实例创建后,注入A的时候,能够拿到A的实例,这样才能打破无限创建实例的情况。

而B实例的初始化流程,是在A实例创建之后,在populateBean方法中进行依赖注入时触发的。那么如果我们B实例化过程中,想要拿到A的实例,那么A实例必须在createBeanInstance创建实例后(实例都没有就啥也别说了)、populateBean方法调用之前,就暴露出去,让B能通过getBean获取到!(同学们认真想一下这个流程,在现有的流程下改造,是不是只能够这样操作?自己先想清楚这个流程,再去结合spring源码验证,这一块的知识点你以后想忘都忘不掉)

那么结合我们的思路,我们再修改一下流程图:

使用缓存解决循环依赖

1.2. 伪代码实现

流程已经设计好了,那么我们其实也可以出一下这个流程的伪代码(伪代码就不写加锁那些流程了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码// 正真已经初始化完成的map
private Map<String, Object> singleMap = new ConcurrentHashMap<>(16);
// 缓存的map
private Map<String, Object> cacheMap = new ConcurrentHashMap<>(16);

protected Object getBean(final String beanName) {
// 先看一下目标bean是否完全初始化完了,完全初始化完直接返回
Object single = singleMap.get(beanName);
if (single != null) {
return single;
}
// 再看一下目标bean实例是否已经创建,已经创建直接返回
single = cacheMap.get(beanName);
if (single != null) {
return single;
}
// 创建实例
Object beanInstance = createBeanInstance(beanName);
// 实例创建之后,放入缓存
// 因为已经创建实例了,这个时候这个实例的引用暴露出去已经没问题了
// 之后的属性注入等逻辑还是在这个实例上做的
cacheMap.put(beanName, beanInstance);
// 依赖注入,会触发依赖的bean的getBean方法
populateBean(beanName, beanInstance);
// 初始化方法调用
initializeBean(beanName, beanInstance);
// 从缓存移除,放入实例map
singleMap.put(beanName, beanInstance);
cacheMap.remove(beanName)

return beanInstance;
}

可以看到,如果我们自己实现一个缓存结构来解决循环依赖的问题的话,可能只需要两层结构就可以了,但是spring却使用了3级缓存,它有哪些不一样的考量呢?

2. Spring源码

我们已经知道该怎么解决循环依赖问题了,那么现在我们就一起看一下spring源码,看一下我们的分析是否正确。

由于之前我们已经详细讲过整个bean的生命周期了,所以这里就只挑三级缓存相关的代码段来讲了,会跳过比较多的代码,同学们如果有点懵,可以温习一下万字长文讲透bean的生命周期。

2.1. Spring的三级缓存设计

2.1.1. 三级缓存源码

首先,在我们的AbstractBeanFactory#doGetBean的逻辑中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码// 初始化是通过getBean触发bean创建的,依赖注入最终也会使用getBean获取依赖的bean的实例
public Object getBean(String name) throws BeansException {
return doGetBean(name, null, null, false);
}
protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {

final String beanName = transformedBeanName(name);
Object bean;

// 获取bean实例
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// beanFactory相关,之后再讲
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
else {
// 跳过一些代码
// 创建bean的逻辑
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);
}
// 跳过一些代码
}
// 跳过一些代码
// 返回bean实例
return (T) bean;
}

可以看到,如果我们使用getSingleton(beanName)直接获取到bean实例了,是会直接把bean实例返回的,我们一起看一下这个方法(这个方法属于DefaultSingletonBeanRegistry):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码// 一级缓存,缓存正常的bean实例
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存,缓存还未进行依赖注入和初始化方法调用的bean实例
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三级缓存,缓存bean实例的ObjectFactory
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

public Object getSingleton(String beanName) {
return getSingleton(beanName, true);
}

protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 先尝试中一级缓存获取
Object singletonObject = this.singletonObjects.get(beanName);
// 获取不到,并且当前需要获取的bean正在创建中
// 第一次容器初始化触发getBean(A)的时候,这个isSingletonCurrentlyInCreation判断一定为false
// 这个时候就会去走创建bean的流程,创建bean之前会先把这个bean标记为正在创建
// 然后A实例化之后,依赖注入B,触发B的实例化,B再注入A的时候,会再次触发getBean(A)
// 此时isSingletonCurrentlyInCreation就会返回true了

// 当前需要获取的bean正在创建中时,代表出现了循环依赖(或者一前一后并发获取这个bean)
// 这个时候才需要去看二、三级缓存
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
// 加锁了
synchronized (this.singletonObjects) {
// 从二级缓存获取
singletonObject = this.earlySingletonObjects.get(beanName);
if (singletonObject == null && allowEarlyReference) {
// 二级缓存也没有,并且允许获取早期引用的话 - allowEarlyReference传进来是true
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);
// 从三级缓存获取ObjectFactory
if (singletonFactory != null) {
// 通过ObjectFactory获取bean实例
singletonObject = singletonFactory.getObject();
// 放入二级缓存
this.earlySingletonObjects.put(beanName, singletonObject);
// 从三级缓存删除
// 也就是说对于一个单例bean,ObjectFactory#getObject只会调用到一次
// 获取到早期bean实例之后,就把这个bean实例从三级缓存升级到二级缓存了
this.singletonFactories.remove(beanName);
}
}
}
}
// 不管从哪里获取到的bean实例,都会返回
return singletonObject;
}

一二级缓存都好理解,其实就可以理解为我们伪代码里面的那两个Map,但是这个三级缓存是怎么回事?ObjectFactory又是个什么东西?我们就先看一下这个ObjectFactory的结构:

1
2
3
4
5
java复制代码@FunctionalInterface
public interface ObjectFactory<T> {
// 好吧,就是简简单单的一个获取实例的函数接口而已
T getObject() throws BeansException;
}

我们回到这个三级缓存的结构,二级缓存是是在getSingleton方法中put进去的,这跟我们之前分析的,创建bean实例之后放入,好像不太一样?那我们是不是可以推断一下,其实创建bean实例之后,是放入三级缓存的呢(总之实例创建之后是需要放入缓存的)?我们来跟一下bean实例化的代码,主要看一下上一篇时刻意忽略掉的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
java复制代码// 代码做了很多删减,只把主要的逻辑放出来的
protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {

// 创建bean实例
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);
final Object bean = instanceWrapper.getWrappedInstance();
// beanPostProcessor埋点调用
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);

// 重点是这里了,如果是单例bean&&允许循环依赖&&当前bean正在创建
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 加入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

Object exposedObject = bean;
try {
// 依赖注入
populateBean(beanName, mbd, instanceWrapper);
// 初始化方法调用
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(...);
}

if (earlySingletonExposure) {
// 第二个参数传false是不会从三级缓存中取值的
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
// 如果发现二级缓存中有值了 - 说明出现了循环依赖
if (exposedObject == bean) {
// 并且initializeBean没有改变bean的引用
// 则把二级缓存中的bean实例返回出去
exposedObject = earlySingletonReference;
}
}
}

try {
// 注册销毁逻辑
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(...);
}

return exposedObject;
}

可以看到,初始化一个bean是,创建bean实例之后,如果这个bean是单例bean&&允许循环依赖&&当前bean正在创建,那么将会调用addSingletonFactory加入三级缓存:

1
2
3
4
5
6
7
8
9
10
java复制代码protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
if (!this.singletonObjects.containsKey(beanName)) {
// 加入三级缓存
this.singletonFactories.put(beanName, singletonFactory);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}
}

也就是说我们伪代码中的这一段有了:

1
2
3
4
5
6
java复制代码// 创建实例
Object beanInstance = createBeanInstance(beanName);
// 实例创建之后,放入缓存
// 因为已经创建实例了,这个时候这个实例的引用暴露出去已经没问题了
// 之后的属性注入等逻辑还是在这个实例上做的
cacheMap.put(beanName, beanInstance);

那么接下来,完全实例化完成的bean又是什么时候塞入我们的实例Map(一级缓存)singletonObjects的呢?

这个时候我们就要回到调用createBean方法的这一块的逻辑了:

1
2
3
4
5
6
7
8
9
10
11
12
13
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创建逻辑是通过一个lamdba语法传入getSingleton方法了,我们进入这个方法看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 一级缓存拿不到
// 注意一下这个方法,这里会标记这个bean正在创建
beforeSingletonCreation(beanName);
boolean newSingleton = false;
try {
// 调用外部传入的lamdba,即createBean逻辑
// 获取到完全实例化好的bean
// 需要注意的是,这个时候这个bean的实例已经在二级缓存或者三级缓存中了
// 三级缓存:bean实例创建后放入的,如果没有循环依赖/并发获取这个bean,那会一直在三级缓存中
// 二级缓存:如果出现循环依赖,第二次进入getBean->getSingleton的时候,会从三级缓存升级到二级缓存
singletonObject = singletonFactory.getObject();
// 标记一下
newSingleton = true;
}
catch (IllegalStateException ex) {
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
throw ex;
}
finally {
// 这里是从正在创建的列表移除,到这里这个bean要么已经完全初始化完成了
// 要么就是初始化失败,都需要移除的
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 如果是新初始化了一个单例bean,加入一级缓存
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}

哈哈,加入实例Map(一级缓存)singletonObjects的逻辑明显就是在这个addSingleton中了:

1
2
3
4
5
6
7
8
9
10
11
java复制代码protected void addSingleton(String beanName, Object singletonObject) {
synchronized (this.singletonObjects) {
// 这个逻辑应该一点也不意外吧
// 放入一级缓存,从二、三级缓存删除,这里就用判断当前bean具体是在哪个缓存了
// 反正都要删的
this.singletonObjects.put(beanName, singletonObject);
this.singletonFactories.remove(beanName);
this.earlySingletonObjects.remove(beanName);
this.registeredSingletons.add(beanName);
}
}

也就是说,我们伪代码的这一块在spring里面也有对应的体现,完美:

1
2
3
4
5
java复制代码// 初始化方法调用
initializeBean(beanName, beanInstance);
// 从缓存移除,放入实例map
singleMap.put(beanName, beanInstance);
cacheMap.remove(beanName)

就这样,spring通过缓存设计解决了循环依赖的问题。

2.1.2. 三级缓存解决循环依赖流程图

什么,看完代码之后还是有点模糊?那么把我们的流程图再改一下,按照spring的流程来:

Spring使用三级缓存解决循环依赖

2.1.3. 三级缓存解决循环依赖伪代码

看完图还觉得不清晰的话,我们把所有spring中三级缓存相关的代码汇总到一起,用伪代码的方式,拍平成一个方法,大家应该感觉会更清晰了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
java复制代码// 一级缓存
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 二级缓存
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);
// 三级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

protected Object getBean(final String beanName) {
// !以下为getSingleton逻辑!
// 先从一级缓存获取
Object single = singletonObjects.get(beanName);
if (single != null) {
return single;
}
// 再从二级缓存获取
single = earlySingletonObjects.get(beanName);
if (single != null) {
return single;
}
// 从三级缓存获取objectFactory
ObjectFactory<?> objectFactory = singletonFactories.get(beanName);
if (objectFactory != null) {
single = objectFactory.get();
// 升到二级缓存
earlySingletonObjects.put(beanName, single);
singletonFactories.remove(beanName);
return single;
}
// !以上为getSingleton逻辑!

// !以下为doCreateBean逻辑
// 缓存完全拿不到,需要创建
// 创建实例
Object beanInstance = createBeanInstance(beanName);
// 实例创建之后,放入三级缓存
singletonFactories.put(beanName, () -> return beanInstance);
// 依赖注入,会触发依赖的bean的getBean方法
populateBean(beanName, beanInstance);
// 初始化方法调用
initializeBean(beanName, beanInstance);

// 依赖注入完之后,如果二级缓存有值,说明出现了循环依赖
// 这个时候直接取二级缓存中的bean实例
Object earlySingletonReference = earlySingletonObjects.get(beanName);
if (earlySingletonReference != null) {
beanInstance = earlySingletonObject;
}
// !以上为doCreateBean逻辑

// 从二三缓存移除,放入一级缓存
singletonObjects.put(beanName, beanInstance);
earlySingletonObjects.remove(beanName);
singletonFactories.remove(beanName);

return beanInstance;
}

把所有逻辑放到一起之后会清晰很多,同学们只需要自行模拟一遍,再populateBean中再次调用getBean逻辑进行依赖注入,应该就能捋清楚了。

2.1.4. 标记当前bean正在创建

在我们刚刚看到的将bean实例封装成ObjectFactory并放入三级缓存的流程中,有一个判断是当前bean是正在创建,这个状态又是怎么判断的呢:

1
2
3
4
5
6
7
java复制代码// 重点是这里了,如果是单例bean&&允许循环依赖&&当前bean正在创建
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 加入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}

我们看一下这个isSingletonCurrentlyInCreation的逻辑:

1
2
3
4
5
java复制代码private final Set<String> singletonsCurrentlyInCreation =
Collections.newSetFromMap(new ConcurrentHashMap<>(16));
public boolean isSingletonCurrentlyInCreation(String beanName) {
return this.singletonsCurrentlyInCreation.contains(beanName);
}

可以看到额,其实就是判断当前beanName是不是在这个singletonsCurrentlyInCreation容器中,那么这个容器中的值又是什么时候操作的呢?

希望同学们还记得getSingleton(beanName, singletonFactory)中有调用的beforeSingletonCreation和afterSingletonCreation:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
java复制代码public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 一级缓存拿不到
// 注意一下这个方法,这里会标记这个bean正在创建
beforeSingletonCreation(beanName);
boolean newSingleton = false;
try {
// 调用外部传入的lamdba,即createBean逻辑
singletonObject = singletonFactory.getObject();
// 标记一下
newSingleton = true;
}
catch (BeanCreationException ex) {
throw ex;
}
finally {
// 这里是从正在创建的列表移除,到这里这个bean要么已经完全初始化完成了
// 要么就是初始化失败,都需要移除的
afterSingletonCreation(beanName);
}
if (newSingleton) {
// 如果是新初始化了一个单例bean,加入一级缓存
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}

我们现在来看一下这两个方法的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码protected void beforeSingletonCreation(String beanName) {
// 加入singletonsCurrentlyInCreation,由于singletonsCurrentlyInCreation是一个set
// 如果加入失败的话,说明在创建两次这个bean
// 这个时候会抛出循环依赖异常
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.add(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}
}

protected void afterSingletonCreation(String beanName) {
// 从singletonsCurrentlyInCreation中删除
if (!this.inCreationCheckExclusions.contains(beanName) && !this.singletonsCurrentlyInCreation.remove(beanName)) {
throw new IllegalStateException("Singleton '" + beanName + "' isn't currently in creation");
}
}

可以看到,我们这两个方法主要就是对singletonsCurrentlyInCreation容器进行操作的,inCreationCheckExclusions这个容器可以不用管它,这名称一看就是一些白名单之类的配置。

这里需要主要的是beforeSingletonCreation中,如果singletonsCurrentlyInCreation.add(beanName)失败的话,是会抛出BeanCurrentlyInCreationException的,这代表spring遇到了无法解决的循环依赖问题,此时会抛出异常中断初始化流程,毕竟单例的bean不允许被创建两次。

2.2. 为什么要设计为三级结构?

2.2.1. 只做两级缓存会有什么问题?

其实到这里,我们已经清楚,三级缓存的设计已经成功的解决了循环依赖的问题。

可是按我们自己的设计思路,明明只需要两级缓存就可以解决,spring却使用了三级缓存,难道是为了炫技么?

这个时候,就需要我们再细致的看一下bean初始化过程了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {
// ...
if (earlySingletonExposure) {
// 放入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
Object exposedObject = bean;
try {
populateBean(beanName, mbd, instanceWrapper);
// 这里这个引用被替换了
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
// ...
return exposedObject;
}

仔细观察,initializeBean方法是可能返回一个新的对象,从而把createBeanInstance创建的bean实例替换掉的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
// 调用aware接口
invokeAwareMethods(beanName, bean);
Object wrappedBean = bean;
if (mbd == null || !mbd.isSynthetic()) {
// 埋点
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

try {
// 初始化方法
invokeInitMethods(beanName, wrappedBean, mbd);
}
catch (Throwable ex) {
throw new BeanCreationException(...);
}
if (mbd == null || !mbd.isSynthetic()) {
// 埋点
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}

可以看到,我们的postProcessBeforeInitialization和postProcessAfterInitialization的埋点方法都是有可能把我们的bean替换掉的。

那么结合整个流程来看,由于我们放入缓存之后,initializeBean方法中可能存在替换bean的情况,如果只有两级缓存的话:

Spring只使用二级缓存会出现的问题

这会导致B中注入的A实例与singletonObjects中保存的AA实例不一致,而之后其他的实例注入a时,却会拿到singletonObjects中的AA实例,这样肯定是不符合预期的。

2.2.2. 三级缓存是如何解决问题的

那么这个问题应该怎么解决呢?

这个时候我们就要回到添加三级缓存的地方看一下了。addSingletonFactory的第二个参数就是一个ObjectFactory,并且这个ObjectFactory最终将会放入三级缓存,现在我们再回头看调用addSingletonFactory的地方:

1
2
java复制代码 // 加入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));

熟悉lamdba语法的同学都知道,getEarlyBeanReference其实就是放入三级缓存中的ObjectFactory的getObject方法的逻辑了,那我们一起来看一下,这个方法是做了什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, Object bean) {
Object exposedObject = bean;
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
// 调用了beanPostProcessor的一个埋点方法
exposedObject = ibp.getEarlyBeanReference(exposedObject, beanName);
}
}
}
// 返回的是埋点替换的bean
return exposedObject;
}

咦,这里也有个埋点,可以替换掉bean的引用。

原来为了解决initializeBean可能替换bean引用的问题,spring就设计了这个三级缓存,他在第三级里保存了一个ObjectFactory,其实具体就是getEarlyBeanReference的调用,其中提供了一个getEarlyBeanReference的埋点方法,通过这个埋点方法,它允许开发人员把需要替换的bean,提早替换出来。

比如说如果在initializeBean方法中希望把A换成AA(这个逻辑肯定是通过某个beanPostProcessor来做的),那么你这个beanPostProcessor可以同时提供getEarlyBeanReference方法,在出现循环依赖的时候,可以提前把A->AA这个逻辑做了,并且initializeBean方法不再做这个A->AA的逻辑,并且,当我们的循环依赖逻辑走完,A创建->注入B->触发B初始化->注入A->执行缓存逻辑获取AA实例并放入二级缓存->B初始化完成->回到A的初始化逻辑时,通过以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码protected Object doCreateBean(...) {
populateBean(beanName, mbd, instanceWrapper);
Object exposedObject = initializeBean(beanName, exposedObject, mbd);

if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
// 如果二级缓存存在,直接使用二级缓存
exposedObject = earlySingletonReference;
}
}
}
return exposedObject;
}

这样就能保证当前bean中注入的AA和singletonObjects中的AA实例是同一个对象了。

将会把二级缓存中的AA直接返回,这是就能保证B中注入的AA实例与spring管理起来的最终的AA实例是同一个了。

整个流程梳理一下就是这样:

Spring使用三级缓存解决循环依赖-最终版

2.2.3. 三级缓存的实际应用

既然设计了这个三级缓存,那么肯定是有实际需求的,我们上面分析了一大堆,现在正好举一个例子看一下,为什么spring需要三级缓存。

我们都知道,Spring的AOP功能,是通过生成动态代理类来实现的,而最后我们使用的也都是代理类实例而不是原始类实例。而AOP代理类的创建,就是在initializeBean方法的postProcessAfterInitialization埋点中,我们直接看一下getEarlyBeanReference和postProcessAfterInitialization这两个埋点吧(具体类是AbstractAutoProxyCreator,之后讲AOP的时候会细讲):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport
implements SmartInstantiationAwareBeanPostProcessor, BeanFactoryAware {

private final Map<Object, Object> earlyProxyReferences = new ConcurrentHashMap<>(16);
// 如果出现循环依赖,getEarlyBeanReference会先被调用到
public Object getEarlyBeanReference(Object bean, String beanName) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 这个时候把当前类放入earlyProxyReferences
this.earlyProxyReferences.put(cacheKey, bean);
// 直接返回了一个代理实例
return wrapIfNecessary(bean, beanName, cacheKey);
}

public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean != null) {
Object cacheKey = getCacheKey(bean.getClass(), beanName);
// 注意这个判断,如果出现了循环依赖,这个if块是进不去的
if (this.earlyProxyReferences.remove(cacheKey) != bean) {
// 如果没有出现循环依赖,会在这里创建代理类
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
}

就这样,Spring巧妙的使用三级缓存来解决了这个不同实例的问题。当然,如果我们需要自己开发类似代理之类的可能改变bean引用的功能时,也需要遵循getEarlyBeanReference方法的埋点逻辑,学习AbstractAutoProxyCreator中的方式,才能让spring按照我们的预期来工作。

四、三级缓存无法解决的问题

1. 构造器循环依赖

刚刚讲了很多三级缓存的实现,以及它是怎么解决循环依赖的问题的。

但是,是不是使用了三级缓存,就能解决所有的循环依赖问题呢?

当然不是的,有一个特殊的循环依赖,由于java语言特性的原因,是永远无法解决的,那就是构造器循环依赖。

比如以下两个类:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class A {
private final B b;
public A(final B b) {
this.b = b;
}
}
public class B {
private final A a;
public B(final A a) {
this.a = a;
}
}

抛开Spring来讲,同学们你们有办法让这两个类实例化成功么?

该不会有同学说,这有何难看我的:

1
2
java复制代码// 你看,这样不行么~
final A a = new A(new B(a));

不好意思,这个真的不行,不信可以去试试。从语法上来讲,java的语言特性决定了不允许使用未初始化完成的变量。我们只能无限套娃:

1
2
java复制代码// 这样明显就没有解决问题,是个无限套娃的死循环
final A a = new A(new B(new A(new B(new A(new B(...))))));

所以,连我们都无法解决的问题,就不应该强求spring来解决了吧~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码@Service
public class A {
private final B b;
public A(final B b) {
this.b = b;
}
}
@Service
public class B {
private final A a;
public B(final A a) {
this.a = a;
}
}

启动之后,果然报错了:

1
shell复制代码Error creating bean with name 'a': Requested bean is currently in creation: Is there an unresolvable circular reference?

2. Spring真的对构造器循环依赖束手无策么?

难道,spring对于这种循环依赖真的束手无策了么?其实不是的,spring还有@Lazy这个大杀器…只需要我们对刚刚那两个类小小的改造一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码@Service
public class A {
private final B b;
public A(final B b) {
this.b = b;
}
public void prt() {
System.out.println("in a prt");
}
}
@Service
public class B {
private final A a;
public B(@Lazy final A a) {
this.a = a;
}
public void prt() {
a.prt();
}
}
// 启动
@Test
public void test() {
applicationContext = new ClassPathXmlApplicationContext("spring.xml");
B bean = applicationContext.getBean(B.class);
bean.prt();
}

都说了成功了,运行结果同学们也能猜到了吧:

1
shell复制代码in a prt

(同学们也可以自己尝试一下~

3. @Lazy原理

这个时候我们必须要想一下,spring是怎么通过 @Lazy来绕过我们刚刚解决不了的无限套娃问题了。

因为这里涉及到之前没有细讲的参数注入时候的参数解析问题,我这边就不带大家从入口处一步一步深入了,这边直接空降到目标代码DefaultListableBeanFactory#resolveDependency:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
// 跳过...
// 这个地方是我们获取依赖的地方
// 尝试获取一个懒加载代理
Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
descriptor, requestingBeanName);
if (result == null) {
// 如果没获取到懒加载代理,就直接去获取bean实例了,这里最终会调用getBean
result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
}
return result;
}

我们直接看一下这个getLazyResolutionProxyIfNecessary,这个方法就是获取LazyProxy的地方了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
java复制代码public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver {

@Override
@Nullable
public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
// 如果是懒加载的,就构建一个懒加载的代理
return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null);
}
// 判断是否是懒加载的,主要就是判断@Lazy注解,简单看下就好了
protected boolean isLazy(DependencyDescriptor descriptor) {
for (Annotation ann : descriptor.getAnnotations()) {
Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class);
if (lazy != null && lazy.value()) {
return true;
}
}
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
Method method = methodParam.getMethod();
if (method == null || void.class == method.getReturnType()) {
Lazy lazy = AnnotationUtils.getAnnotation(methodParam.getAnnotatedElement(), Lazy.class);
if (lazy != null && lazy.value()) {
return true;
}
}
}
return false;
}

protected Object buildLazyResolutionProxy(final DependencyDescriptor descriptor, final @Nullable String beanName) {
final DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) getBeanFactory();
// 构造了一个TargetSource
TargetSource ts = new TargetSource() {
@Override
public Class<?> getTargetClass() {
return descriptor.getDependencyType();
}
@Override
public boolean isStatic() {
return false;
}
@Override
public Object getTarget() {
// 再对应的getTarget方法里,才会去正真加载依赖,进而调用getBean方法
Object target = beanFactory.doResolveDependency(descriptor, beanName, null, null);
if (target == null) {
Class<?> type = getTargetClass();
if (Map.class == type) {
return Collections.emptyMap();
}
else if (List.class == type) {
return Collections.emptyList();
}
else if (Set.class == type || Collection.class == type) {
return Collections.emptySet();
}
throw new NoSuchBeanDefinitionException(...);
}
return target;
}
@Override
public void releaseTarget(Object target) {
}
};
// 创建代理工厂ProxyFactory
ProxyFactory pf = new ProxyFactory();
pf.setTargetSource(ts);
Class<?> dependencyType = descriptor.getDependencyType();
if (dependencyType.isInterface()) {
pf.addInterface(dependencyType);
}
// 创建返回代理类
return pf.getProxy(beanFactory.getBeanClassLoader());
}
}

同学们可能对TargetSource和ProxyFactory这些不熟悉,没关系,这不妨碍我们理解逻辑。

从源码我们可以看到,对于@Lazy的依赖,我们其实是返回了一个代理类(以下称为LazyProxy)而不是正真通过getBean拿到目标bean注入。而真正的获取bean的逻辑,被封装到了一个TargetSource类的getTarget方法中,而这个TargetSource类最终被用来生成LazyProxy了,那么我们是不是可以推测,LazyProxy应该持有这个TargetSource对象。

而从我们懒加载的语意来讲,是说真正使用到这个bean(调用这个bean的某个方法时)的时候,才对这个属性进行注入/初始化。

那么对于当前这个例子来讲,就是说其实B创建的时候,并没有去调用getBean("a")去获取构造器的参数,而是直接生成了一个LazyProxy来做B构造器的参数,而B之后正真调用到A的方法时,才会去调用TargetSource中的getTarget获取A实例,即调用getBean("a"),这个时候A早就实例化好了,所以也就不会有循环依赖问题了。

4. 伪代码描述

还是同样,我们可以用伪代码来描述一下这个流程,伪代码我们就直接用静态代理来描述了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码public class A {
private final B b;
public A(final B b) {
this.b = b;
}
public void prt() {
System.out.println("in a prt");
}
}

public class B {
private final A a;
public B(final A a) {
this.a = a;
}
public void prt() {
a.prt();
}
}
// A的懒加载代理类
public class LazyProxyA extends A {
private A source;
private final Map<String, Object> ioc;
private final String beanName;
public LazyProxyA(Map<String, Object> ioc, String beanName) {
super(null);
this.ioc = ioc;
this.beanName = beanName;
}
@Override
public void prt() {
if (source == null) {
source = (A) ioc.get(beanName);
}
source.prt();
}
}

那么整个初始化的流程简单来描述就是:

1
2
3
4
5
6
7
java复制代码Map<String, Object> ioc = new HashMap<>();
void init() {
B b = new B(new LazyProxyA(ioc, "a"));
ioc.put("b", b);
A a = new A((B)ioc.get("b"));
ioc.put("a", a);
}

我们也模拟一下运行:

1
2
3
4
5
6
java复制代码void test() {
// 容器初始化
init();
B b = (B)ioc.get("b");
b.prt();
}

当然是能成功打印的:

1
shell复制代码in a prt

六、总结

关于循环依赖的问题,Spring提供了通过设计缓存的方式来解决的,而设计为三级缓存,主要是为了解决bean初始化过程中,实例被放入缓存之后,实例的引用还可能在调用initializeBean方法时被替换的问题。

对于构造器的循环依赖,三级缓存设计是无法解决的,这属于java语言的约束;但是spring提供了一种使用@Lazy的方式,绕过这个限制,使得构造器的循环依赖在特定情况下(循环链中的某个注入打上@Lazy注解)也能解决。

(小声BB,接下来更新应该会变慢了…

创作不易,转载请篇首注明 作者:掘金@小希子 + 来源链接~

如果想了解更多Spring源码知识,点击前往其余逐行解析Spring系列

٩(* ఠO ఠ)=3⁼³₌₃⁼³₌₃⁼³₌₃嘟啦啦啦啦。。。

这里是新人博主小希子,大佬们都看到这了,左上角点个赞再走吧~~

本文转载自: 掘金

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

用python写游戏脚本原来这么简单

发表于 2020-08-14

前言

最近在玩儿公主连结,之前也玩儿过阴阳师这样的游戏,这样的游戏都会有个初始号这样的东西,或者说是可以肝的东西。

当然,作为一名程序员,肝这种东西完全可以用写代码的方式帮我们自动完成。游戏脚本其实并不高深,最简单的体验方法就是下载一个Airtest了,直接截几个图片,写几层代码,就可以按照自己的逻辑玩儿游戏了。

当然,本篇文章不是要讲Airtest这个怎么用,而是用原始的python+opencv来实现上面的操作。

这两天我写了一个公主连结刷初始号的程序,也不能算写游戏脚本的老手,这篇文章主要是分享一些基础的技术和使用上的心得吧。

准备工作

首先,我们要完成以下准备。

  • 安卓设备一个:模拟器或者真机都可以。
  • 安装ADB,并添加到系统的PATH里:adb是用来
  • 安装tesseract-ocr,并添加到系统的PATH里:帮助我们实现简单的字符识别
  • 安装python3.7以上的版本

这里adb和tesseract我放在百度网盘里了,里面顺便放了一个录制的效果视频。

链接:pan.baidu.com/s/1edTPu2o7… 提取码:33aw

python库安装

1
shell复制代码pip install pillow pytesseract opencv-python

除此以外,如果有需要可以安装uiautomator2,这篇文章就不涉及这块知识了。

使用adb获取安卓设备

这里我们主要是涉及到单个安卓设备的ADB连接操作,首先我们打开模拟器。

然后我们调用adb devices来获取当前的安卓设备,我这里是一个模拟器。

接下来可以调用adb shell测试一下是否能进入到安卓设备的shell环境下,确认可以输入exit退出即可。

如果有的时候进不了shell,可以先调用一下adb kill-server,然后再调用adb devices。

可能常用的ADB Shell命令

接下来是一些ADB的命令操作。通过adb命令,我们可以用python来操作的安卓设备。

屏幕截图

最常见的操作就是截图了,先调用screencap截图放到安卓设备里,然后再把截图下拉到电脑。

1
2
3
python复制代码def take_screenshot():
os.system("adb shell screencap -p /data/screenshot.png")
os.system("adb pull /data/screenshot.png ./tmp.png")

下拉文件

下拉文件就是刚刚那个adb pull了,以公主连结为例,以下代码可以导出账号信息的xml,以后通过xml就可以登录了。

1
python复制代码os.system(f"adb pull /data/data/tw.sonet.princessconnect/shared_prefs/tw.sonet.princessconnect.v2.playerprefs.xml ./user_info.xml")

上传文件

有了下拉自然就有上传了,通过adb push即可完成。以公主连结为例,以下代码可以完成账号的切换。

1
2
3
4
5
python复制代码# 切换账号1
os.system("adb push ./user_info1.xml /data/data/tw.sonet.princessconnect/shared_prefs/tw.sonet.princessconnect.v2.playerprefs.xml")

# 切换账号2
os.system("adb push ./user_info2.xml /data/data/tw.sonet.princessconnect/shared_prefs/tw.sonet.princessconnect.v2.playerprefs.xml")

点击屏幕某个位置

1
2
3
4
5
python复制代码def adb_click(center, offset=(0, 0)):
(x, y) = center
x += offset[0]
y += offset[1]
os.system(f"adb shell input tap {x} {y}")

输入文字

1
2
arduino复制代码text = "YourPassword"
os.system(f"adb shell input text {text}")

删除字符

有的时候输入框会有输入的缓存,我们需要删除字符。

1
2
3
python复制代码# 删除10个字符
for i in range(10):
os.system("adb shell input keyevent 67")

查询当前运行的包名和Activity

通过以下代码,可以查询当前运行的程序的Activity,也可以顺便查包名。

1
shell复制代码adb shell dumpsys activity activities

停止某个应用

有时候会需要停止某个应用,需要提供应用的包名。

1
arduino复制代码adb shell am force-stop tw.sonet.princessconnect

开启某个应用

开启某个应用需要提供包名以及Activity。

1
shell复制代码adb shell am start -W -n tw.sonet.princessconnect/jp.co.cygames.activity.OverrideUnityActivity

图像操作

对于图像的操作第一就是图像查找了,比如说像Airtest提供的这种,无非就是判断某个图像在不在截屏中,在的话在什么位置。

除此之外还需要一些抠图,比如说我们想获取账号的id,账号的等级,需要截取出一部分图片然后进行OCR操作。

图像查找

图像查找其实就是先拿到两张图片,然后调用cv2.matchTemplate方法来查找是否存在以及位置,这里匹配是一个相对模糊的匹配,会有一个相似度的概率,最高是1。我们设定一个阈值来判断模板是否在截屏里即可。

这里截屏如下,文件名为tmp.png:

模板如下:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
python复制代码import cv2

def image_to_position(screen, template):
image_x, image_y = template.shape[:2]
result = cv2.matchTemplate(screen, template, cv2.TM_CCOEFF_NORMED)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
print("prob:", max_val)
if max_val > 0.98:
global center
center = (max_loc[0] + image_y / 2, max_loc[1] + image_x / 2)
return center
else:
return False

if __name__ == "__main__":
screen = cv2.imread('tmp.png')
template = cv2.imread('Xuandan.png')
print(image_to_position(screen, template))

运行上述代码后,可以看到模板匹配出来的概率为0.9977,位置为(1165, 693),对于一张图片,左上角为原点,因为我的分辨率是1280 * 720,那么右下角的坐标就是(1280, 720)。可以看到我们这个选单其实就是刚好在右下角的位置。

如何快速裁剪模板?(win10)

游戏脚本其实并不是代码很难写,而是需要截很多的图,这些图要保证分辨率和原始一样。我发现在win10如果用画图打开图片

可以保证使用QQ截屏出来的分辨率,和图片本身的分辨率一样。

这个时候直接用qq截屏出来的模板即可直接用于识别。

图像裁剪

接下来就是有时候需要裁剪一些图像了,当然我们的模板图片也可以通过裁剪图片的方式得到,这样的模板图片是最准的。

裁剪其实就是需要裁剪的位置,以及需要的高度和宽度,说白了就是一篇长方形的区域,下面的代码使用PIL库实现。

1
2
3
4
5
6
7
8
9
10
11
python复制代码from PIL import Image

def crop_screenshot(img_file, pos_x, pos_y, width, height, out_file):
img = Image.open(img_file)
region = (pos_x, pos_y, pos_x + width, pos_y + height)
cropImg = img.crop(region)
cropImg.save(out_file)
print("exported:", out_file)

if __name__ == "__main__":
crop_screenshot("tmp.png", 817,556, 190, 24, "test_id.png")

上面的代码以截取玩家的id为例。

运行代码后,得到截图如下:

简单的OCR

得到了以上的图片信息后就是进行OCR了,也就是光学字符识别。这里代码非常简单,只要调用API即可。

1
2
3
4
5
6
python复制代码from PIL import Image
import pytesseract

image = Image.open('test_id.png')
content = pytesseract.image_to_string(image) # 识别图片
print(content)

不过需要注意的一点就是pytesseract识别出来的结果会有空格符,换行符这样的符号,真正要用的时候进行一些字符的过滤即可。

The End

这篇文章到这里就结束了,主要还是介绍一些ADB以及图像相关的基础操作,有些内容比如说多开和uiautomator2因为我暂时没用到所以就没写,百度一下应该也不是很难。代码写的比较丑还没完善好,就先不放了。

本文转载自: 掘金

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

计算机系统

发表于 2020-08-13

⭐️ 本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 和 [BaguTree Pro] 知识星球提问。

计算机系统是程序员的知识体系中最基础的理论知识,你越早掌握这些知识,你就能越早享受知识带来的 “复利效应”。

本文是计算机系统系列的第 5 篇文章,完整文章目录请移步到文章末尾~

前言

最近在公众号阿里技术上看到一套孤尽老师出的 10 道 Java 测试题(据说阿里 P7 工程师的答题正确率只有 50%) ,其中有几道题是关于浮点数的,聪明的你,在评论区留下答案吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
ini复制代码(1)
float a = 0.125f;
double b = 0.125d;
System.out.println((a - b) == 0.0);
代码的输出结果是什么?

A. true
B. false

(2)
double c = 0.8;
double d = 0.7;
double e = 0.6;

那么 c-d 与 d-e 是否相等?

A. true
B. false

(3)
System.out.println(1.0 / 0); 的结果是什么?

A. 抛出异常
B. Infinity
C. NaN

(4)
System.out.println(0.0 / 0.0); 的结果是什么?

A. 抛出异常
B. Infinity
C. NaN
D. 1.0


(5) 引用自《技术之瞳》
以下数字在表示为double(8字节的双精度浮点数)时存在舍入误差的有:

A 100
B 根号2
C 10^30
D 0.1
E 0.5

(6)
写出float x 与“零值”比较的if语句

目录


  1. 相关概念

关于浮点数的相关概念如下,在下面的分享中,我将不重复解释:


  1. 计算机中数据的表示方法

  • 在你的Chrome浏览器上按F12,然后找到console,输入表达式0.1 + 0.2,回车
  • 在你的电子计算器上按0.1 + 0.2 =

你会发现前者的结果是0.30000000000000004,而后者的结果是0.3(当然了!)。那么,为什么计算机的准确度,连普通的电子计算器的都比不上?关键在于计算机与计算器使用了不同的数据表示方法。

2.1 n 位二进制可以表示的信息量

对于整数来说,大家都知道8位有符号整数可以表示[-128,127],8位无符号整数可以表示[0,255],不管怎么样,8位二进制无论如何也只能表示256个整数。当需要表示257这个数,有且只有两个办法:

  • 1、增加位数,例如9位二进制可表示的数值范围就可以容纳257这个数
  • 2、改变编码规则,例如规定真值是在机器数的基础上加一,这样的话,0000,0000就表示数1,1111,1111就表示数257。(事实上,这就是移码干的事情,3.1节会再提到)

这就是计算机的自有属性,数字计算机只能处理离散数据,二进制的位数直接决定了它能表示的离散数据个数,也决定了它所能表示的信息个数,对于n位二进制数,它可以表示的信息量为2N2^N2N。

同理,我们把问题域扩展到全体实数,8位二进制同样也只能表示256个实数。假如约定这样一种8位编码:最低两位为小数区域,其余是整数区域,这样就有:

1
2
3
4
5
6
7
arduino复制代码000000.00 // 表示 0.0
000000.01 // 表示 0.25
000000.10 // 表示 0.5
000000.11 // 表示 0.75
000001.00 // 表示 1.0
000001.01 // 表示 1.25
... 此处省略250个数

我们发现,介于0.0到0.25的数字被跳过了,而即使把小数区域的位长扩大到8位、16位、甚至一个极大的位数,也无法充分表示介于0.0到0.25所有的数。这是因为,在0.0到0.25之间的数是连续的,有无限多个数,但是有限的N位长二进制最多只能表示2N2^N2N个信息量,有限的信息量无法表示无限的数据量,这就是现实世界与计算机世界的矛盾。

2.2 定点数表示

实数有两种表示格式,分别是定点数和浮点数。像上面说的这种约定整数部分和小数部分为固定位置的格式,就是定点数表示。

  • 定义:
  • 定点数(fixed point numbers)* 约定机器数中的小数点总是固定在某个特定的位置。
  • 格式:
    分为符号位、整数部分、隐含的小数点、小数部分。
  • 特点:
  • 整数部分和小数部分位长固定,当需要表示绝对值特大或者特小的数需要很大的空间*。

2.3 浮点数表示

我们已经知道32位二进制可以表示的信息量有232≈4∗1092^{32}\approx 4*10^9232≈4∗109,但是很多语言都会宣称它们的32位单精度浮点数的数值范围约为−3.4∗1038~3.4∗1038-3.4*10^{38}~ 3.4*10^{38}−3.4∗1038~3.4∗1038(左右边界),这是因为采用了浮点数格式。

  • 定义:
  • 浮点数(floating point numbers)使用科学计数法存储数字*,小数点的位置根据指数的大小而浮动。
  • 格式:
    分为符号位、指数、尾数 :

N=2E∗MN=2^E*MN=2E∗M

  • 特点:
    一部分位作为指数,可以扩大所表示的数值范围
  • 意义:
    是数字计算机表示实数的格式,并以IEEE 754 (IEEE Standard for Binary Floating-Point Arithmetic)为标准。

2.4 定点数和浮点数的区别

  • 表示范围:浮点数一部分位为指数,相同位长,浮点数格式所能表示的数值范围远远大于定点数格式;
  • 精度大小:浮点数格式只有一部分位是有效数值位,相同位长,浮点格式的精度比定点格式低;
  • 运算复杂度:浮点数主要包括指数和尾数两部分,运算时需要对阶、尾数计算、规格化等步骤,浮点运算比定点运算复杂;
  • 溢出:定点运算在数超过可表示数值范围即发生溢出;在浮点运算中,只有规格化后数值超过指数所能表示的范围才溢出。

2.5 计算机表示实数的步骤

前面讲到相关概念时提到了实数的概念,具体如下:

复数的分类

一个虚数上相当于两个实数,所以我们只需要关心实数在计算机中的表示即可,将一个实数装载入计算机需要分为三个步骤:

2.5.1 转换为二进制数格式

这个步骤可能损失精度,换句话说,有些数会损失精度,而有些数不会,这取决于表示这个数需要的信息量和浮点数的存储格式

  • 无理数(无限不循环小数) 包含的信息量是无限的,例如圆周率π\piπ,没有任何一本书能够写到圆周率最后一位,java.lang.Math.PI也只是π\piπ的近似值,类似的,使用有限的二进制位自然无法精确表示;
  • 有限循环小数包含的信息量是有限的,它的信息量分为整数部分+小数不循环部分+小数循环部分,例如1.8333333…=1.83‾1.8333333… = 1.8\overline{3}1.8333333…=1.83。但是浮点数的表示方法分为符号位、指数区域和尾数区域,并不会单独用一块区域来存储循环的部分,因此有限循环小数也无法精确表示;
  • 最后剩下整数和有限小数,它们包含的信息量也是有限的,关键看是否有因子5。举两个例子:0.1和1万亿,请问哪个数能用二进制数精确表示?

从十进制看,0.1拥有2个信息量(个位数为0,第一位小数为1),1万亿拥有一万亿个信息量,二选一的话,肯定是选择信息量更低的0.1。但是,从二进制看,我们会发现0.1转换为二进制居然是一个无限循环小数0.00011‾0.0\overline{0011}0.00011(将整数部分除2取余、小数部分乘2取整来完成转换),所以答案是:1万亿可以精确表示,而0.1无法精确表示!

事实上,在0.1 到 0.9 的 9 个小数中,只有 0.5 可以用二进制精确的表示。怎么理解呢?我们把1想象成一个圆,在十进制里,它可以划分为10等分;但在二进制里,它只能划分为2等分。
也就是说二进制里一位,要么表示0,要么表示一半,它没有办法像十进制那样表示3/10、4/10、6/10…… 1的一半在十进制里是什么?0.5,所以二进制可以精确表示0.5,任何包含因子5的数都可以用二进制精确表示。无法精确表示的数字,存储值只能是真实值的近似表示。

提示: 类似地,思考下十进制数格式可以精确表示1/3吗?

2.5.2 转换为二进制科学计数法表示

这个步骤将二进制小数转换为规范化的科学计数法表示:N=a∗BEN = a * B^EN=a∗BE,因为只是写法的转换,所以这一步没有精度损失。

2.5.3 转换为IEEE 754 标准格式

IEEE 754严格规定了尾数域和指数域可表示的大小,位数有限,意味着信息量是有限的。有些数需要的二进制数据量巨大,在这个步骤自然会损失精度,具体如下:

  • 大于浮点数可以表示的最大绝对值:上溢(溢出到±∞\pm\infty±∞)
  • 小于浮点数可以表示的最小绝对值:下溢(溢出到±0\pm0±0)
  • 尾数有效位数超过尾数域位数(另外还有隐含的整数位1):舍入误差

  1. IEEE 754 标准的浮点数

IEEE 二进制浮点数算术标准(IEEE 754)是广泛使用的浮点数运算标准,是大多数高级语言的现行浮点运算标准,例如C/C++、Java、JavaScript等。

3.1 一般格式

浮点数格式的关键是科学计数法格式:N=a∗BEN = a * B^EN=a∗BE,其中:

  • a称为尾数(mantissa),或称有效数字(significand)
  • B称为基数(base),在二进制数中,基数是2
  • E称为指数(exponent)

一个数的科学计数法表示是不唯一的,举个例子,对于二进制数1111.0000(2)1111.0000_{(2)}1111.0000(2)来说,以下都是合法的科学计数法表示:111.1∗2111.1*2111.1∗2、11.11∗2211.11*2^211.11∗22、11110∗2−111110*2^{-1}11110∗2−1,但这些都不是规格化的表示,唯一规格化的表示为:1.111∗231.111*2^31.111∗23。

对于一个科学计数法表示,当尾数a的整数部分有且仅有一位有效数字时,我们称它是规格化的。由于0在数字的最左边是无效的,而在二进制的世界里只有0和1,因此,二进制数使用规格化的科学计数法时,整数部分固定为1。

既然整数部分1是固定的,那么就没有必要存储整数部分的信息了。正因如此,IEEE 754 标准的浮点数采用隐藏位的策略,整数部分的1是隐含的,不需要占用一位比特,这样是使得尾数可以多一位有效数。

综上,IEEE 754 浮点数的一般格式如下:
N=(−1)s∗1.f∗2EN = (-1)^s*1.f*2^EN=(−1)s∗1.f∗2E

IEEE 754 标准的一般格式

现在,我们已经知道浮点数划分的三个区域,现在我们来看这三个区域是如何求值的:

  • 符号位:0表示正,1表示负
  • 指数区域:移码
    指数区域采用移码表示:E=机器数−biasE = 机器数 - biasE=机器数−bias,偏移值bias=2位长−1−1bias=2^{位长-1}-1bias=2位长−1−1
    例如位长为8时,bias=127bias=127bias=127,位长为11时,bias=1023bias=1023bias=1023。注意:指数域全0和全1为特殊值
  • 尾数区域:隐藏整数位的原码
    尾数区域采用原码表示:1.f=1+机器数1.f = 1 + 机器数1.f=1+机器数

举个例子,十进制数100(10)100_{(10)}100(10)转换为二进制为1.100100∗2(2)61.100100*2^6_{(2)}1.100100∗2(2)6。这里推荐一个站点:浮点数转换器,它可以很方便地对比实数的真值与机器数表示,如下图所示:

3.2 两种常用格式

前面讲的是IEEE 754 浮点数的一般格式,其中最常用的是32位单精度浮点数和64位双精度浮点数,在高级语言中通常代表float和double两种数据类型(例如C/C++、Java),在有些语言中只有一种数字格式number(例如JavaScript/TypeScript)。

  • 单精度
    单精度浮点数有8位指数,23位尾数,再加上隐藏的整数1,总共有24位二进制精度
  • 双精度
    双精度浮点数有11位指数,52位尾数,再加上隐藏的整数1,总共有53位二进制精度,具体如下:

3.3 特殊值

在 IEEE 754 标准规定指数区域全0 或 全1为特殊值,具体如下:

  • 非规范化数(Denormalized Number)
+ 定义:指数域全0,尾数域不为0(去掉隐含整数域为1的约定)
+ 意义:可以保存绝对值更小的数,所有可表示的浮点数的差值都可以表示
  • +0/-0
+ 定义:指数域全0,尾数域全0(去掉隐含整数域为1的约定)。IEEE 754 未要求具体的尾数域,意味着NaN不是一个而是一族。
+ 意义:符号位为0是+0,符号位为1是-0,在涉及无穷的运算中避免丢失符号信息,例如11/x=x\frac{1}{1/x} = x1/x1=x,如果0不区分正负,在x=±∞x=\pm\inftyx=±∞时不成立
  • 正负无穷(Infinity)
+ 定义:指数域全1,尾数全0
+ 意义:用于表达计算中产生的上溢(overflow),使得计算中出现上溢不至于终止计算
+ 产生:除了NaN外的非零值除以0,其结果为正负无穷
  • NaN(Not a Number)
+ 定义:指数域全1,尾数域不为0
+ 意义:表示计算中的错误情况,例如0.00.0\frac{0.0}{0.0}0.00.0、2\sqrt22,使得计算中出现错误不至于终止计算
+ 特点:`NaN`是无序的,比较操作符在任一操作数为`NaN`是为`false`,`!=`在任一操作数为`NaN`时为true,这意味着`NaN != NaN`。

  1. 总结

为什么 0.1 + 0.2 != 0.3 呢?首先,0.1 和 0.2 这两个实数无法用二进制精确表示。在二进制的世界里,只有包含因子 5 的十进制数才有可能精确表示,而 0.1 和 0.2 转换为二进制后是无限循环小数,在计算机中存储的值只能是真实值的近似表示,这里是第一次精度丢失;其次,计算机浮点数采用了 IEEE 754 标准格式,IEEE 754 严格规定了尾数域和指数域可表示的大小,位数有限,意味着可表示的信息量是有限的,换句话说就会存在三种误差:上溢、下溢和舍入误差。而 0.1 + 0.2 的结果的尾数域部分刚好超过了尾数域位数,超过位数的部分舍去,存在舍入误差,这里是第二次精度丢失。


参考资料

  • 《编码·隐匿在计算机软硬件背后的语言》(第23章) —— [美] Charles Petzold 著
  • 《Java编程思想》(第2章) —— [美] Bruce Eckel 著
  • 《深入理解Java虚拟机》(第6.4节) —— 周志明 著
  • 《JavaScript权威指南》(第3章) —— [美] David Flanagan 著
  • 《计算机组成原理考研复习指导》(第2章) —— 王道论坛 组编
  • 《代码之谜》 (第4、5章)—— justjavac(迷渡)的博客文章

推荐阅读

计算机系统系列完整目录如下(2023/07/11 更新):

  • #1 从图灵机到量子计算机,计算机可以解决所有问题吗?
  • #2 一套用了 70 多年的计算机架构 —— 冯·诺依曼架构
  • #3 为什么计算机中的负数要用补码表示?
  • #4 今天一次把 Unicode 和 UTF-8 说清楚
  • #5 为什么浮点数运算不精确?(阿里笔试)
  • #6 计算机的存储器金字塔长什么样?
  • #7 程序员学习 CPU 有什么用?
  • #8 我把 CPU 三级缓存的秘密,藏在这 8 张图里
  • #9 图解计算机内部的高速公路 —— 总线系统
  • #10 12 张图看懂 CPU 缓存一致性与 MESI 协议,真的一致吗?
  • #11 已经有 MESI 协议,为什么还需要 volatile 关键字?
  • #12 什么是伪共享,如何避免?

⭐️ 永远相信美好的事情即将发生,欢迎加入小彭的 Android 交流社群~

本文转载自: 掘金

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

Kotlin Sealed 是什么?为什么 Google 都

发表于 2020-08-12

在上一篇文章 Google 推荐在项目中使用 Sealed 和 RemoteMediator 中介绍了如何使用 Sealed Classes 在 Flow 基础上对网络请求成功和失败进行处理,而这篇文章是对 Sealed Classes 更加深入的解析,结合函数式编程功能很强大,掌握并且灵活运用它,需要大量的实践。

通过这篇文章你将学习到以下内容:

  • Sealed Classes 原理分析?
  • 枚举和抽象类都有那些局限性?
  • 为什么枚举可以作为单例?枚举作为单例有那些优点?
  • 分别在什么情况下使用枚举和 Sealed Classes?
  • Sealed Classes 究竟是什么?
  • 为什么 Sealed Classes 用于表示受限制的类层次结构?
  • 为什么说 Sealed Classes 是枚举类的扩展?
  • Sealed Classes 的子类可以表示不同状态的实例,那么在项目中如何使用?
  • 禁止在 Sealed Classes 所定义的文件外使用, Kotlin 是如何做到的呢?

枚举和抽象类的局限性

在分析 Sealed Classes 之前,我们先来分析一下枚举和抽象类都有那些局限性,注意:这些局限性是相对于 Sealed Classes 而言的,但是相对于它们自身而言是优点,而 Sealed Classes 出现也正是为了解决这些问题。先来看一下枚举的局限性:

  • 限制枚举每个类型只允许有一个实例
  • 限制所有枚举常量使用相同的类型的值

限制枚举每个类型只允许有一个实例

1
2
3
4
5
6
7
8
9
kotlin复制代码enum class Color(val value: Int) {
Red(1)
}

fun main(args: Array<String>) {
val red1 = Color.Red
val red2 = Color.Red
println("${red1 == red2}") // true
}

最后输出结果

1
ini复制代码red1 == red2 : true

正如你看到的,我们定义了一个单元素的枚举类型,无论 Color.Red 有多少个对象,最终他们的实例都是一个,每个枚举常量仅作为一个实例存在,而一个密封类的子类可以有多个包含状态的实例,这既是枚举的局限性也是枚举的优点。

枚举常量作为一个实例存在的优点: 枚举不仅能防止多次实例化,而且还可以防止反序列化,还能避免多线程同步问题,所以它也被列为实现单例方法之一。简单汇总一下。

是否只有一个实例 是否反序列化 是否是线程安全 是否是懒加载
是 是 是 否

《Effective Java》 一书的作者 Josh Bloch 建议我们使用枚举作为单例,虽然使用枚举实现单例的方法还没有广泛采用,但是单元素的枚举类型已经成为实现 Singleton 的最佳方法。

我们来看一下如何用枚举实现一个单例(与 Java 的实现方式相同),这里不会深究其原理,因为这不是本文的重点内容,小伙伴们可以从掘金搜索,有很多分析这方面原理的文章。

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码interface ISingleton {
fun doSomething()
}

enum class Singleton : ISingleton {
INSTANCE {
override fun doSomething() {
// to do
}
};

fun getInstance(): Singleton = Singleton.INSTANCE
}

但是在实际项目中使用枚举作为单例的很少,我看了很多开源项目,将枚举作为单例的场景少之有少,很大部分原因是因为使用枚举的时候非常不方便。

我这有个建议如果涉及反序列化创建对象的时候,建议使用枚举,因为 Java 规定,枚举的序列化和反序列化是有特殊定制的,因此禁用编译器使用 writeObject 、readObject 、readObjectNoData 、 writeReplace 、readResolve 等方法。

限制所有枚举常量使用相同的类型的值

限制所有枚举常量使用相同的类型的值,也就是说每个枚举常量类型的值是相同的,我们还是用刚才的例子做个演示。

1
2
3
4
5
scss复制代码enum class Color(val value: Int) {
Red(1),
Green(2),
Blue(3);
}

正如你所见,我们在枚举 Color 中定义了三个常量 Red 、Green 、Blue,但是它们只能使用 Int 类型的值,不能使用其他类型的值,如果使用其它类型的值会怎么样?如下所示:

编译器会告诉你只接受 Int 类型的值,无法更改它的类型,也就是说你无法为枚举类型,添加额外的信息。

抽象类的局限性

对于一个抽象类我们可以用一些子类去继承它,但是子类不是固定的,它可以随意扩展,同时也失去枚举常量的受限性。

Sealed Classes 包含了抽象类和枚举的优势:抽象类表示的灵活性和枚举常量的受限性

到这里可能会有一个疑问,如果 Sealed Classes 没有枚举和抽象类的局限性,那么它能在实际项目中给我们带来哪些好处呢?在了解它能带来哪些好处之前,我们先来看看官方对 Sealed Classes 的解释。

Sealed Classes 是什么?

我们先来看一下官方对 Sealed Classes 的解释

我们将上面这段话,简单的总结一下:

  • Sealed Classes 用于表示受限制的类层次结构
  • 从某种意义上说,Sealed Classes 是枚举类的扩展
  • 枚举的不同之处在于,每个枚举常量仅作为单个实例存在,而 Sealed Classes 的子类可以表示不同状态的实例

那上面这三段话分别是什么意思呢?接下来我们围绕这三个方面来分析。

Sealed Classes 用于表示受限制的类层次结构

Sealed Classes 用于表示受限制的类层次结构,其实这句话可以拆成两句话来理解。

  • Sealed Classes 用于表示层级关系: 子类可以是任意的类, 数据类、Kotlin 对象、普通的类,甚至也可以是另一个 Sealed
  • Sealed Classes 受限制: 必须在同一文件中,或者在 Sealed Classes 类的内部中使用,在Kotlin 1.1 之前,规则更加严格,子类只能在 Sealed Classes 类的内部中使用

Sealed Classes 的用法也非常的简单,我们来看一下如何使用 Sealed Classes。

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码sealed class Color {
class Red(val value: Int) : Color()
class Green(val value: Int) : Color()
class Blue(val name: String) : Color()
}

fun isInstance(color: Color) {
when (color) {
is Color.Red -> TODO()
is Color.Green -> TODO()
is Color.Blue -> TODO()
}
}

在这里推荐大家一个快捷键 Mac/Win/Linux:Alt + Enter 可以补全 when 语句下的所有分支,效果如下所示:

更多 AndroidStudio 快捷键,可以看之前的两篇文章

  • 为数不多的人知道的AndroidStudio快捷键(一)
  • 为数不多的人知道的AndroidStudio快捷键(二)

Sealed Classes 是枚举类的扩展

从某种意义上说,Sealed Classes 是枚举类的扩展,其实 Sealed Classes 和枚举很像,我们先来看一个例子。

sealed-enu

正如你所看到的,在 Sealed Classes 内部中,使用 object 声明时,我们可以重用它们,不需要每次创建一个新实例,当这样使用时候,它看起来和枚举非常相似。

注意:实际上很少有人会这么使用,而且也不建议这么用,因为在这种情况枚举比 Sealed Classes 更适合

在什么情况下使用枚举

如果你不需要多次实例化,也不需要不提供特殊行为,或者也不需要添加额外的信息,仅作为单个实例存在,这个时候使用枚举更加合适。

我们来看一下 Paging3 中是如何使用枚举的,一起来看一下 androidx.paging.LoadType 这个类的源码。

1
2
3
4
5
kotlin复制代码enum class LoadType {
REFRESH,
PREPEND,
APPEND
}
枚举常量 作用
refresh 在初始化刷新的使用
append 在加载更多的时候使用
prepend 在当前列表头部添加数据的时候使用

它们不需要多次实例化,也不需要添加任何额外的信息,仅仅表示某种状态,而且它在很多地方都会用到比如 RemoteMediator、PagingSource 等等,想了解更多关于 Paging3 原理和实战案例可以看之前写的几篇文章。

  • Jetpack 成员 Paging3 数据库实践以及源码分析(一)
  • Jetpack 成员 Paging3 网络实践及原理分析(二)
  • Jetpack 成员 Paging3 使用 RemoteMediator 实现加载网络分页数据并更新到数据库中(三)
  • 神奇宝贝(PokemonGo) 眼前一亮的 Jetpack + MVVM 极简实战

4.Sealed Classes 的子类可以表示不同状态的实例

与枚举的不同之处在于,每个枚举常量仅作为单个实例存在,而 Sealed Classes 的子类可以表示不同状态的实例,我们来看个例子可能更容易理解这句话。

这里我们延用之前在 Google 推荐在项目中使用 sealed 和 RemoteMediator 这篇文章中用到的例子,在请求网络的时候需要对成功或者失败进行处理,我们来看一下用 Sealed Classes 如何进行封装。

1
2
3
4
5
csharp复制代码sealed class PokemonResult<out T> {
data class Success<out T>(val value: T) : PokemonResult<T>()

data class Failure(val throwable: Throwable?) : PokemonResult<Nothing>()
}

这里只贴出来部分代码,核心实现可以查看项目 PokemonGo
GitHub 地址:https://github.com/hi-dhl/PokemonGo
代码路径:PokemonGo/app/…/com/hi/dhl/pokemon/data/remote/PokemonResult.kt

一起来看一下如何使用

1
2
3
4
5
6
7
8
csharp复制代码when (result) {
is PokemonResult.Failure -> {
// 进行失败提示
}
is PokemonResult.Success -> {
// 进行成功处理
}
}

我们在来看另外一个例子,在一个列表中可能会有不同类型的数据,比如图片、文本等等,那么用 Sealed Classes 如何表示。

1
2
3
4
kotlin复制代码sealed class ListItem {
class Text(val title: String, val content: String) : ListItem()
class Image(val url: String) : ListItem()
}

这是两个比较常见的例子,当然 Sealed Classes 强大不止于此,还有更多场景,等着一起来挖掘。

我们来看一下大神 Antonio Leiva 在这篇文章 Sealed classes in Kotlin: enums with super-powers 分享的一个比较有趣的例子,对 View 进行的一系列操作可以封装在 Sealed Classes 中,我们来看一下会有什么样的效果。

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码sealed class UiOp {
object Show: UiOp()
object Hide: UiOp()
class TranslateX(val px: Float): UiOp()
class TranslateY(val px: Float): UiOp()
}

fun execute(view: View, op: UiOp) = when (op) {
UiOp.Show -> view.visibility = View.VISIBLE
UiOp.Hide -> view.visibility = View.GONE
is UiOp.TranslateX -> view.translationX = op.px
is UiOp.TranslateY -> view.translationY = op.px
}

在 Sealed Classes 类中,我们定义了一系列 View 的操作 Show 、 Hide 、 TranslateX 、 TranslateY ,现在我们创建一个类,将这些对视图的操作整合在一起。

1
2
3
kotlin复制代码class Ui(val uiOps: List = emptyList()) {
operator fun plus(uiOp: UiOp) = Ui(uiOps + uiOp)
}

在 Ui 这个类中声明了一个 List 存储了所有的操作,并重写了 plus 操作符,关于 plus 操作符可以看之前的文章 为数不多的人知道的 Kotlin 技巧以及 原理解析,通过 plus 操作符将这些对视图的操作拼接在一起,这样不仅可以提高代码的可读性,而且使用起来也非常的方便,都定义好之后,我们来看一下如何使用这个类。

1
2
3
4
5
6
7
scss复制代码val ui = Ui() +
UiOp.Show +
UiOp.TranslateX(20f) +
UiOp.TranslateY(40f) +
UiOp.Hide

run(view, ui)

定义了一系列操作之后,然后通过 run 方法来执行这些操作,来看一下 run 方法的实现。

1
2
3
kotlin复制代码fun run(view: View, ui: Ui) {
ui.uiOps.forEach { execute(view, it) }
}

代码很简单,这里就不多做解释了,在 kotlin 中函数可以作为参数传递,可以将 run 方法传递给另一个函数或者一个类,并且这些操作完全可互换的,将它们结合在一起功能将非常强大。

Sealed Classes 强大不止于此,还有很多很多非常实用的场景,现在我对 Sealed Classes 的理解也非常有限,还不够灵活的使用它,我相信在更多项目,更多的场景,会看到更多实用的一些技巧。

Sealed Classes 原理

在这里我们还是使用上文中用到的例子,来分析 Sealed Classes 原理。

1
2
3
4
5
css复制代码sealed class Color {
object Red : Color()
object Green : Color()
object Blue : Color()
}

一起来分析一下反编译后的 Java 代码都做了什么。PS:Tools → Kotlin → Show Kotlin Bytecode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
swift复制代码... // 省略部分代码

@Metadata(
mv = {1, 1, 13},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u001a\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0002\b\u0004\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\b6\u0018\u00002\u00020\u0001:\u0003\u0003\u0004\u0005B\u0007\b\u0002¢\u0006\u0002\u0010\u0002\u0082\u0001\u0003\u0006\u0007\b¨\u0006\t"},
d2 = {"Lcom/hidhl/leetcode/test/saledvsemun/Color;", "", "()V", "Blue", "Green", "Red", "Lcom/hidhl/leetcode/test/saledvsemun/Color$Red;", "Lcom/hidhl/leetcode/test/saledvsemun/Color$Green;", "Lcom/hidhl/leetcode/test/saledvsemun/Color$Blue;", "Java-kotlin"}
)
public abstract class Color {
private Color() {
}

public Color(DefaultConstructorMarker $constructor_marker) {
this();
}
}
... // 省略部分代码

@Metadata 这个注解会出现在 Kotlin 编译器生成的任何类文件中,可以通过反射的方式获取 @Metadata 信息。参数名称都非常短,可以帮助减少 class 文件的大小。

@Metadata 存储了 Kotlin 主要的语法信息例如扩展函数、typealias 等等,这些信息都是由 kotlinc 编译器,并以注解的形式存放在 Java 的字节码中的,如果元数据被丢弃掉,运行在 JVM 上会抛出异常,那么如何才能确定它们之间的对应关系呢,其实就是通过 @Metadata 这个注解提供的信息。

正因为元数据不能被丢掉,R8 带了新的优化,将元数据信息记录在 R8 的内部数据结构中,当 R8 完成对第三库或者应用程序的优化和收缩时,它会为所有 Kotlin 类合成新的正确的 Kotlin 元数据,其目的就是为了减少应用程序的大小,目前我也在研究中,日后会分享。

而在本例中 @Metadata 保存了一个子类的列表,编译器在使用的时候会用到这些信息。正如你看到的 Sealed class 被编译成了 abstract class,它本身是不能被实例化,只能用它的子类实例化对象。

抽象类 Color 默认的构造方法被私有化了,所以在 Kotlin 1.1 之前,子类必须嵌套在 Sealed Classes 类中,后来放宽了要求,禁止在 Sealed Classes 所定义的文件外使用, Kotlin 是如何做到的呢?如果我们在 Sealed Classes 所定义的文件外使用会怎么样?

正如你所看到,会导致编译错误,那么为什么 Sealed Classes 可以在同文件内使用呢?来看一下反编译后的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
scala复制代码// sealed
sealed class Color
// 同文件中使用 sealed class
class Red : Color()


// 以下是反编译代码

... // 省略部分代码
public final class Red extends Color {
public Red() {
super((DefaultConstructorMarker)null);
}
}

... // 省略部分代码
public abstract class Color {
private Color() {
}

// $FF: synthetic method
public Color(DefaultConstructorMarker $constructor_marker) {
this();
}
}
... // 省略部分代码

可以看到 Red class 被编译成了 final class, Sealed class 被编译成了 abstract class,同时编译器生成了一个 公有 的构造方法,其他的类无法直接调用,只有 Kotlin 编译器可以使用,Red class 被编译成 final class,在其构造方法内调用了 Color class 公有 的构造方法,而这些都是 Kotlin 编译器帮我们做的。

  • 构造函数私有化,限制了子类必须嵌套在 Sealed Classes 类中
  • 编译器生成了一个 公有 的构造方法,在子类的构造方法中调用了父类 公有 的构造方法,而这些都是 Kotlin 编译器帮我们做的

总结

枚举的局限性

  • 限制枚举每个类型只允许有一个实例
  • 限制所有枚举常量使用相同的类型的值

抽象类的局限性

对于一个抽象类我们可以用一些子类去继承它,但是子类不是固定的,它可以随意扩展,同时也失去枚举常量受限性。

枚举作为单例的优点

是否只有一个实例 是否反序列化 是否是线程安全 是否是懒加载
是 是 是 否

Sealed Classes 是什么?

Sealed 是一个 abstract 类,它本身是不能被实例化,只能用它的子类实例化对象。Sealed 的构造方法私有化,禁止在 Sealed 所定义的文件外使用。

  • Sealed Classes 用于表示受限制的类层次结构
  • 从某种意义上说,Sealed Classes 是枚举类的扩展
  • 枚举的不同之处在于,每个枚举常量仅作为单个实例存在,而 Sealed Classes 的子类可以表示不同状态的实例。

在什么情况下使用枚举或者 Sealed?

  • 如果涉及反序列化创建对象的时候,建议使用枚举,因为 Java 规定,枚举的序列化和反序列化是有特殊定制的,因此禁用编译器使用 writeObject 、readObject 、readObjectNoData 、 writeReplace 、readResolve 等方法。
  • 如果你不需要多次实例化,也不需要不提供特殊行为,或者也不需要添加额外的信息,仅作为单个实例存在,这个时候使用枚举更加合适。
  • 其他情况下使用 Sealed Classes,在一定程度上可以使用 Sealed Classes 代替枚举

自动补全 when 语句下的所有分支

推荐给大家一个快捷键 Mac/Win/Linux:Alt + Enter 可以补全 when 语句下的所有分支,效果如下所示:

参考文献

  • Sealed classes in Kotlin: enums with super-powers
  • Sealed Classes
  • Kotlin Vocabulary | 密封类 sealed class
  • Kotlin Metadata

结语

公众号开通了:ByteCode , 欢迎小伙伴们前去查看 Android 10 系列源码,Jetpack ,Kotlin ,译文,LeetCode / 剑指 Offer / 国内外大厂算法题 等等一系列文章,如果对你有帮助,请帮我点个 star,感谢!!!,欢迎一起来学习,在技术的道路上一起前进。

正在建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,目前已经包含了 App Startup、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请仓库右上角帮我点个赞。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

正在写一系列的 Android 10 源码分析的文章,了解系统源码,不仅有助于分析问题,在面试过程中,对我们也是非常有帮助的,如果你同我一样喜欢研究 Android 源码,可以关注我 GitHub 上的 Android10-Source-Analysis,文章都会同步到这个仓库。

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构
  • 0xA07 Android 10 源码分析:Window 的类型 以及 三维视图层级分析
  • 更多……

Android 应用系列

  • 为数不多的人知道的 Kotlin 技巧以及 原理解析
  • Jetpack 最新成员 AndroidX App Startup 实践以及原理分析
  • Jetpack 成员 Paging3 实践以及源码分析(一)
  • Jetpack 新成员 Paging3 网络实践及原理分析(二)
  • Jetpack 新成员 Hilt 实践(一)启程过坑记
  • Jetpack 新成员 Hilt 实践之 App Startup(二)进阶篇
  • Jetpack 新成员 Hilt 与 Dagger 大不同(三)落地篇
  • 全方面分析 Hilt 和 Koin 性能
  • 神奇宝贝(PokemonGo) 眼前一亮的 Jetpack + MVVM 极简实战
  • Google 推荐在 MVVM 架构中使用 Kotlin Flow

精选译文

目前正在整理和翻译一系列精选国外的技术文章,不仅仅是翻译,很多优秀的英文技术文章提供了很好思路和方法,每篇文章都会有译者思考部分,对原文的更加深入的解读,可以关注我 GitHub 上的 Technical-Article-Translation,文章都会同步到这个仓库。

  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
  • [译][Google工程师] 详解 FragmentFactory 如何优雅使用 Koin 以及部分源码分析
  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin
  • [译][5k+] Kotlin 的性能优化那些事
  • [译] 解密 RxJava 的异常处理机制
  • [译][1.4K+ Star] Kotlin 新秀 Coil VS Glide and Picasso
  • 更多……

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程
  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

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

逐行解读Spring(四) - 万字长文讲透bean生命周期

发表于 2020-08-11

创作不易,转载请篇首注明 作者:掘金@小希子 + 来源链接~

如果想了解更多Spring源码知识,点击前往其余逐行解析Spring系列

一、前言

这些天一直在琢磨bean生命周期这一块应该怎么写,因为这一块的流程实在比较长,而且涉及到很多beanPostProcessor的埋点,很多我们常见的功能都是通过这些埋点来做的。

最终,我决定先用一篇博文,把bean生命周期的主流程较为**粗略(相对)**的讲一下。之后,会通过一系列博文对主流程中的一些细节、和一些常见的功能是怎么通过spring预留的beanPostProcessor埋点来实现的。感兴趣的同学可以自己选择查看。

(由于掘金对文章字数的限制,这篇博文被迫分为上下两篇,点击前往下一篇)

二、Spring容器的启动

发现其实到现在,我的这一系列spring博文,都没有好好讲过spring容器启动的过程(第一篇中也是直接给定位到了refresh方法)。正好上一篇讲的纯注解启动类AnnotationConfigApplicationContext,这里我们再回顾一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Test
public void test() {
// 我们平常使用AnnotationConfigApplicationContext的时候,只需要这样直接new出来就好了
applicationContext = new AnnotationConfigApplicationContext("com.xiaoxizi.spring");
// 然后就可以从容器中拿到bean对象了,说明其实new创建对象的时候,我们容器就做好启动初始化工作了~
MyAnnoClass myAnnoClass = applicationContext.getBean(MyAnnoClass.class);
System.out.println(myAnnoClass);
}

// AnnotationConfigApplicationContext的构造器
public AnnotationConfigApplicationContext(String... basePackages) {
this();
// 扫描目标包,收集并注册beanDefinition,上一篇具体讲过,这里就不赘述了
scan(basePackages);
// 这里就调用到我们大名鼎鼎的refresh方法啦
refresh();
}

我们看一下这个容器启动的核心方法refresh,这个方法的逻辑是在AbstractApplicationContext类中的,也是一个典型的模板方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
java复制代码public void refresh() throws BeansException, IllegalStateException {
synchronized (this.startupShutdownMonitor) {
// 一些准备工作,主要是一下状态的设置事件容器的初始化
prepareRefresh();

// 获取一个beanFactory,这个方法里面调用了一个抽象的refreshBeanFactory方法
// 我们的xml就是在这个入口里解析的,具体的流程有在之前的博文分析过
ConfigurableListableBeanFactory beanFactory = obtainFreshBeanFactory();

// 把拿到的beanFactory做一些准备,这里其实没啥逻辑,同学们感兴趣的可以看下
// 但是这个方法也是一个protected的方法,
// 也就是说我们如果实现自己的spring启动类/或者spring团队需要写一个新的spring启动类的时候
// 是可以在beanFactory获取之后做一些事情的,算是一个钩子
prepareBeanFactory(beanFactory);

try {
// 这也是一个钩子,在处理beanFactory前允许子类做一些事情
postProcessBeanFactory(beanFactory);

// 实例化并且调用factoryPostProcessor的方法,
// 我们@Compoment等注解的收集处理主要就是在这里做的
// 有一个ConfigurationClassPostProcessor专门用来做这些注解支撑的工作
// 这里的逻辑之前也讲过了
// 那么其实我们可以说,到这里为止,我们的beanDefinition的收集(注解/xml/其他来源...)
// 、注册(注册到beanFactory的beanDefinitionMap、beanDefinitionNames)容器
// 工作基本就全部完成了
invokeBeanFactoryPostProcessors(beanFactory);

// 从这里开始,我们就要专注bean的实例化了
// 所以我们需要先实例化并注册所有的beanPostProcessor
// 因为beanPostProcessor主要就是在bean实例化过程中,做一些附加操作的(埋点)
// 这里的流程也不再讲了,感兴趣的同学可以自己看一下,
// 这个流程基本跟FactoryPostProcessor的初始化是一样的,
// 排序,创建实例,然后放入一个list --> AbstractBeanFactory#beanPostProcessors
registerBeanPostProcessors(beanFactory);

// 初始化一些国际化相关的组件,这一块我没有去详细了解过(主要是暂时用不到...)
// 之后如果有时间也可以单独拉个博文来讲吧
initMessageSource();

// 初始化事件多播器,本篇不讲
initApplicationEventMulticaster();

// 也是个钩子方法,给子类创建一下特殊的bean
onRefresh();

// 注册事件监听器,本篇不讲
registerListeners();

// !!!实例化所有的、非懒加载的单例bean
// Instantiate all remaining (non-lazy-init) singletons.
finishBeanFactoryInitialization(beanFactory);

// 初始化结束,清理资源,发送事件
finishRefresh();
}
catch (BeansException ex) {
// 销毁已经注册的单例bean
destroyBeans();
// 修改容器状态
cancelRefresh(ex);
// Propagate exception to caller.
throw ex;
}

finally {
// Reset common introspection caches in Spring's core, since we
// might not ever need metadata for singleton beans anymore...
resetCommonCaches();
}
}
}

其实说白了,我们spring容器的启动,主要就是要把那些非懒加载的单例bean给实例化,并且管理起来。

三、bean实例化

1. 哪些bean需要在启动的时候实例化?

刚刚refresh方法中,我们有看到finishBeanFactoryInitialization方法是用来实例化bean的,并且源码中的英文也说明了,说是要实例化所以剩余的非懒加载的单例bean,那么实际情况真的如此么?我们跟源码看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
java复制代码protected void finishBeanFactoryInitialization(ConfigurableListableBeanFactory beanFactory) {
// skip .. 我把前面的非主流程的跳过了
// Instantiate all remaining (non-lazy-init) singletons.
beanFactory.preInstantiateSingletons();
}

// DefaultListableBeanFactory#preInstantiateSingletons
public void preInstantiateSingletons() throws BeansException {
// 我们之前注册beanDefinition的时候,有把所有的beanName收集到这个beanDefinitionNames容器
// 这里我们就用到了
List<String> beanNames = new ArrayList<>(this.beanDefinitionNames);

// 循环所有的已注册的beanName
for (String beanName : beanNames) {
// 获取合并后的beanDefinition,简单来讲,我们的beanDefinition是可以存在继承关系的
// 比如xml配置从的parent属性,这种时候,我们需要结合父子beanDefinition的属性,生成一个新的
// 合并的beanDefinition,子beanDefinition中的属性会覆盖父beanDefinition的属性,
// 并且这是一个递归的过程(父还可以有父),不过这个功能用的实在不多,就不展开了,
// 同学们有兴趣可以自行看一下,这里可以就理解为拿到对应的beanDefinition就好了
RootBeanDefinition bd = getMergedLocalBeanDefinition(beanName);
// 非抽象(xml有一个abstract属性,而不是说这个类不是一个抽象类)、单例的、非懒加载的才需要实例化
if (!bd.isAbstract() && bd.isSingleton() && !bd.isLazyInit()) {
if (isFactoryBean(beanName)) {
// 这里是处理factoryBean的,暂时不讲,之后再专门写博文
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<Boolean>)
((SmartFactoryBean<?>) factory)::isEagerInit,
getAccessControlContext());
}
else {
isEagerInit = (factory instanceof SmartFactoryBean &&
((SmartFactoryBean<?>) factory).isEagerInit());
}
if (isEagerInit) {
getBean(beanName);
}
}
}
else {
// !!!我们正常普通的bean会走到这个流程,这里就把这个bean实例化并且管理起来的
// 这里是获取一个bean,如果获取不到,则创建一个
getBean(beanName);
}
}
}

// 所以的bean实例化之后,还会有一些处理
for (String beanName : beanNames) {
// 获取到这个bean实例
Object singletonInstance = getSingleton(beanName);
// 如果bean实现了SmartInitializingSingleton接口
if (singletonInstance instanceof SmartInitializingSingleton) {
final SmartInitializingSingleton smartSingleton = (SmartInitializingSingleton) singletonInstance;
// 会调用它的afterSingletonsInstantiated方法
// 这是最外层的一个钩子了,平常其实用的不多
// 不过@Listener的发现是在这里做的
smartSingleton.afterSingletonsInstantiated();
}
}
}

可以看到,原来是非抽象(xml有一个abstract属性,而不是说这个类不是一个抽象类)、单例的、非懒加载的bean才会在spring容器启动的时候实例化,spring老哥们,你们的注释打错了呀(抠一下字眼就很开心)~

2. 使用getBean从beanFactory获取bean

刚刚有说到,调用getBean方法的时候,会先尝试中spring容器中获取这个bean,获取不到的时候则会创建一个,现在我们就来梳理一下这个流程:

1
2
3
4
5
6
7
8
java复制代码public Object getBean(String name) throws BeansException {
// 调用了doGetBean
// 说一下这种方式吧,其实我们能在很多框架代码里看到这种方式
// 就是会有一个参数最全的,可以最灵活使用的方法,用来处理我们的业务
// 然后会对不同的使用方,提供一些便于使用的类似于门面的方法,这些方法会简化一些参数,使用默认值填充
// 或者实际业务可以很灵活,但是不打算完全开放给使用方的时候,也可以使用类似的模式
return doGetBean(name, null, null, false);
}

getBean->doGetBean是我们beanFactory对外提供的获取bean的接口,只是说我们初始化spring容器的时候会为所有单例的beanDefinition调用getBean方法实例化它们定义的bean而已,所以它的的逻辑并不仅仅是为spring容器初始化定义的,我们也需要带着这个思维去看这个方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
java复制代码protected <T> T doGetBean(final String name, @Nullable final Class<T> requiredType,
@Nullable final Object[] args, boolean typeCheckOnly) throws BeansException {
// 转换一下beanName,暂时不看,之后统一讲
final String beanName = transformedBeanName(name);
Object bean;

// 看一下这个bean是否已经实例化了,如果实例化了这里能直接拿到
// 这个方法涉及到spring bean的3级缓存,之后会开一篇博客细讲
Object sharedInstance = getSingleton(beanName);
if (sharedInstance != null && args == null) {
// 通过这个bean实例获取用户真正需要的bean实例
// 有点绕,其实这里主要是处理当前bean实现了FactoryBean接口的情况的
bean = getObjectForBeanInstance(sharedInstance, name, beanName, null);
}
else {
// 当前线程下的,循环依赖检测,如果当前bean已经在创建中,这里又进来创建了,说明是循环依赖了
// 会直接报错,代码逻辑也很简单,这里主要是一个TheadLocal持有了一个set,
// 可以认为是一个快速失败检测,和后面的全局循环依赖检测不是一个容器
// 容器是 prototypesCurrentlyInCreation
if (isPrototypeCurrentlyInCreation(beanName)) {
throw new BeanCurrentlyInCreationException(beanName);
}


BeanFactory parentBeanFactory = getParentBeanFactory();
if (parentBeanFactory != null && !containsBeanDefinition(beanName)) {
// 如果父容器不为空且当前容器没有这个beanName对应的beanDefinition
// 则尝试从父容器获取(因为当期容器已经确定没有了)
// 下面就是调用父容器的getBean了
String nameToLookup = originalBeanName(name);
if (parentBeanFactory instanceof AbstractBeanFactory) {
return ((AbstractBeanFactory) parentBeanFactory).doGetBean(
nameToLookup, requiredType, args, typeCheckOnly);
}
else if (args != null) {
return (T) parentBeanFactory.getBean(nameToLookup, args);
}
else if (requiredType != null) {
return parentBeanFactory.getBean(nameToLookup, requiredType);
}
else {
return (T) parentBeanFactory.getBean(nameToLookup);
}
}
// 如果不是只检测类型是否匹配的话,这里要标记bean已创建(因为马上就要开始创建了)
if (!typeCheckOnly) {
markBeanAsCreated(beanName);
}

try {
final RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
checkMergedBeanDefinition(mbd, beanName, args);
// 拿到这个bean的所有依赖的bean
String[] dependsOn = mbd.getDependsOn();
if (dependsOn != null) {
// 如果依赖不为空,需要先循环实例化依赖
for (String dep : dependsOn) {
if (isDependent(beanName, dep)) {
throw new BeanCreationException(...);
}
registerDependentBean(dep, beanName);
try {
getBean(dep);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(...);
}
}
}

// 这里开始真正创建bean实例的流程了
if (mbd.isSingleton()) {
// 如果是单例的bean(当然我们启动的时候会实例化的也就是单例bean了),这里会进行创建
// 注意这里也是一个getSingleton方法,跟之前那个getSingleton方法差不多,不过这里是
// 如果获取不到就会使用这个lamdba的逻辑创建一个,
// 也就是说我的的createBean方法是真正创建bean实例的方法,这里我们之后会重点看
sharedInstance = getSingleton(beanName, () -> {
try {
return createBean(beanName, mbd, args);
}
catch (BeansException ex) {
destroySingleton(beanName);
throw ex;
}
});
// 通过这个bean实例获取用户真正需要的bean实例
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}
else if (mbd.isPrototype()) {
// 如果是多例的bean
// 那么每次获取都是创建一个新的bean实例
Object prototypeInstance = null;
try {
beforePrototypeCreation(beanName);
// 可以看到这里直接去调用createBean了
prototypeInstance = createBean(beanName, mbd, args);
}
finally {
afterPrototypeCreation(beanName);
}
// 这里逻辑还是一样的
bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
}
else {
// spring是允许我们自定义scope的,这里是自定义scope的逻辑
// 需要注意的是,spring mvc 的 session、request那些scope也是走这里的逻辑的
// 这里感兴趣的同学可以自行看下,暂时不讲
String scopeName = mbd.getScope();
final Scope scope = this.scopes.get(scopeName);
if (scope == null) {
throw new IllegalStateException(...);
}
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(...);
}
}
}
catch (BeansException ex) {
cleanupAfterBeanCreationFailure(beanName);
throw ex;
}
}

// 这里是类型转换的逻辑,getBean是有可以传类型的重载方法的
// 不过我们初始化的时候不会走到这个逻辑来,感兴趣的同学可以自行看
if (requiredType != null && !requiredType.isInstance(bean)) {
try {
T convertedBean = getTypeConverter().convertIfNecessary(bean, requiredType);
if (convertedBean == null) {
throw new BeanNotOfRequiredTypeException(...);
}
return convertedBean;
}
catch (TypeMismatchException ex) {
throw new BeanNotOfRequiredTypeException(...);
}
}
// 返回获取到的bean
return (T) bean;
}

我们继续看一下单例bean的创建逻辑,即:

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

我们看一下这个getSingleton方法,需要注意的是,这个方法在DefaultSingletonBeanRegistry类中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
java复制代码/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
/** Names of beans that are currently in creation. */
private final Set<String> singletonsCurrentlyInCreation = Collections.newSetFromMap(new ConcurrentHashMap<>(16));

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
synchronized (this.singletonObjects) {
// 可以看到,我们先从singletonObjects通过beanName获取实例
// 这是不是说明singletonObjects就是spring用来存放所以单例bean的容器呢?可以说是的。
Object singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
// 跳过了一个spring单例bean容器状态判断,
// 如果spring单例bean容器正在销毁时不允许继续创建单例bean的

// 创建容器之前的钩子,这里默认会把bean那么加入到一个正在创建的beanNameSet,
// 如果加入失败就代表是循环依赖了。
// 检测容器是 singletonsCurrentlyInCreation
beforeSingletonCreation(beanName);
boolean newSingleton = false;
boolean recordSuppressedExceptions = (this.suppressedExceptions == null);
if (recordSuppressedExceptions) {
this.suppressedExceptions = new LinkedHashSet<>();
}
try {
// 这里就是调用传进来的lamdba了
// 也就是调用了createBean创建了bean实例
singletonObject = singletonFactory.getObject();
newSingleton = true;
}
catch (IllegalStateException ex) {
// Has the singleton object implicitly appeared in the meantime ->
// if yes, proceed with it since the exception indicates that state.
singletonObject = this.singletonObjects.get(beanName);
if (singletonObject == null) {
throw ex;
}
}
catch (BeanCreationException ex) {
if (recordSuppressedExceptions) {
for (Exception suppressedException : this.suppressedExceptions) {
ex.addRelatedCause(suppressedException);
}
}
throw ex;
}
finally {
if (recordSuppressedExceptions) {
this.suppressedExceptions = null;
}
// 从正在创建的beanNameSet移除
afterSingletonCreation(beanName);
}
// 如果成功创建了bean实例,需要加入singletonObjects容器
// 这样下次再获取就能直接中容器中拿了
if (newSingleton) {
addSingleton(beanName, singletonObject);
}
}
return singletonObject;
}
}

可以看到,这个getSingleton方法就是先从singletonObjects获取bean实例,获取不到就创建一个,其中还加了一些循环依赖的检测逻辑。

3. createBean,真正的bean初始化逻辑

我们说createBean方法是真正的bean初始化逻辑,但是这个初始化不仅仅是说创建一个实例就好了,还涉及到一些校验,以及类里的依赖注入、初始化方法调用等逻辑,我们现在就一起来简单看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {

RootBeanDefinition mbdToUse = mbd;
// 获取bean的类型
Class<?> resolvedClass = resolveBeanClass(mbd, beanName);
if (resolvedClass != null && !mbd.hasBeanClass() && mbd.getBeanClassName() != null) {
mbdToUse = new RootBeanDefinition(mbd);
mbdToUse.setBeanClass(resolvedClass);
}

// Prepare method overrides.
try {
// 这里对beanDefinition中的MethodOverrides做一些准备
// 主要是梳理一下所有重写方法(xml<replaced-method><lockup-method>标签对应的属性)
// 看下这些方法是否是真的有重载方法,没有重载的话会设置overloaded=false,
// 毕竟有些人配置的时候即使没有重载方法也会使用<replaced-method>标签
// (这功能我确实也没用过。。
mbdToUse.prepareMethodOverrides();
}
catch (BeanDefinitionValidationException ex) {
throw new BeanDefinitionStoreException(...);
}

try {
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
// 给BeanPostProcessors一个机会,在我们的bean实例化之前返回一个代理对象,即完全不走spring的实例化逻辑
// 也是个BeanPostProcessors的钩子,就是循环beanPostProcessors然后调用的逻辑
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}
}
catch (Throwable ex) {
throw new BeanCreationException(...);
}

try {
// 这里是spring真正bean实例化的地方了
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
// 获取到了直接返回
return beanInstance;
}
// 跳过异常处理
}

3.0. doCreateBean是如何实例化一个bean的?

刚刚有说到,doCreateBean是我们spring真正的实例化bean的逻辑,那我们一起来看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
java复制代码protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
throws BeanCreationException {

// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
// 创建bean实例
instanceWrapper = createBeanInstance(beanName, mbd, args);
}
final Object bean = instanceWrapper.getWrappedInstance();
Class<?> beanType = instanceWrapper.getWrappedClass();
if (beanType != NullBean.class) {
mbd.resolvedTargetType = beanType;
}

synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
// 调用一个BeanPostProcessor的钩子方法,这里调用的是
// MergedBeanDefinitionPostProcessor#postProcessMergedBeanDefinition
// 这个钩子方法是在bean实例创建之后,依赖注入之前调用的,需要注意的是
// @Autowired和@Value注解的信息收集-AutowiredAnnotationBeanPostProcessor
// @PostConstruct、@PreDestroy注解信息收集-CommonAnnotationBeanPostProcessor
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(...);
}
mbd.postProcessed = true;
}
}

// 这一部分是使用3级缓存来解决循环依赖问题的,之后再看
boolean earlySingletonExposure = (mbd.isSingleton() && this.allowCircularReferences &&
isSingletonCurrentlyInCreation(beanName));
if (earlySingletonExposure) {
// 加入三级缓存
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}


Object exposedObject = bean;
try {
// 依赖注入
populateBean(beanName, mbd, instanceWrapper);
// bean初始化-主要是调用一下初始化方法
exposedObject = initializeBean(beanName, exposedObject, mbd);
}
catch (Throwable ex) {
if (ex instanceof BeanCreationException && beanName.equals(((BeanCreationException) ex).getBeanName())) {
throw (BeanCreationException) ex;
}
else {
throw new BeanCreationException(...);
}
}
// 这里也算是循环依赖检测的,暂时不讲
if (earlySingletonExposure) {
Object earlySingletonReference = getSingleton(beanName, false);
if (earlySingletonReference != null) {
if (exposedObject == bean) {
exposedObject = earlySingletonReference;
}
else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) {
String[] dependentBeans = getDependentBeans(beanName);
Set<String> actualDependentBeans = new LinkedHashSet<>(dependentBeans.length);
for (String dependentBean : dependentBeans) {
if (!removeSingletonIfCreatedForTypeCheckOnly(dependentBean)) {
actualDependentBeans.add(dependentBean);
}
}
if (!actualDependentBeans.isEmpty()) {
throw new BeanCurrentlyInCreationException(...);
}
}
}
}
try {
// 如果是单例bean,还会注册销毁事件
registerDisposableBeanIfNecessary(beanName, bean, mbd);
}
catch (BeanDefinitionValidationException ex) {
throw new BeanCreationException(...);
}

return exposedObject;
}

可以看到,我们的doCreateBean大致做了5件事:

  1. 创建bean实例
  2. 调用beanPostProcessor的埋点方法
  3. 注入当前类依赖的bean
  4. 调用当前bean的初始化方法
  5. 注册当前bean的销毁逻辑

接下来我们来详细看一下这些流程

3.1. createBeanInstance创建bean实例

大家平常是怎么实例化一个类呢?是直接使用构造器new出来一个?还是使用工厂方法获取?

很显然,spring也是支持这两种方式的,如果同学们还记得bean标签的解析的话,那应该还会记得spring除了有提供使用构造器实例化bean的constructor-arg标签外,还提供了factory-bean和factory-method属性来配置使用工厂方法来实例化bean。

并且之前在讲ConfigurationClassPostProcessor的时候,我们讲到@bean标签的时候,也有看到,对于@bean标签的处理,就是新建一个beanDefinition,并把当前的配置类和@Bean修饰的方法分别塞入了这个beanDefinition的factoryBeanName和factoryMethodName属性(可以空降ConfigurationClassBeanDefinitionReader#loadBeanDefinitionsForBeanMethod)。

接下来我们就来看一下createBeanInstance的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
java复制代码protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) {
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());
}
// 如果beanDefinition里有instanceSupplier,直接通过instanceSupplier拿就行了
// 这种情况我们就不重点讲了,其实跟工厂方法的方式也差不多
Supplier<?> instanceSupplier = mbd.getInstanceSupplier();
if (instanceSupplier != null) {
return obtainFromSupplier(instanceSupplier, beanName);
}

// 如果工厂方法不为空,就使用工厂方法实例化
if (mbd.getFactoryMethodName() != null) {
return instantiateUsingFactoryMethod(beanName, mbd, args);
}

// 这里是对非单例bean做的优化,如果创建过一次了,
// spring会把相应的构造器或者工厂方法存到resolvedConstructorOrFactoryMethod字段
// 这样再次创建这个类的实例的时候就可以直接使用resolvedConstructorOrFactoryMethod创建了
boolean resolved = false;
boolean autowireNecessary = false;
if (args == null) {
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);
}
}

// 如果beanDefinition没有构造器信息,则通过beanPostProcessor选择一个
Constructor<?>[] ctors = determineConstructorsFromBeanPostProcessors(beanClass, beanName);
// 1.如果通过beanPostProcessor找到了合适的构造器
// 2.或者autowireMode==AUTOWIRE_CONSTRUCTOR(这个xml配置的时候也可以指定的)
// 3.或者有配置构造器的参数(xml配置constructor-arg标签)
// 4.获取实例化bean是直接传进来了参数
// 只要符合上面四种情况之一,我们就会通过autowireConstructor方法来实例化这个bean
if (ctors != null || mbd.getResolvedAutowireMode() == AUTOWIRE_CONSTRUCTOR ||
mbd.hasConstructorArgumentValues() || !ObjectUtils.isEmpty(args)) {
// 需要构造器方式注入的bean的实例化
return autowireConstructor(beanName, mbd, ctors, args);
}

// 这里主要逻辑是兼容kotlin的,我们暂时不看
ctors = mbd.getPreferredConstructors();
if (ctors != null) {
// 需要构造器方式注入的bean的实例化
return autowireConstructor(beanName, mbd, ctors, null);
}

// 不需要特殊处理的话,就直接使用无参构造器了
return instantiateBean(beanName, mbd);
}

具体的instantiateUsingFactoryMethod、autowireConstructo方法这边就不带同学们看了,因为里面涉及到的一些参数注入的逻辑比较复杂,之后会单独开一篇博客来讲。

而拿到具体的参数之后,其实不管是构造器还是工厂方法实例化,都是很清晰的,直接反射调用就好了。

instantiateBean就是获取无参构造器然后反射实例化的一个逻辑,逻辑比较简单,这边也不跟了。

3.1.1. 通过determineConstructorsFromBeanPostProcessors方法选择构造器

这边主要带大家跟一下determineConstructorsFromBeanPostProcessors这个方法,因为我们现在大部分都是使用注解来声明bean的,而如果大家在使用注解的时候也是使用构造器的方式注入的话,那么是通过这个方法来拿到相应的构造器的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码protected Constructor<?>[] determineConstructorsFromBeanPostProcessors(@Nullable Class<?> beanClass, String beanName)
throws BeansException {
if (beanClass != null && hasInstantiationAwareBeanPostProcessors()) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof SmartInstantiationAwareBeanPostProcessor) {
SmartInstantiationAwareBeanPostProcessor ibp = (SmartInstantiationAwareBeanPostProcessor) bp;
Constructor<?>[] ctors = ibp.determineCandidateConstructors(beanClass, beanName);
if (ctors != null) {
// 一旦拿到构造器就返回了
return ctors;
}
}
}
}
return null;
}

可以看到,还是通过beanPostProcessor的埋点来做的,这里是调用的SmartInstantiationAwareBeanPostProcessor#determineCandidateConstructors,这里也不给大家卖关子了,我们真正支撑注解方式,选择构造器的逻辑在AutowiredAnnotationBeanPostProcessor中,有没有感觉这个类好像也有点熟悉?

1
2
3
4
5
6
7
8
9
10
java复制代码public static Set<BeanDefinitionHolder> registerAnnotationConfigProcessors(
BeanDefinitionRegistry registry, @Nullable Object source) {
// ...
if (!registry.containsBeanDefinition(AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME)) {
RootBeanDefinition def = new RootBeanDefinition(AutowiredAnnotationBeanPostProcessor.class);
def.setSource(source);
beanDefs.add(registerPostProcessor(registry, def, AUTOWIRED_ANNOTATION_PROCESSOR_BEAN_NAME));
}
// ...
}

也是有在调用AnnotationConfigUtils#registerAnnotationConfigProcessors方法的时候有注入哦~

从名称可以看到,这个beanPostProcessor是应该是用来处理@Autowired注解的,有同学要说了,这不是属性注入的注解么,跟构造器有什么关系?那我们已一个构造器注入的bean来举例:

1
2
3
4
5
6
7
8
java复制代码@Service
public class ConstructorAutowiredBean {
private Student student;
@Autowired
public ConstructorAutowiredBean(Student student) {
this.student = student;
}
}

大部分同学可能忘了,@Autowired是可以用来修饰构造器的,被@Autowired修饰的构造器的参数也将会中spring容器中获取(这么说可能不太准确,大家明白我的意思就好,就是说构造器注入的意思…)。

不过,其实我们平常即使使用构造器注入也不打@Autowired注解也是没问题的,这其实也是AutowiredAnnotationBeanPostProcessor获取构造器时的一个容错逻辑,我们一起看一下代码就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
java复制代码public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName)
throws BeanCreationException {
// 整个方法分为了两个部分
// 第一部分是收集这个类上被@Lookup修饰的方法
// 这个注解的功能和我们xml的lookup-method标签是一样的
// 而收集部分也是一样的封装到了一个MethodOverride并且加入到beanDefinition里面去了
// 虽然这部分工作(@Lookup注解的收集工作)是应该放在bean创建之前(有MethodOverride的话会直接生成代理实例)
// 但是放在当前这个determineCandidateConstructors方法里我还是觉得不太合适
// 毕竟跟方法名的语意不符,不过好像确实没有其它合适的钩子了,可能也只能放这了
if (!this.lookupMethodsChecked.contains(beanName)) {
if (AnnotationUtils.isCandidateClass(beanClass, Lookup.class)) {
try {
Class<?> targetClass = beanClass;
do {
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
// 循环处理所有的方法,获取@Lookup注解并封装信息
Lookup lookup = method.getAnnotation(Lookup.class);
if (lookup != null) {
LookupOverride override = new LookupOverride(method, lookup.value());
try {
RootBeanDefinition mbd = (RootBeanDefinition)
this.beanFactory.getMergedBeanDefinition(beanName);
mbd.getMethodOverrides().addOverride(override);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(...);
}
}
});
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);

}
catch (IllegalStateException ex) {
throw new BeanCreationException(...);
}
}
this.lookupMethodsChecked.add(beanName);
}

// 这里开始是选择构造器的逻辑了
// 先从缓存拿...这些也是为非单例bean设计的,这样就不用每次进来都走选择构造器的逻辑了
Constructor<?>[] candidateConstructors = this.candidateConstructorsCache.get(beanClass);
if (candidateConstructors == null) {
synchronized (this.candidateConstructorsCache) {
candidateConstructors = this.candidateConstructorsCache.get(beanClass);
if (candidateConstructors == null) {
Constructor<?>[] rawCandidates;
try {
// 获取当前类的所有的构造器
rawCandidates = beanClass.getDeclaredConstructors();
}
catch (Throwable ex) {
throw new BeanCreationException(...);
}
// 这个列表存符合条件的构造器
List<Constructor<?>> candidates = new ArrayList<>(rawCandidates.length);
Constructor<?> requiredConstructor = null;
Constructor<?> defaultConstructor = null;
// 这个primaryConstructor我们不管,是兼容kotlin的逻辑
Constructor<?> primaryConstructor = BeanUtils.findPrimaryConstructor(beanClass);
int nonSyntheticConstructors = 0;
for (Constructor<?> candidate : rawCandidates) {
// 循环每个构造器
if (!candidate.isSynthetic()) {
// 这个判断是判断不是合成的构造器,同学们想了解这个Synthetic可以自行查一下
// 这边就不展开了,这个主意是和内部类有关,Synthetic的构造器是编译器自行生成的
nonSyntheticConstructors++;
}
else if (primaryConstructor != null) {
continue;
}
// 找一下构造器上有没有目标注解,说白了就是找@Autowired注解
MergedAnnotation<?> ann = findAutowiredAnnotation(candidate);
if (ann == null) {
// 如果找不到,这里认为可能是因为当前这个class是spring生成的cglib代理类
// 所以这里尝试拿一下用户的class
Class<?> userClass = ClassUtils.getUserClass(beanClass);
// 如果用户的class和之前的beanClass不一致,说明之前那个class真的是代理类了
if (userClass != beanClass) {
try {
// 这个时候去userClass拿一下对应的构造器
Constructor<?> superCtor =
userClass.getDeclaredConstructor(candidate.getParameterTypes());
// 再在用户的构造器上找一下注解
ann = findAutowiredAnnotation(superCtor);
}
catch (NoSuchMethodException ex) {
}
}
}
if (ann != null) {
// 这里是找到注解了
if (requiredConstructor != null) {
// 这个分支直接报错了,意思是之前已经如果有被@Autowired注解修饰了的构造器
// 且注解中的Required属性为true的时候,
// 就不允许再出现其他被@Autowired注解修饰的构造器了
// 说明@Autowired(required=true)在构造器上的语言是必须使用这个构造器
throw new BeanCreationException(...);
}
// 拿注解上的required属性
boolean required = determineRequiredStatus(ann);
if (required) {
if (!candidates.isEmpty()) {
// 这里也是一样的,有required的构造器,就不预约有其他被
// @Autowired注解修饰的构造器了
throw new BeanCreationException(...);
}
// requiredConstructor只能有一个
requiredConstructor = candidate;
}
// 符合条件的构造器加入列表-即有@Autowired的构造器
candidates.add(candidate);
}
else if (candidate.getParameterCount() == 0) {
// 如果构造器的参数为空,那就是默认构造器了
defaultConstructor = candidate;
}
}

if (!candidates.isEmpty()) {
// 如果被@Autowired修饰的构造器不为空
if (requiredConstructor == null) {
// 如果没有requiredConstructor,就把默认构造器加入列表
// 如果有requiredConstructor,实际上candidates中就只有一个构造器了
if (defaultConstructor != null) {
candidates.add(defaultConstructor);
}
else if (candidates.size() == 1 && logger.isInfoEnabled()) {
logger.info(...);
}
}
// 然后把candidates列表赋值给返回值
candidateConstructors = candidates.toArray(new Constructor<?>[0]);
}
else if (rawCandidates.length == 1 && rawCandidates[0].getParameterCount() > 0) {
// 如果当前类总共也只有一个构造器,并且这个构造器是需要参数的
// 那就直接使用这个构造器了
// 这就是为什么我们平常构造器注入不打@Autowired注解也可以的原因
candidateConstructors = new Constructor<?>[] {rawCandidates[0]};
}
// 以下主要是处理primaryConstructor的,我们就不读了
else if (nonSyntheticConstructors == 2 && primaryConstructor != null &&
defaultConstructor != null && !primaryConstructor.equals(defaultConstructor)) {
candidateConstructors = new Constructor<?>[] {primaryConstructor, defaultConstructor};
}
else if (nonSyntheticConstructors == 1 && primaryConstructor != null) {
candidateConstructors = new Constructor<?>[] {primaryConstructor};
}
else {
// 都不满足,就是空数组了
candidateConstructors = new Constructor<?>[0];
}
// 处理完之后放入缓存
this.candidateConstructorsCache.put(beanClass, candidateConstructors);
}
}
}
// 之所以上面解析的时候,没找到构造器也是使用空数组而不是null
// 就是为了从缓存拿的时候,能区分究竟是没处理过(null),还是处理了但是找不到匹配的(空数组)
// 避免缓存穿透
return (candidateConstructors.length > 0 ? candidateConstructors : null);
}

如果能找到合适的构造器的话,就可以直接通过反射实例化对象了~

3.2. 通过beanPostProcessor埋点来收集注解信息

通过createBeanInstance创建完类的实例之后,注入属性之前,我们有一个beanPostProcessor的埋点方法的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码synchronized (mbd.postProcessingLock) {
if (!mbd.postProcessed) {
try {
applyMergedBeanDefinitionPostProcessors(mbd, beanType, beanName);
}
catch (Throwable ex) {
throw new BeanCreationException(...);
}
mbd.postProcessed = true;
}
}
protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, Class<?> beanType, String beanName) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof MergedBeanDefinitionPostProcessor) {
MergedBeanDefinitionPostProcessor bdp = (MergedBeanDefinitionPostProcessor) bp;
bdp.postProcessMergedBeanDefinition(mbd, beanType, beanName);
}
}
}

由于这个埋点中有一部分对注解进行支撑的逻辑还挺重要的,所以这里单独拿出来讲一下。

3.2.1. CommonAnnotationBeanPostProcessor收集@PostConstruct、@PreDestroy、@Resource信息

CommonAnnotationBeanPostProcessor也是AnnotationConfigUtils#registerAnnotationConfigProcessors方法注入的,这里我就不带大家再看了。由于CommonAnnotationBeanPostProcessor实现了MergedBeanDefinitionPostProcessor接口,所以在这个埋点中也会被调用到,我们来看一下这个逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBeanPostProcessor
implements InstantiationAwareBeanPostProcessor, BeanFactoryAware, Serializable{
// 构造器
public CommonAnnotationBeanPostProcessor() {
setOrder(Ordered.LOWEST_PRECEDENCE - 3);
// 给两个关键字段设置了
setInitAnnotationType(PostConstruct.class);
setDestroyAnnotationType(PreDestroy.class);
ignoreResourceType("javax.xml.ws.WebServiceContext");
}

@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
// 这里调用了父类的方法,正真的收集`@PostConstruct`、`@PreDestroy`注解的逻辑是在这里做的
super.postProcessMergedBeanDefinition(beanDefinition, beanType, beanName);
// 这里就是收集@Resource注解的信息啦
InjectionMetadata metadata = findResourceMetadata(beanName, beanType, null);
// 检查一下
metadata.checkConfigMembers(beanDefinition);
}
}
3.2.1.1. 生命周期注解@PostConstruct、@PreDestroy信息收集

我们先看一下父类收集生命周期注解的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
java复制代码public class InitDestroyAnnotationBeanPostProcessor
implements DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, PriorityOrdered, Serializable{
@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
// 寻找生命周期元数据
LifecycleMetadata metadata = findLifecycleMetadata(beanType);
// 对收集到的声明周期方法做一下校验处理
metadata.checkConfigMembers(beanDefinition);
}

private LifecycleMetadata findLifecycleMetadata(Class<?> clazz) {
if (this.lifecycleMetadataCache == null) {
// 没有开启缓存就直接拿构建生命周期元数据了
return buildLifecycleMetadata(clazz);
}
// 有开启缓存的话,就先从缓存找,找不到再构建,然后丢回缓存
LifecycleMetadata metadata = this.lifecycleMetadataCache.get(clazz);
if (metadata == null) {
synchronized (this.lifecycleMetadataCache) {
metadata = this.lifecycleMetadataCache.get(clazz);
if (metadata == null) {
// 构建
metadata = buildLifecycleMetadata(clazz);
this.lifecycleMetadataCache.put(clazz, metadata);
}
return metadata;
}
}
return metadata;
}

private LifecycleMetadata buildLifecycleMetadata(final Class<?> clazz) {
// 简单判断类上是不是一定没有initAnnotationType和destroyAnnotationType这两个注解修饰的方法
// 相当于快速失败
// 需要注意的是,当前场景下,这两个注解实例化的时候已经初始化为PostConstruct和PreDestroy了
if (!AnnotationUtils.isCandidateClass(clazz, Arrays.asList(this.initAnnotationType, this.destroyAnnotationType))) {
return this.emptyLifecycleMetadata;
}
// 用来储存类上所有初始化/销毁方法的容器
List<LifecycleElement> initMethods = new ArrayList<>();
List<LifecycleElement> destroyMethods = new ArrayList<>();
Class<?> targetClass = clazz;

do {
// 中间容器来储存当前类的初始化/销毁方法
final List<LifecycleElement> currInitMethods = new ArrayList<>();
final List<LifecycleElement> currDestroyMethods = new ArrayList<>();
// 循环类上的每一个方法
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
if (this.initAnnotationType != null && method.isAnnotationPresent(this.initAnnotationType)) {
// 如果方法被@PostConstruct注解修饰,包装成一个LifecycleElement
LifecycleElement element = new LifecycleElement(method);
// 加入收集初始化方法的中间容器
currInitMethods.add(element);
}
if (this.destroyAnnotationType != null && method.isAnnotationPresent(this.destroyAnnotationType)) {
// 如果方法被@PreDestroy注解修饰,包装成一个LifecycleElement
// 加入收集销毁方法的中间容器
currDestroyMethods.add(new LifecycleElement(method));
}
});
// 加入所有初始化/销毁方法的容器
// 需要注意的是,在整个循环过程中,
// 当前类的初始化方法都是加入初始化方法容器的头部
// 当前类的销毁方法都是加入销毁方法容器的尾部
// 所以可以推断,初始化方法调用的时候是从父类->子类调用
// 而销毁方法从子类->父类调用。
// 即 bean初始化->调用父类初始化方法->调用子类初始化方法->...->调用子类销毁方法->调用父类销毁方法->销毁bean
initMethods.addAll(0, currInitMethods);
destroyMethods.addAll(currDestroyMethods);
// 获取父类,循环处理所有父类上的初始化/销毁方法
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);
// 把当前类class对象+初始化方法列表+销毁方法列表封装成一个LifecycleMetadata对象
return (initMethods.isEmpty() && destroyMethods.isEmpty() ? this.emptyLifecycleMetadata :
new LifecycleMetadata(clazz, initMethods, destroyMethods));
}
}

看一下这个生命周期元数据LifecycleMetadata的结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
java复制代码private class LifecycleMetadata {
// 目标类
private final Class<?> targetClass;
// 目标类上收集到的初始化方法
private final Collection<LifecycleElement> initMethods;
// 目标类上收集到的销毁方法
private final Collection<LifecycleElement> destroyMethods;
// 检查、校验后的初始化方法
@Nullable
private volatile Set<LifecycleElement> checkedInitMethods;
// 检查、校验后的销毁方法
@Nullable
private volatile Set<LifecycleElement> checkedDestroyMethods;

public void checkConfigMembers(RootBeanDefinition beanDefinition) {
// 这是那个检查、校验方法
Set<LifecycleElement> checkedInitMethods = new LinkedHashSet<>(this.initMethods.size());
for (LifecycleElement element : this.initMethods) {
// 循环处理每个初始化方法
String methodIdentifier = element.getIdentifier();
// 判断是否是标记为外部处理的初始化方法,
// 如果是外部处理的方法的话,其实spring是不会管理这些方法的
if (!beanDefinition.isExternallyManagedInitMethod(methodIdentifier)) {
// 这里把当前方法注册到这个externallyManagedDestroyMethods
// 我猜想是方法签名相同的方法就不调用两次了
// 比如可能父类的方法打了@PostConstruct,子类重写之后也在方法上打了@PostConstruct
// 这两个方法都会被收集到initMethods,但是当然不应该调用多次
beanDefinition.registerExternallyManagedInitMethod(methodIdentifier);
// 加入了检查后的初始化方法列表,实际调用初始化方法时也是会调用这个列表
checkedInitMethods.add(element);
}
}
// 销毁方法的处理逻辑和初始化方法一样,我直接跳过了
this.checkedInitMethods = checkedInitMethods;
this.checkedDestroyMethods = checkedDestroyMethods;
}
}

到这里为止,其实我们CommonAnnotationBeanPostProcessor对生命周期注解的收集过程就完成了,其实主要是通过父类的模本方法,把被@PostConstruct、@PreDestroy修饰的方法的信息封装到了LifecycleMetadata。看完InitDestroyAnnotationBeanPostProcessor的逻辑之后,同学们会不会有实现一套自己的生命周期注解的冲动呢?毕竟写一个类继承一下然后在自己的类构造器中set一下initAnnotationType、destroyAnnotationType就可以了!

3.2.1.2. 依赖注入注解@Resource信息收集

刚刚有说道我们的findResourceMetadata方法是用来收集@Resource注解信息的,我们现在来看一下这里的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
java复制代码private InjectionMetadata findResourceMetadata(String beanName, final Class<?> clazz, @Nullable PropertyValues pvs) {
// 也是一个缓存逻辑
String cacheKey = (StringUtils.hasLength(beanName) ? beanName : clazz.getName());
InjectionMetadata metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
synchronized (this.injectionMetadataCache) {
metadata = this.injectionMetadataCache.get(cacheKey);
if (InjectionMetadata.needsRefresh(metadata, clazz)) {
if (metadata != null) {
metadata.clear(pvs);
}
// 构建逻辑
metadata = buildResourceMetadata(clazz);
this.injectionMetadataCache.put(cacheKey, metadata);
}
}
}
return metadata;
}

private InjectionMetadata buildResourceMetadata(final Class<?> clazz) {
// 这个方法除了收集@Resource注解之外,
// 其实还会收集@WebServiceRef和@EJB注解(如果你的项目有引入这些)
// 不过由于@WebServiceRef和@EJB我们现在基本也不用了(反正我没用过)
// 我这边就把相应的逻辑删除掉了,这样看也清晰点
// 而且这些收集逻辑也是一致的,最多只是说最后把注解信息封装到不同的子类型而已
// 快速失败检测
if (!AnnotationUtils.isCandidateClass(clazz, resourceAnnotationTypes)) {
return InjectionMetadata.EMPTY;
}
// 收集到注入元素
List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;

do {
// 这里套路其实跟收集生命周期注解差不多了
// 也是循环收集父类的
final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
// 循环处理每个属性
ReflectionUtils.doWithLocalFields(targetClass, field -> {
if (...) {...} // 其他注解的处理
else if (field.isAnnotationPresent(Resource.class)) {
// 静态属性不允许注入,当然其实@Autowired和@Value也是不允许的,
// 只是那边不会报错,只是忽略当前方法/属性而已
if (Modifier.isStatic(field.getModifiers())) {
throw new IllegalStateException(...);
}
if (!this.ignoredResourceTypes.contains(field.getType().getName())) {
// 不是忽略的资源就加入容器
// ejb那些就是封装成EjbRefElement
currElements.add(new ResourceElement(field, field, null));
}
}
});
// 循环处理每个方法,比如@Resource修饰的set方法啦(当然没规定要叫setXxx)
// 这里会循环当前类声明的方法和接口的默认(default)方法
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
// 这里是处理桥接方法的逻辑,桥接方法是编译器自行生成的方法。
// 主要跟泛型相关,这里也不多拓展了
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
}
// 由于这个工具类的循环是会循环到接口的默认方法的
// 这里这个判断是处理以下场景的:
// 接口有一个default方法,而当前类重写了这个方法
// 那如果子类重写的method循环的时候,这个if块能进去
// 接下来接口的相同签名的默认method进来时,
// ClassUtils.getMostSpecificMethod(method, clazz)会返回子类中重写的那个方法
// 这是就和当前方法(接口方法)不一致,就不会再进if块收集一遍了
if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
if (...) {...} // 其他注解的处理
else if (bridgedMethod.isAnnotationPresent(Resource.class)) {
if (Modifier.isStatic(method.getModifiers())) {
throw new IllegalStateException(...);
}
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length != 1) {
// 原来@Resource方法注入只支持一个参数的方法(set方法)
// 这个限制估计是规范定的
// @Autowired没有这个限制
throw new IllegalStateException(...);
}
if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) {
// 封装了一个属性描述符,这个主要用来加载方法参数的,暂时不展开
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
// 也封装成一个ResourceElement加入容器
currElements.add(new ResourceElement(method, bridgedMethod, pd));
}
}
}
});
// 每次都放到列表的最前面,说明是优先会注入父类的
elements.addAll(0, currElements);
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);
// 把当前类的class和收集到的注入元素封装成一个注入元数据
return InjectionMetadata.forElements(elements, clazz);
}

可以看到,其实跟生命周期那一块差不多,也是收集注解信息然后封装,只是这个注入元素的收集要同时收集属性和(set)方法而已,我们还是照常瞄一下这个数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码public class InjectionMetadata {
// 目标类--属性需要注入到哪个类
private final Class<?> targetClass;
// 注入元素
private final Collection<InjectedElement> injectedElements;
// 检查后的注入元素
@Nullable
private volatile Set<InjectedElement> checkedElements;
}

public abstract static class InjectedElement {
// Member是Method和Field的父类
protected final Member member;
// 通过这个属性区分是field还是method
protected final boolean isField;
// 属性描述符,如果是method会通过这个描述符获取入参
@Nullable
protected final PropertyDescriptor pd;
@Nullable
protected volatile Boolean skip;
}

获取到InjectionMetadata之后的metadata.checkConfigMembers逻辑,和生命周期那一块是一模一样的,这边就不跟了。

那么到这里为止我们CommonAnnotationBeanPostProcessor类在bean实例创建之后的埋点的逻辑就分析完了。

3.2.2. AutowiredAnnotationBeanPostProcessor收集@Autowired、@Value信息

AutowiredAnnotationBeanPostProcessor这个类的注册时机已经讲过很多遍了,也是AnnotationConfigUtils#registerAnnotationConfigProcessors方法注入的,这边我们直接看一下它的postProcessMergedBeanDefinition方法是如何收集注解信息的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
java复制代码public class AutowiredAnnotationBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter
implements MergedBeanDefinitionPostProcessor, PriorityOrdered, BeanFactoryAware {
// 自动注入注解类型
private final Set<Class<? extends Annotation>> autowiredAnnotationTypes = new LinkedHashSet<>(4);

public AutowiredAnnotationBeanPostProcessor() {
// autowiredAnnotationTypes中放入@Autowired、@Value
this.autowiredAnnotationTypes.add(Autowired.class);
this.autowiredAnnotationTypes.add(Value.class);
try {
this.autowiredAnnotationTypes.add((Class<? extends Annotation>)
ClassUtils.forName("javax.inject.Inject", AutowiredAnnotationBeanPostProcessor.class.getClassLoader()));
}
catch (ClassNotFoundException ex) {
// JSR-330 API not available - simply skip.
}
}

@Override
public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, Class<?> beanType, String beanName) {
// 这里其实就很熟悉了,和@Resource的处理过程看起来就是一模一样的
InjectionMetadata metadata = findAutowiringMetadata(beanName, beanType, null);
metadata.checkConfigMembers(beanDefinition);
}

private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
// 缓存逻辑我这边就不看了,都是一模一样的
// 这个,其实连收集逻辑都基本是一致的,我们就简单过一下吧
if (!AnnotationUtils.isCandidateClass(clazz, this.autowiredAnnotationTypes)) {
return InjectionMetadata.EMPTY;
}

List<InjectionMetadata.InjectedElement> elements = new ArrayList<>();
Class<?> targetClass = clazz;

do {
final List<InjectionMetadata.InjectedElement> currElements = new ArrayList<>();
// 处理属性
ReflectionUtils.doWithLocalFields(targetClass, field -> {
MergedAnnotation<?> ann = findAutowiredAnnotation(field);
if (ann != null) {
// 静态属性不允许注入
if (Modifier.isStatic(field.getModifiers())) {
return;
}
// @Autowrired有一个required属性需要收集一下
boolean required = determineRequiredStatus(ann);
currElements.add(new AutowiredFieldElement(field, required));
}
});
// 处理方法
ReflectionUtils.doWithLocalMethods(targetClass, method -> {
Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
return;
}
MergedAnnotation<?> ann = findAutowiredAnnotation(bridgedMethod);
if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
if (Modifier.isStatic(method.getModifiers())) {
// 静态方法不处理,忽略,相当于不生效,@Resource那边是会报错的。
return;
}
boolean required = determineRequiredStatus(ann);
// 封装一个属性描述符描述符
PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
currElements.add(new AutowiredMethodElement(method, required, pd));
}
});
// 父类优先
elements.addAll(0, currElements);
targetClass = targetClass.getSuperclass();
}
while (targetClass != null && targetClass != Object.class);
// 封装到InjectionMetadata
return InjectionMetadata.forElements(elements, clazz);
}
}

啊,索然无味,这个逻辑简直跟@Resource的处理一模一样的,同学们有没有一丢丢疑惑–这样如此雷同的方法,spring为什么要写两遍呢?这是不是违反了DRY原则呢?同学们可以思考一下这个问题。

我倒是认为没有违反DRY原则,但是对于javax.inject.Inject注解的划分还是不太合适,应该划分到common的;而common中对各种类型的注解的处理(@EJB、@Resource、@WebServiceRef)使用if-else也不太优雅,完全可以使用一个小小的策略模式的。

不过这东西每个人看法也不一样的,同学们有兴趣也可以在评论区探讨一下~

3.2.3. 总结CommonAnnotationBeanPostProcessor和AutowiredAnnotationBeanPostProcessor

这个埋点基本上就这两个beanPostProcessor做了事情了,而且也与我们平常的开发息息相关,这边简单总结一下。

3.2.3.1 职能划分

这两个beanPostProcessor的职能上是有划分的:

  1. CommonAnnotationBeanPostProcessor主要处理jdk相关的规范的注解,@Resource、@PostConstruct等注解都是jdk的规范中定义的。
* 收集生命周期相关的`@PostConstruct`、`@PreDestroy`注解信息封装成`LifecycleMetadata`
* 收集资源注入注解(我们主要关注`@Resource`)信息封装成`InjectionMetadata`
  1. AutowiredAnnotationBeanPostProcessor主要处理spring定义的@Autowired相关的功能
* 这里不得不说一下我觉得这个类也用来处理`javax.inject.Inject`不合理
* 收集自动注入相关的注解`@Autowired`、`@Value`信息封装成`InjectionMetadata`
3.2.3.2 使用@Resouce还是@Autowired?

那么日常我们开发过程中,究竟推荐使用@Resouce还是@Autowired呢?这个问题我认为仁者见仁智者见智,我这边只稍微列一下使用这两个注解时需要注意的问题:

  1. @Resouce和@Autowired都不能用来注入静态属性(通过在静态属性上使用注解和静态方法上使用注解)
  2. 使用@Resouce注入静态属性时,会直接抛出IllegalStateException导致当前实例初始化流程失败
  3. 而使用@Autowired注入静态属性时,只会忽略当前属性,不注入了,不会导致实例初始化流程失败
  4. 使用@Resouce修饰方法时,方法只能有一个入参,而@Autowired没有限制
  5. @Resouce属于jdk的规范,可以认为对项目零入侵;@Autowired属于spring的规范,使用了@Autowired的话就不能替换成别的IOC框架了(这个我确实也没替换过…)

(由于掘金对文章字数的限制,这篇博文被迫分为上下两篇,点击前往下一篇)

创作不易,转载请篇首注明 作者:掘金@小希子 + 来源链接~

如果想了解更多Spring源码知识,点击前往其余逐行解析Spring系列

٩(* ఠO ఠ)=3⁼³₌₃⁼³₌₃⁼³₌₃嘟啦啦啦啦。。。

这里是新人博主小希子,大佬们都看到这了,左上角点个赞再走吧~~

本文转载自: 掘金

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

一文搞懂什么是递归,程序员必会算法之一

发表于 2020-08-11

前言

今天我们来讲讲递归算法,递归在我们日常工作中是比较常见且常用的算法,面试中面试官也经常会让我们手写递归算法。由此可见递归算法的重要性。

递归

什么是递归

简单来说递归就是方法自己调用自己,每次调用时传入不同的变量。一直到程序执行到指定的出口时停止调用本身,并将结果层层返回。

递归的优点

递归的核心思想就是将一个大问题,拆解成一个小问题,然后将小问题再次拆解,层层拆分从而简化问题。这种设计理念可以简化重复的代码让程序变得更加简洁。

递归的缺点

  • 使用递归算法时每次方法的调用都需要在栈中开辟出一个空间保存相关数据,频繁的压栈、弹栈会导致效率变低。
  • 使用递归算法解决问题必须要有出口,不然就形成死循环了。好好的递归变成了“死归”!
  • 递归的调用次数不宜过多不然会造成栈溢出。

举个例子


从前有座山,山里有座庙,庙里有位老和尚,老和尚在干嘛呢?老和尚在讲故事。讲的什么故事呢?从前有座山,山里有座庙 ……

这是我们耳熟能详的故事,我们就通过这个案例来讲讲递归。

条件设定

  • 老和尚讲故事,老和尚寿命是5岁
  • 每讲一次寿命就减一岁
  • 当岁数减到0的时候,老和尚就圆寂了,就停止了讲故事
  • 最终返回总共讲故事的次数

算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码// 寿命5岁
private static Integer life = 5;

// 讲故事次数
private static Integer storyNum = 0;

public static Integer tellStory(Integer life) {

    if (life > 0) {
        System.out.println("从前有座山,山里有座庙,庙里有位老和尚,老和尚在干嘛呢?老和尚在讲故事。讲的什么故事呢?");
        -- life;
        // 老和尚还没圆寂,继续讲故事,这就是递归
        tellStory(life);
    } else {
        System.out.println("老和尚圆寂了,没办法在给大家讲故事了!");
        return storyNum;
    }
    return ++ storyNum;
}

public static void main(String[] args) {
    Integer num = tellStory(life);
    System.out.println("老和尚总共讲了" + num + "次故事");
}

递归结果



IT 老哥


一个在大厂做高级Java开发的程序猿

❝
关注微信公众号:IT 老哥

❞

❝
回复:Java 全套教程,即可领取:Java 基础、Java web、JavaEE 全部的教程,包括 spring boot 等

❞

❝
回复:简历模板,即可获取 100 份精美简历

❞

❝
回复:Java 学习路线,即可获取最新最全的一份学习路线图

❞

❝
回复:Java 电子书,即可领取 13 本顶级程序员必读书籍

❞

本文转载自: 掘金

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

1…787788789…956

开发者博客

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