# 蚂蚁金服 Java 面试
大家好,我是小林。
蚂蚁金服校招也开奖,也就是我们用的支付宝的公司。
我整理了 25 届蚂蚁金服校招薪资情况(数据来源于 offershow + 读者分享):
- ssp offer:28k x 16 + 1.5k x 12 房补,同学 bg 硕士 985,base 成都
- sp offer:26k x 16 + + 2k x 12 房补,同学 bg 硕士双一流,base 北京
- sp offer:25k x 16 + 2k x 12 房补,同学 bg 硕士 985,base 杭州
- 普通 offer:23k x 16 + 2k x 12 房补,同学 bg 硕士 985,base 杭州
- 普通 offer:23k x 16 + 1.5k x 12 房补,同学 bg 硕士 985,base 成都
蚂蚁金服工作地点分布比较多,杭州、成都、北京、深圳都有,不同地方的薪资基本没什么区别,主要是房补稍微有点差距,房补是持续三年,成都是每个月 1.5k,杭州和北京是 2k。
从目前蚂蚁金服开的薪资来看,和去年差不多完全一样的水平,而今年不少互联网大厂给的校招薪资都比去年多了一些,如果手上有多个大厂 offer 的话,蚂蚁的薪资竞争力就相对弱了一些。
但是其实已经很不错的,主要是大厂之间为了抢优秀的人才还是太卷的了,把今年校招薪资卷出了新高度。
不少去年 24 届进大厂的同学都跟我说,工作半年了,结果都还没今年还没毕业的 25 届同学薪资高,泪崩了。
薪资倒挂这件事还是很正常的,倘然接受就行,说不定明年 26 届的大厂校招薪资,比 25 届的还多呢。
蚂蚁金服面试跟阿里巴巴没什么区别,面试的重点主要是Java+框架+后端组件多一些,而网络和操作系统相对腾讯和字节会问的少一些,所以大家面不同公司的时候,需要针对性的做好复习的侧重点。
这次分享的是一位同学的**阿里巴巴Java后端的校招面经,**感觉问的问题还挺多的,面试时长有 1小时+,压力不小,面试官一直追问。
考察的知识点,我帮大家罗列了一下:
- MySQL:主键、b+树搜索、索引、存储引擎、隔离级别、锁
- Redis:应用场景、本地缓存、大 key、持久化、zset
- MyBatis:SQL注入
- Spring:bean 生命周期
# MySQL
# 表中十个字段,你主键用自增ID还是UUID,为什么?(我回答了自增和UUID的优缺点)
用的是自增 id。
因为 uuid 相对顺序的自增 id 来说是毫无规律可言的,新行的值不一定要比之前的主键的值要大,所以 innodb 无法做到总是把新行插入到索引的最后,而是需要为新行寻找新的合适的位置从而来分配新的空间。
这个过程需要做很多额外的操作,数据的毫无顺序会导致数据分布散乱,将会导致以下的问题:
- 写入的目标页很可能已经刷新到磁盘上并且从缓存上移除,或者还没有被加载到缓存中,innodb 在插入之前不得不先找到并从磁盘读取目标页到内存中,这将导致大量的随机 IO。
- 因为写入是乱序的,innodb 不得不频繁的做页分裂操作,以便为新的行分配空间,页分裂导致移动大量的数据,影响性能。
- 由于频繁的页分裂,页会变得稀疏并被不规则的填充,最终会导致数据会有碎片。
结论:使用 InnoDB 应该尽可能的按主键的自增顺序插入,并且尽可能使用单调的增加的聚簇键的值来插入新行。
# 为什么自增ID更快一些,UUID不快吗,它在B+树里面存储是有序的吗?
自增的主键的值是顺序的,所以 Innodb 把每一条记录都存储在一条记录的后面,所以自增 id 更快的原因:
- 下一条记录就会写入新的页中,一旦数据按照这种顺序的方式加载,主键页就会近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费
- 新插入的行一定会在原有的最大数据行下一行,mysql定位和寻址很快,不会为计算新行的位置而做出额外的消耗
- 减少了页分裂和碎片的产生
但是 UUID 不是递增的,MySQL 中索引的数据结构是 B+Tree,这种数据结构的特点是索引树上的节点的数据是有序的,而如果使用 UUID 作为主键,那么每次插入数据时,因为无法保证每次产生的 UUID 有序,所以就会出现新的 UUID 需要插入到索引树的中间去,这样可能会频繁地导致页分裂,使性能下降。
而且,UUID 太占用内存。每个 UUID 由 36 个字符组成,在字符串进行比较时,需要从前往后比较,字符串越长,性能越差。另外字符串越长,占用的内存越大,由于页的大小是固定的,这样一个页上能存放的关键字数量就会越少,这样最终就会导致索引树的高度越大,在索引搜索的时候,发生的磁盘 IO 次数越多,性能越差。
查询数据时,到了B+树的叶子节点,之后的查找数据是如何做?
数据页中的记录按照「主键」顺序组成单向链表,单向链表的特点就是插入、删除非常方便,但是检索效率不高,最差的情况下需要遍历链表上的所有节点才能完成检索。因此,数据页中有一个页目录,起到记录的索引作用,就像我们书那样,针对书中内容的每个章节设立了一个目录,想看某个章节的时候,可以查看目录,快速找到对应的章节的页数,而数据页中的页目录就是为了能快速找到记录。那 InnoDB 是如何给记录创建页目录的呢?页目录与记录的关系如下图:页目录创建的过程如下:
- 将所有的记录划分成几个组,这些记录包括最小记录和最大记录,但不包括标记为“已删除”的记录;
- 每个记录组的最后一条记录就是组内最大的那条记录,并且最后一条记录的头信息中会存储该组一共有多少条记录,作为 n_owned 字段(上图中粉红色字段)
- 页目录用来存储每组最后一条记录的地址偏移量,这些地址偏移量会按照先后顺序存储起来,每组的地址偏移量也被称之为槽(slot),每个槽相当于指针指向了不同组的最后一个记录。
从图可以看到,页目录就是由多个槽组成的,槽相当于分组记录的索引。然后,因为记录是按照「主键值」从小到大排序的,所以我们通过槽查找记录时,可以使用二分法快速定位要查询的记录在哪个槽(哪个记录分组),定位到槽后,再遍历槽内的所有记录,找到对应的记录,无需从最小记录开始遍历整个页中的记录链表。以上面那张图举个例子,5 个槽的编号分别为 0,1,2,3,4,我想查找主键为 11 的用户记录:
- 先二分得出槽中间位是 (0+4)/2=2 ,2号槽里最大的记录为 8。因为 11 > 8,所以需要从 2 号槽后继续搜索记录;
- 再使用二分搜索出 2 号和 4 槽的中间位是 (2+4)/2= 3,3 号槽里最大的记录为 12。因为 11 < 12,所以主键为 11 的记录在 3 号槽里;
- 再从 3 号槽指向的主键值为 9 记录开始向下搜索 2 次,定位到主键为 11 的记录,取出该条记录的信息即为我们想要查找的内容。
你说你会MySQL,那它有哪些存储引擎(InnoDB和MyISAM)?
- InnoDB:InnoDB是MySQL的默认存储引擎,具有ACID事务支持、行级锁、外键约束等特性。它适用于高并发的读写操作,支持较好的数据完整性和并发控制。
- MyISAM:MyISAM是MySQL的另一种常见的存储引擎,具有较低的存储空间和内存消耗,适用于大量读操作的场景。然而,MyISAM不支持事务、行级锁和外键约束,因此在并发写入和数据完整性方面有一定的限制。
- Memory:Memory引擎将数据存储在内存中,适用于对性能要求较高的读操作,但是在服务器重启或崩溃时数据会丢失。它不支持事务、行级锁和外键约束。
InnoDB的四种隔离级别,你平常做项目和实习用的什么隔离级别(默认的)
用的是默认的,可重复读隔离级别
# 可重复读有没有幻读的问题?(举了例子)
有的,可重复读隔离级别下虽然很大程度上避免了幻读,但是还是没有能完全解决幻读。我举例一个可重复读隔离级别发生幻读现象的场景。以这张表作为例子:事务 A 执行查询 id = 5 的记录,此时表中是没有该记录的,所以查询不出来。
# 事务 A
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t_stu where id = 5;
Empty set (0.01 sec)
然后事务 B 插入一条 id = 5 的记录,并且提交了事务。
# 事务 B
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into t_stu values(5, '小美', 18);
Query OK, 1 row affected (0.00 sec)
mysql> commit;
Query OK, 0 rows affected (0.00 sec)
此时,事务 A 更新 id = 5 这条记录,对没错,事务 A 看不到 id = 5 这条记录,但是他去更新了这条记录,这场景确实很违和,然后再次查询 id = 5 的记录,事务 A 就能看到事务 B 插入的纪录了,幻读就是发生在这种违和的场景。
# 事务 A
mysql> update t_stu set name = '小林coding' where id = 5;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> select * from t_stu where id = 5;
+----+--------------+------+
| id | name | age |
+----+--------------+------+
| 5 | 小林coding | 18 |
+----+--------------+------+
1 row in set (0.00 sec)
整个发生幻读的时序图如下:在可重复读隔离级别下,事务 A 第一次执行普通的 select 语句时生成了一个 ReadView,之后事务 B 向表中新插入了一条 id = 5 的记录并提交。接着,事务 A 对 id = 5 这条记录进行了更新操作,在这个时刻,这条新记录的 trx_id 隐藏列的值就变成了事务 A 的事务 id,之后事务 A 再使用普通 select 语句去查询这条记录时就可以看到这条记录了,于是就发生了幻读。
因为这种特殊现象的存在,所以我们认为 MySQL Innodb 中的 MVCC 并不能完全避免幻读现象。
# MySQL的锁讲一下(按锁的粒度讲了一遍)
全局锁:通过flush tables with read lock 语句会将整个数据库就处于只读状态了,这时其他线程执行以下操作,增删改或者表结构修改都会阻塞。全局锁主要应用于做全库逻辑备份,这样在备份数据库期间,不会因为数据或表结构的更新,而出现备份文件的数据与预期的不一样。
表级锁:MySQL 里面表级别的锁有这几种:
表锁:通过lock tables 语句可以对表加表锁,表锁除了会限制别的线程的读写外,也会限制本线程接下来的读写操作。
元数据锁:当我们对数据库表进行操作时,会自动给这个表加上 MDL,对一张表进行 CRUD 操作时,加的是 MDL 读锁;对一张表做结构变更操作的时候,加的是 MDL 写锁;MDL 是为了保证当用户对表执行 CRUD 操作时,防止其他线程对这个表结构做了变更。
意向锁:当执行插入、更新、删除操作,需要先对表加上「意向独占锁」,然后对该记录加独占锁。意向锁的目的是为了快速判断表里是否有记录被加锁。
行级锁:InnoDB 引擎是支持行级锁的,而 MyISAM 引擎并不支持行级锁。
记录锁,锁住的是一条记录。而且记录锁是有 S 锁和 X 锁之分的,满足读写互斥,写写互斥
间隙锁,只存在于可重复读隔离级别,目的是为了解决可重复读隔离级别下幻读的现象。
Next-Key Lock 称为临键锁,是 Record Lock + Gap Lock 的组合,锁定一个范围,并且锁定记录本身。
# 设计一个行级锁的死锁,举一个实际的例子(有思路,但是不好描述)
假设这时有两事务,一个事务要插入订单 1007 ,另外一个事务要插入订单 1008,因为需要对订单做幂等性校验,所以两个事务先要查询该订单是否存在,不存在才插入记录,过程如下:
可以看到,两个事务都陷入了等待状态,也就是发生了死锁,因为都在相互等待对方释放锁。
在执行下面这条语句的时候:
select id from t_order where order_no = 1008 for update;
因为 order_no 不是唯一索引,所以行锁的类型是间隙锁,于是间隙锁的范围是(1006, +∞)
。那么,当事务 B 往间隙锁里插入 id = 1008 的记录就会被锁住。因为当我们执行以下插入语句时,会在插入间隙上再次获取插入意向锁。
insert into t_order (order_no, create_date) values (1008, now());
插入意向锁与间隙锁是冲突的,所以当其它事务持有该间隙的间隙锁时,需要等待其它事务释放间隙锁之后,才能获取到插入意向锁。而间隙锁与间隙锁之间是兼容的,所以所以两个事务中 select ... for update
语句并不会相互影响。
案例中的事务 A 和事务 B 在执行完后 select ... for update
语句后都持有范围为(1006,+∞)
的间隙锁,而接下来的插入操作为了获取到插入意向锁,都在等待对方事务的间隙锁释放,于是就造成了循环等待,导致死锁。
# MyBatis
# 我看你写到了MyBatis,#和$有什么区别(主要是SQL注入的问题)
- Mybatis 在处理 #{} 时,会创建预编译的 SQL 语句,将 SQL 中的 #{} 替换为 ? 号,在执行 SQL 时会为预编译 SQL 中的占位符(?)赋值,调用 PreparedStatement 的 set 方法来赋值,预编译的 SQL 语句执行效率高,并且可以防止SQL 注入,提供更高的安全性,适合传递参数值。
- Mybatis 在处理 ${} 时,只是创建普通的 SQL 语句,然后在执行 SQL 语句时 MyBatis 将参数直接拼入到 SQL 里,不能防止 SQL 注入,因为参数直接拼接到 SQL 语句中,如果参数未经过验证、过滤,可能会导致安全问题。
# 你说到了SQL注入,那你给我设计出一个SQL注入,具体说表中的字段,然后SQL语句是怎样的(有印象,但是自己说不来)
每次用户登录时,都会执行一个相应的 SQL 语句。这时,黑客会通过构造一些恶意的输入参数,在应用拼接 SQL 语句的时候,去篡改正常的 SQL 语意,从而执行黑客所控制的 SQL 查询功能。这个过程,就相当于黑客“注入”了一段 SQL 代码到应用中。这就是我们常说的 SQL 注入。
我们先来看一个例子。现在有一个简单的登录页面,需要用户输入 Username 和 Password 这两个变量来完成登录。具体的 Web 后台代码如下所示:
uName = getRequestString("username");
uPass = getRequestString("password");
sql='SELECT * FROM Users WHERE Username ="'+ uName +'" AND Password ="'+ uPass +'"'
当用户提交一个表单(假设 Username 为 admin,Password 为 123456)时,Web 将执行下面这行代码:
SELECT*FROM Users WHERE Username ="admin" AND Password ="123456"
用户名密码如果正确的话,这句 SQL 就能够返回对应的用户信息;如果错误的话,不会返回任何信息。因此,只要返回的行数≥1,就说明验证通过,用户可以成功登录。
所以,当用户正常地输入自己的用户名和密码时,自然就可以成功登录应用。那黑客想要在不知道密码的情况下登录应用,他又会输入什么呢?他会输入 " or ""="。这时,应用的数据库就会执行下面这行代码:
SELECT*FROM Users WHERE Username ="" AND Password ="" or ""=""
我们可以看到,WHERE 语句后面的判断是通过 or 进行拼接的,其中""=""的结果是 true。那么,当有一个 or 是 true 的时候,最终结果就一定是 true 了。因此,这个 WHERE 语句是恒为真的,所以,数据库将返回全部的数据。
这样一来,我们就能解答文章开头的问题了,也就是说,黑客只需要在登录页面中输入 " or ""=",就可以在不知道密码的情况下,成功登录后台了。而这,也就是所谓的“万能密码”。而这个“万能密码”,其实就是通过修改 WHERE 语句,改变数据库的返回结果,实现无密码登录。
# Redis
# 你用Redis做了什么
主要用作缓存,Redis 除了用于缓存,还能实现分布式锁、消息队列等等。
# 本地缓存和Redis缓存的区别(没了解过)
本地缓存是指将数据存储在本地应用程序或服务器上,通常用于加速数据访问和提高响应速度。本地缓存通常使用内存作为存储介质,利用内存的高速读写特性来提高数据访问速度。
本地缓存的优势:
- 访问速度快:由于本地缓存存储在本地内存中,因此访问速度非常快,能够满足频繁访问和即时响应的需求。
- 减轻网络压力:本地缓存能够降低对远程服务器的访问次数,从而减轻网络压力,提高系统的可用性和稳定性。
- 低延迟:由于本地缓存位于本地设备上,因此能够提供低延迟的访问速度,适用于对实时性要求较高的应用场景。
本地缓存的不足:
- 可扩展性有限:本地缓存的可扩展性受到硬件资源的限制,无法支持大规模的数据存储和访问。
**分布式缓存(Redis)**是指将数据存储在多个分布式节点上,通过协同工作来提供高性能的数据访问服务。分布式缓存通常使用集群方式进行部署,利用多台服务器来分担数据存储和访问的压力。
分布式缓存的优势:
- 可扩展性强:分布式缓存的节点可以动态扩展,能够支持大规模的数据存储和访问需求。
- 数据一致性高:通过分布式一致性协议,分布式缓存能够保证数据在多个节点之间的一致性,减少数据不一致的问题。
- 易于维护:分布式缓存通常采用自动化管理方式,能够降低维护成本和管理的复杂性。
分布式缓存的不足:
- 访问速度相对较慢:相对于本地缓存,分布式缓存的访问速度相对较慢,因为数据需要从多个节点进行访问和协同。
- 网络开销大:由于分布式缓存需要通过网络进行数据传输和协同操作,因此相对于本地缓存来说,网络开销较大。
在选择使用本地缓存还是分布式缓存时,我们需要根据具体的应用场景和需求进行权衡。以下是一些考虑因素:
- 数据大小:如果数据量较小,且对实时性要求较高,本地缓存更适合;如果数据量较大,且需要支持大规模的并发访问,分布式缓存更具优势。
- 网络状况:如果网络状况良好且稳定,分布式缓存能够更好地发挥其优势;如果网络状况较差或不稳定,本地缓存的访问速度和稳定性可能更有优势。
- 业务特点:对于实时性要求较
# Redis的Key过期了是立马删除吗(回答了定期删除和惰性删除两种策略)
不会,Redis 的过期删除策略是选择「惰性删除+定期删除」这两种策略配和使用。
- 惰性删除策略的做法是,不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。
- 定期删除策略的做法是,每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。
# Redis的大Key问题是什么?(答出来了)
Redis大key问题指的是某个key对应的value值所占的内存空间比较大,导致Redis的性能下降、内存不足、数据不均衡以及主从同步延迟等问题。
到底多大的数据量才算是大key?
没有固定的判别标准,通常认为字符串类型的key对应的value值占用空间大于1M,或者集合类型的k元素数量超过1万个,就算是大key。
Redis大key问题的定义及评判准则并非一成不变,而应根据Redis的实际运用以及业务需求来综合评估。
例如,在高并发且低延迟的场景中,仅10kb可能就已构成大key;然而在低并发、高容量的环境下,大key的界限可能在100kb。因此,在设计与运用Redis时,要依据业务需求与性能指标来确立合理的大key阈值。
# 大Key问题的缺点?(答出来了)
- 内存占用过高。大Key占用过多的内存空间,可能导致可用内存不足,从而触发内存淘汰策略。在极端情况下,可能导致内存耗尽,Redis实例崩溃,影响系统的稳定性。
- 性能下降。大Key会占用大量内存空间,导致内存碎片增加,进而影响Redis的性能。对于大Key的操作,如读取、写入、删除等,都会消耗更多的CPU时间和内存资源,进一步降低系统性能。
- 阻塞其他操作。某些对大Key的操作可能会导致Redis实例阻塞。例如,使用DEL命令删除一个大Key时,可能会导致Redis实例在一段时间内无法响应其他客户端请求,从而影响系统的响应时间和吞吐量。
- 网络拥塞。每次获取大key产生的网络流量较大,可能造成机器或局域网的带宽被打满,同时波及其他服务。例如:一个大key占用空间是1MB,每秒访问1000次,就有1000MB的流量。
- 主从同步延迟。当Redis实例配置了主从同步时,大Key可能导致主从同步延迟。由于大Key占用较多内存,同步过程中需要传输大量数据,这会导致主从之间的网络传输延迟增加,进而影响数据一致性。
- 数据倾斜。在Redis集群模式中,某个数据分片的内存使用率远超其他数据分片,无法使数据分片的内存资源达到均衡。另外也可能造成Redis内存达到maxmemory参数定义的上限导致重要的key被逐出,甚至引发内存溢出。
# ZSet的底层数据结构,查询的时间复杂度是多少?
Zset 类型的底层数据结构是由压缩列表或跳表实现的:如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构,压缩列表的查找操作是顺序查找,时间复杂度为O(n)
- 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构,跳表查询任意数据的时间复杂度就是 O(logn)
在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。
# Redis的持久化(AOF和RDB)?
Redis 的持久化机制有两种,一种是快照(RDB),另一种是 AOF 日志。
RDB是一次全量备份,AOF 日志是连续的增量备份。快照是内存数据的二进制序列化形式,在存储上非常紧凑,而 AOF 日志记录的是内存数据修改的指令记录文本。
# RDB是怎样做的?(答出来了)
所谓的快照,就是记录某一个瞬间东西,比如当我们给风景拍照时,那一个瞬间的画面和信息就记录到了一张照片。
所以,RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行:
- 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
- 执行了 bgsave 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞,basave命令可以在主进程的基础上,fork一个子进程,子进程会共享主进程的代码段和数据段,相当于是在后台生成快照。
# aof的写入策略,按时间写入和每次都写入的区别,优缺点(答出来了)
Redis 提供了 3 种写回硬盘的策略, 在 Redis.conf 配置文件中的 appendfsync 配置项可以有以下 3 种参数可填:
- Always,这个单词的意思是「总是」,所以它的意思是每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;
- Everysec,这个单词的意思是「每秒」,所以它的意思是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;
- No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机,也就是每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,再由操作系统决定何时将缓冲区内容写回硬盘。
我也把这 3 个写回策略的优缺点总结成了一张表格:
# 你平常是怎么使用RDB和AOF的?
数据安全性:如果要求数据不丢失,推荐AOF
如果使用每秒同步一次策略,则最多丢失一秒的数据
如果使用每次写操作都同步策略,安全性达到了极致,但这会影响性能
AOF可以采取每秒同步一次数据或每次写操作都同步用来保证数据安全性
RDB是一个全量的二进制文件,恢复时只需要加载到内存即可,但是可能会丢失最近几分钟的数据(取决于RDB持久化策略)
数据恢复速度:如果要求快速恢复数据,推荐RDB
AOF需要重新执行所有的写命令,恢复时间会更长
RDB是一个全量的二进制文件,恢复时只需要加载到内存即可
数据备份和迁移:如果要求方便地进行数据备份和迁移,推荐RDB
AOF文件可能会很大,传输速度慢
RDB文件是一个紧凑的二进制文件,占用空间小,传输速度快
数据可读性:如果要求能够方便地查看和修改数据,推荐AOF
AOF是一个可读的文本文件,记录了所有的写命令,可以用于灾难恢复或者数据分析
RDB是一个二进制文件,不易查看和修改
# Spring
# Bean的生命周期(答出来了,主要分几个过程,细致介绍了一遍)
- Spring启动,查找并加载需要被Spring管理的bean,进行Bean的实例化
- Bean实例化后对将Bean的引入和值注入到Bean的属性中
- 如果Bean实现了BeanNameAware接口的话,Spring将Bean的Id传递给setBeanName()方法
- 如果Bean实现了BeanFactoryAware接口的话,Spring将调用setBeanFactory()方法,将BeanFactory容器实例传入
- 如果Bean实现了ApplicationContextAware接口的话,Spring将调用Bean的setApplicationContext()方法,将bean所在应用上下文引用传入进来。
- 如果Bean实现了BeanPostProcessor接口,Spring就将调用他们的postProcessBeforeInitialization()方法。
- 如果Bean 实现了InitializingBean接口,Spring将调用他们的afterPropertiesSet()方法。类似的,如果bean使用init-method声明了初始化方法,该方法也会被调用
- 如果Bean 实现了BeanPostProcessor接口,Spring就将调用他们的postProcessAfterInitialization()方法。
- 此时,Bean已经准备就绪,可以被应用程序使用了。他们将一直驻留在应用上下文中,直到应用上下文被销毁。
- 如果bean实现了DisposableBean接口,Spring将调用它的destory()接口方法,同样,如果bean使用了destory-method 声明销毁方法,该方法也会被调用。
# Bean是否单例?
Spring 中的 Bean 默认都是单例的。
就是说,每个Bean的实例只会被创建一次,并且会被存储在Spring容器的缓存中,以便在后续的请求中重复使用。这种单例模式可以提高应用程序的性能和内存效率。
但是,Spring也支持将Bean设置为多例模式,即每次请求都会创建一个新的Bean实例。要将Bean设置为多例模式,可以在Bean定义中通过设置scope属性为"prototype"来实现。
需要注意的是,虽然Spring的默认行为是将Bean设置为单例模式,但在一些情况下,使用多例模式是更为合适的,例如在创建状态不可变的Bean或有状态Bean时。此外,需要注意的是,如果Bean单例是有状态的,那么在使用时需要考虑线程安全性问题。
# Bean的单例和非单例,生命周期是否一样
不一样的,Spring Bean 的生命周期完全由 IoC 容器控制。Spring 只帮我们管理单例模式 Bean 的完整生命周期,对于 prototype
的 Bean,Spring 在创建好交给使用者之后,则不会再管理后续的生命周期。
# 你刚才说的Bean生命周期,是单例的还是非单例的
单例 Bean
# Spring容器里存的是什么?
在Spring容器中,存储的主要是Bean对象。
Bean是Spring框架中的基本组件,用于表示应用程序中的各种对象。当应用程序启动时,Spring容器会根据配置文件或注解的方式创建和管理这些Bean对象。Spring容器会负责创建、初始化、注入依赖以及销毁Bean对象。
# Bean注入和xml注入最终得到了相同的效果,它们在底层是怎样做的
XML 注入
使用 XML 文件进行 Bean 注入时,Spring 在启动时会读取 XML 配置文件,以下是其底层步骤:
Bean 定义解析:Spring 容器通过
XmlBeanDefinitionReader
类解析 XML 配置文件,读取其中的<bean>
标签以获取 Bean 的定义信息。注册 Bean 定义:解析后的 Bean 信息被注册到
BeanDefinitionRegistry
(如DefaultListableBeanFactory
)中,包括 Bean 的类、作用域、依赖关系、初始化和销毁方法等。实例化和依赖注入:当应用程序请求某个 Bean 时,Spring 容器会根据已经注册的 Bean 定义:
首先,使用反射机制创建该 Bean 的实例。
然后,根据 Bean 定义中的配置,通过 setter 方法、构造函数或方法注入所需的依赖 Bean。
注解注入
使用注解进行 Bean 注入时,Spring 的处理过程如下:
- 类路径扫描:当 Spring 容器启动时,它首先会进行类路径扫描,查找带有特定注解(如
@Component
、@Service
、@Repository
和@Controller
)的类。 - 注册 Bean 定义:找到的类会被注册到
BeanDefinitionRegistry
中,Spring 容器将为其生成 Bean 定义信息。这通常通过AnnotatedBeanDefinitionReader
类来实现。 - 依赖注入:与 XML 注入类似,Spring 在实例化 Bean 时,也会检查字段上是否有
@Autowired
、@Inject
或@Resource
注解。如果有,Spring 会根据注解的信息进行依赖注入。
尽管使用的方式不同,但 XML 注入和注解注入在底层的实现机制是相似的,主要体现在以下几个方面:
BeanDefinition:无论是 XML 还是注解,最终都会生成
BeanDefinition
对象,并存储在同一个BeanDefinitionRegistry
中。后处理器:
Spring 提供了多个 Bean 后处理器(如
AutowiredAnnotationBeanPostProcessor
),用于处理注解(如@Autowired
)的依赖注入。对于 XML,Spring 也有相应的后处理器来处理 XML 配置的依赖注入。
依赖查找:在依赖注入时,Spring 容器会通过
ApplicationContext
中的 BeanFactory 方法来查找和注入依赖,无论是通过 XML 还是注解,都会调用类似的查找方法。
对了,最新的互联网大厂后端面经都会在公众号首发,别忘记关注哦!!如果你想加入百人技术交流群,扫码下方二维码回复「加群」。