
💡 「你讲讲 Bean 的生命周期」这道题,八成的人是从实例化背到销毁,背到「初始化」就断了------因为初始化这一步里塞了 Aware 回调、
@PostConstruct、InitializingBean、initMethod、BeanPostProcessor、AOP 代理六个东西,谁先谁后说不清。更麻烦的是,视频里同一道题给了短版和长版两套答法,面试官接着追问的三刀往往在这:单例 Bean 和单例模式到底有什么区别、@Autowired为什么把别人家的 Bean 注进来了、单例 Bean 到底是不是线程安全的。这一篇把六个分 P 的素材合并成一条主线,顺序按 Spring Framework 5.3.x 的实际调用链核对过,能背也能解释。
1. Bean 生命周期到底指什么:一条主线,四大步
视频里短版(P01)和长版(P18)问的是同一件事,答案口径完全一致:Bean 的生命周期就是一个 Bean 从创建到销毁的整个流程,说白了是「从生到死」。长版给的记忆抓手是四大步:
text
实例化 ──► 属性赋值 ──► 初始化 ──► 销毁
(new 出对象) (DI 填依赖) (回调+AOP) (容器关闭)
短版则强调另外两个容易漏讲的点:什么时候开始创建 ,以及什么时候销毁。
关于创建时机,视频的表述是:容器加载的时候开始创建。你 new 一个 AnnotationConfigApplicationContext(传配置类)或者 ClassPathXmlApplicationContext(传 XML),底层就根据你配置的 Bean 定义一个一个地开始创建。这句话很重要,因为 Bean 生命周期本身就发生在 IoC 容器的加载过程里------IoC 加载过程说白了就是一堆 Bean 的创建过程,两道题是一回事。
四大步各自在干什么:
| 步骤 | 容器做了什么 | 关键动作 | 你能感知到什么 | 常见漏答点 |
|---|---|---|---|---|
| 实例化 | 根据 Bean 的类,通过反射推断构造函数并 new |
createBeanInstance |
拿到一个「早期对象」 | 实例化不止反射一条路,静态工厂、实例工厂也在这一步 |
| 属性赋值 | 解析依赖并注入 | populateBean |
@Autowired、@Resource、@Value 生效 |
循环依赖就是在这一步炸的 |
| 初始化 | 回调一堆扩展点 | initializeBean |
Aware 接口、生命周期回调、AOP 代理 | 这一步不是「调一个 init 方法」,是一整条链 |
| 销毁 | 容器关闭时回调销毁方法 | destroy() |
@PreDestroy、DisposableBean、destroyMethod |
只发生在容器关闭,不发生在加载过程 |
这里有个概念必须单独点出来:早期 Bean 对象 。视频(P01)给的说法是,实例化刚结束得到的那个对象叫早期对象,因为它还没经过依赖注入,要注入的属性还是 null。这个命名不是学术词汇,它是为后面的循环依赖服务的------三级缓存要提前暴露出去的就是这个早期对象。三级缓存和 IoC 的扩展点属于系列其他分篇,这里只标个锚点。
一句话理解:四大步是骨架,initializeBean 里的六个回调是血肉,背下骨架只能及格,讲清血肉才是看过源码。
关于销毁,短版里那段回答我认为是全篇最值钱的部分:Bean 不会自动销毁。它不像普通对象那样被垃圾回收器回收,因为默认作用域是单例,单例对象会被缓存进容器的一张 Map 里,这张 Map 一直强引用着它。除非你主动调用容器的 close() 方法,否则它一路活到进程结束。
2. 把四大步摊开:容器的真实调用顺序
四大步太粗,面试官会追「初始化里到底谁先谁后」。这一节给一条完整的横向流水线,顺序以 5.3.x 的实际创建链路为准。
text
容器加载(读 BeanDefinition)
│
▼
① 实例化 createBeanInstance ──► 早期 Bean(待注入属性仍为 null)
│
▼
② 属性赋值 populateBean ──► 解析 @Autowired / @Resource / @Value(循环依赖在此暴露)
│
▼
③ 初始化 initializeBean
│ ③.1 setBeanName(BeanNameAware)
│ ③.2 setBeanFactory(BeanFactoryAware)
│ ③.3 setApplicationContext(ApplicationContextAware)
│ ③.4 @PostConstruct
│ ③.5 postProcessBeforeInitialization(BeanPostProcessor 前置)
│ ③.6 afterPropertiesSet(InitializingBean)
│ ③.7 init-method(@Bean(initMethod=) / XML init-method)
│ ③.8 postProcessAfterInitialization(BeanPostProcessor 后置 ──► AOP 动态代理在这里生成)
▼
④ 入单例池 addSingleton ──► singletonObjects{ beanName ──► 实例对象或代理对象 }
│
▼
getBean(beanName) 开始对外服务 ...... 直到容器 close()
│
▼
⑤ 销毁 ──► @PreDestroy ──► DisposableBean.destroy() ──► destroy-method
几个需要展开的点:
第一,Aware 回调在依赖注入之后。 很多背题版答案把 setBeanName、setBeanFactory 排在属性赋值前面,这是错的。我把 v5.3.39 tag 的 AbstractAutowireCapableBeanFactory 源码拉下来逐行对过,又和本地的 6.1.x、主干副本交叉验证,三个版本骨架一致:doCreateBean 里就是 createBeanInstance → populateBean → initializeBean,而 invokeAwareMethods 在 initializeBean 内部。也就是说:setBeanName 拿到的时候,你的 @Autowired 字段已经填好了。invokeAwareMethods 里只判三个接口------BeanNameAware、BeanClassLoaderAware、BeanFactoryAware(视频 P18 里提到的也就是这三个,转写成「banana away / in class Loader away / in factory away」)。ApplicationContextAware 不在这里,它由容器注册的一个 BeanPostProcessor 负责,所以落在 ③.3。
第二,AOP 代理是在 ③.8 生成的。 短版(P01)说的是「初始化之后这一步创建动态代理」,长版(P18)说的是「如果实现了 AOP,在初始化的第一步去创建动态代理」,两套口述互相打架。以我核对到的源码为准:AbstractAutoProxyCreator.postProcessAfterInitialization 里调 wrapIfNecessary,即正常情况下代理在初始化之后生成;只有存在循环依赖需要提前暴露时,才会走 getEarlyBeanReference 那条路提前把代理做出来(v5.3.39 的 doCreateBean 里,addSingletonFactory(beanName, () -> getEarlyBeanReference(...)) 就登记在实例化之后、populateBean 之前------这句话点到为止,三级缓存另篇细讲)。所以面试里说「AOP 代理在初始化之后由 BeanPostProcessor 生成,循环依赖场景会提前到 getEarlyBeanReference」,两头都站得住。
第三,入池的是「最终对象」。 视频(P01)特意强调:放进单例池的 value,如果这个 Bean 走了 AOP,那就是动态代理对象,不是原始对象。singletonObjects 这个字段名我核对过源码,类型是 ConcurrentHashMap,key 是 beanName,value 是实例。
Q1:@PostConstruct 算不算 BeanPostProcessor 链的一环?
严格说,算。@PostConstruct 不是容器直接反射调的,而是 InitDestroyAnnotationBeanPostProcessor(CommonAnnotationBeanPostProcessor 的父类)在 postProcessBeforeInitialization 里触发的。我拉了 v5.3.39 的源码,这个类实现 PriorityOrdered,但 order 字段默认值是 LOWEST_PRECEDENCE。所以上面流水线里把 ③.4、③.5 拆成两行,是面试友好的画法,实际两者同属前置处理器链;你答题时说「注解先、接口中、属性后」这个大类顺序不会错,但如果面试官继续追「那和自定义 BeanPostProcessor 谁先」,正确答案是「取决于 BeanPostProcessor 的注册顺序和 order 值」,而不是硬说注解一定在最前。
3. 生命周期回调的三种写法与执行顺序
P17 单独问的是回调方法:分初始化、销毁两类,每类都有三种实现方式,而且三种方式之间有固定顺序。
| 方式 | 初始化写法 | 销毁写法 | 可配置的位置 | 执行顺序 | 注意点 |
|---|---|---|---|---|---|
| 注解 | @PostConstruct |
@PreDestroy |
任何 Bean 的任意方法上 | 最先 | 来自 JSR-250,Spring 5.3 时代包名是 javax.annotation,Spring 6 迁到 jakarta |
| 接口 | 实现 InitializingBean#afterPropertiesSet |
实现 DisposableBean#destroy |
类上实现接口 | 其次 | 方法名被写死在接口里,侵入 API |
| 指定属性 | @Bean(initMethod = "...") / XML init-method |
@Bean(destroyMethod = "...") / XML destroy-method |
只能用 @Bean 或 XML |
最后 | 方法名随便取;@Component 上没有这个属性可填 |
视频里给的操作细节有三条值得记:
- 销毁回调必须关容器才看得见。 演示里
@PreDestroy的输出是在调用容器close()之后才打出来的,这跟第 1 节讲的「单例不会自动销毁」是同一条事实的两个侧面。 - 属性式配置只能挂在
@Bean或 XML 上。 因为@Component注解本身没有initMethod、destroyMethod可以填;@Bean有。我查了 5.3.39 的@Beanjavadoc,initMethod是「the optional name of a method to call on the bean instance during initialization」,destroyMethod是「upon closing the application context」,并且容器会自动推断 destroy method------只认公有的、无参的、名字叫close或shutdown的方法;写@Bean(destroyMethod = "")可以关掉这个推断,但DisposableBean接口那条路依然生效。 - 三种方式实测的输出顺序就是 注解 → 接口 → 属性 ,初始化侧和销毁侧各自按这个次序。销毁侧我用源码验证过
DisposableBeanAdapter.destroy()的实现:先遍历DestructionAwareBeanPostProcessor.postProcessBeforeDestruction(@PreDestroy从这里进来),再调DisposableBean.destroy(),最后才是自定义destroyMethod。两边结论一致。
一句话理解:注解是零成本默认选择,接口让容器帮你校验必填属性,属性式是配第三方类时唯一的办法 ------第三方的类你改不了源码,没法让它去实现 InitializingBean。
InitializingBean 的 javadoc 我核对了 5.3.39 原文,它对 afterPropertiesSet() 的描述是「Invoked by the containing BeanFactory after it has set all bean properties and satisfied BeanFactoryAware, ApplicationContextAware etc」------这句官方描述正好印证了第 2 节的顺序:Aware 在 afterPropertiesSet 之前,afterPropertiesSet 在自定义 init-method 之前。
4. 单例 Bean 和单例模式的区别:谁来决定「只有一个」
P02 这道题听着像文字游戏,实际考的是你对「单例」这个词的主键是什么有没有概念。
共同点先说:两者都是为了同一个对象只实例化一次 ,下次要的时候如果已经存在就直接返回,不再重复创建。单例 Bean 也确实借用了单例设计模式的思路。区别在单例的维度不一样。
| 对比项 | 单例设计模式 | 单例 Bean(@Scope("singleton")) |
|---|---|---|
| 唯一性的判定依据 | 类(Class) | Bean 的名字(beanName) |
| 谁保证唯一 | 类自己:私有构造 + 静态持有 + 静态工厂方法 | 容器:singletonObjects 这张 Map,key 是 beanName |
| 作用范围 | 一个 JVM 内,同一个类只有一个实例,调多少次都是同一个内存地址 | 一个容器内,同一个 beanName 只有一个实例 |
| 同一个类能不能有多个 | 不能 | 能,只要 beanName 不同 |
| 能不能被绕过 | 反射破环、序列化、多类加载器 | 再起一个容器就有两份,prototype 作用域每次都新建 |
| 生命周期归谁 | 类自己管,JVM 结束才走 | 容器管,容器 close() 时走销毁回调 |
视频给的结论就是这么一句话:单例设计模式以类为约束,单例 Bean 以 beanName 为主键。哪怕你用的是同一个类,只要 Bean 的名字不一样,容器就会实例化出多个对象。
下面这段是我自己补的例子,视频里没有代码:
java
// 笔者补充示例:同一个类注册成两个单例 Bean
@Configuration
public class DemoConfig {
@Bean("cashierService")
public UserService cashier() { return new UserService(); }
@Bean("auditService")
public UserService audit() { return new UserService(); }
}
UserService 类只有一个,但 singletonObjects 里有两条记录、两个对象。而如果 UserService 写成 GoF 单例(私有构造 + getInstance()),这两个 @Bean 方法返回的会是同一个地址------这时候「单例 Bean」的语义就退化成单例模式了,两个 beanName 指向同一实例。
我个人更倾向于这样回答面试官:**单例模式是「类的自证」,靠构造函数私有化在 JVM 层面卡死唯一性;单例 Bean 是「容器的登记」,靠一张以 beanName 为 key 的注册表缓存唯一性。**两者是不同层次的机制,只是恰好都叫单例。
Q2:同一个类注册成两个单例 Bean,按类型注入会拿到哪一个?
拿到哪个都不一定,而且很可能直接启动失败。按类型注入时这两个实例都是候选,容器要走的正是第 5 节那条决胜链:先看有没有 @Primary,再看 @Priority,最后才拿候选名和注入点的字段名/参数名比对;三个环节都分不出胜负,就抛 NoUniqueBeanDefinitionException,报「找到多个候选无法装配」。所以「同一个类多个 beanName」这件事本身合法,但它把选边的责任甩给了注入点------要么起一个能和注入点字段名对上的 beanName,要么显式 @Qualifier。
5. 依赖注入装配规则:为什么注进来的不是你配的那个
P04 这道题视频里是从某电商团队公众号的一篇文章引出来的,视频作者的态度是「这问题不难」。案例的骨架是这样的:自己配了一个 Bean,注入点的名字和 beanName 明明对得上,结果自动装配进来的却是另一个 Bean。
java
// 笔者按视频思路重写的伪代码,视频里用的两个 bean 名是中文人名,这里换成中性名
@Configuration
public class MenuConfig {
@Bean("localMenuService")
public MenuService localMenuService() { return new LocalMenuServiceImpl(); }
}
@RestController
public class MenuController {
@Autowired
private MenuService localMenuService; // 名字对得上,但注进来的不是它
}
调试结果(视频里的现象):注入点拿到的是第三方 jar 里的那个 Bean。原因是那个包的自动配置类也注册了一个同类型的 MenuService,并且加了 @Primary。
要解释这个现象,得把 Spring 的自动装配规则讲准。视频给的版本是「先按类型找,找到多个就按名字匹配,名字匹配上就注入,匹配不上就报多个候选无法装配」。这个说法方向对,但把 @Primary 的位置漏了 。我把 v5.3.39 的 DefaultListableBeanFactory 源码拉下来对过,实际判定链是这样的:
text
findAutowireCandidates(按类型收集候选)
│ isAutowireCandidate(candidate, descriptor) 逐个过滤 ──► @Qualifier 在这一步起作用
▼
候选 > 1
│
├─ ① determinePrimaryCandidate ──► @Primary 命中就直接返回
├─ ② determineHighestPriorityCandidate ──► 其次看 @Priority
└─ ③ matchesBeanName(候选名, 注入点名字) ──► 最后才按名字兜底
关键点:按名字匹配是第 ③ 级兜底,排在 @Primary 后面 。所以你只把字段名写成和 beanName 一样是不够的,只要候选里有一个 @Primary,它先赢。这就把视频里那个「明明匹配上了为什么没注进来」解释干净了。
解法也是视频给的:在注入点加 @Qualifier 指定 beanName。它的原理不是「优先级更高」,而是它作用在候选收集阶段 ------带 @Qualifier 的注入点会把名字不匹配的候选直接从 findAutowireCandidates 的过滤里踢掉,@Primary 那个候选连进决赛的资格都没有,自然谈不上生效。这个解释比「@Qualifier 优先于 @Primary」更准,也和 @Primary javadoc 的描述一致(@Primary 只在「多个候选都能装进这个单值依赖」时才给偏好,而 @Qualifier 已经把候选集缩到一个了)。
Q3:字段名和 beanName 完全一样,为什么不等于按名字注入成功?
因为「名字一样」只在两个地方有用:一是注册阶段,@Bean 方法名或 @Component 的默认值决定这个 Bean 在容器里叫什么;二是决胜阶段的最后一级兜底。而决胜是分层的------@Primary 的候选在第一层就返回了,压根轮不到名字比对。所以「我把字段名改成和 beanName 一样」这个操作,在没有 @Primary 干扰时有效,在有 @Primary 干扰时完全无效。真正表达「我就要这个名字」的语法是 @Qualifier,不是靠同名字段的隐式约定。
第二个场景是视频里另一位同学踩的坑:@Qualifier 加了,没生效。
java
// 构造注入:注解写哪儿才算数
@Service
public class OrderService {
@Qualifier("localMenuService") // 无效:这里走的是构造器注入,字段注解不参与候选过滤
private final MenuService menuService;
public OrderService(MenuService menuService) {
this.menuService = menuService;
}
}
视频的结论是:@Qualifier 得写在构造函数的参数 上,因为依赖描述符(DependencyDescriptor)读的是参数位置的注解。改成手写构造器就对了:
java
public OrderService(@Qualifier("localMenuService") MenuService menuService) {
this.menuService = menuService;
}
但这位同学的构造器是 Lombok 在编译期生成的(@AllArgsConstructor / @RequiredArgsConstructor 那一类),根本没有代码位置让你去标参数注解。视频给的绕法是:把 @AllArgsConstructor 换成 @RequiredArgsConstructor ,这样只有 final(或 @NonNull)字段才会进构造器;那个需要精确装配的属性不加 final,从构造器名单里退出来,改成字段注入,然后在字段上放 @Autowired + @Qualifier。这是「放弃构造注入换可控注入点」的折中,能用,但等于放弃了 final 语义。
我的两条补充:一是手写构造器并显式标 @Qualifier,代价最小;二是 Lombok 有 lombok.copyableAnnotations 配置项,可以把字段上的注解复制到生成的构造器参数上(@Qualifier 是典型用法,但这一条我这次没能从官方文档逐字核实,以你项目实际 Lombok 版本为准)。
顺带一个容易答歪的点:@Primary 打在类上时,只有走组件扫描才有效;如果这个类是 XML <bean> 声明的,注解元数据会被忽略,得改 <bean primary="true"/>。这条出自 @Primary 5.3.39 javadoc。
6. 单例 Bean 线程安全吗:问题从「有状态」开始
P15 这题的评分点在于你敢不敢先说「不是线程安全的」,再补那个「但是」。
视频给的实验(下面代码是我按实验描述重写的,思路与视频一致):一个默认单例的 UserService,类里声明了一个成员变量并且存在读写操作;测试里从容器拿到同一个 UserService,起两个线程分别调 welcome("张三") 和 welcome("李四"),中间 sleep(100ms) 模拟业务耗时。
java
@Component
public class UserService {
private String username; // 共享成员变量:有状态的根源
public String welcome(String name) {
this.username = name; // 写
try { Thread.sleep(100); } catch (InterruptedException ignored) { }
return this.username; // 读
}
}
结果是两个线程都拿到「李四」------线程一先以微弱优势写入 张三,还没返回,线程二把 username 改成了 李四,于是两次读都读到后者。这就是典型的脏读,线程不安全。
视频接着给的转折是:把成员变量挪进方法里,同一个单例又变成线程安全的。因为局部变量在每条线程自己的栈帧上,不存在共享。他改完再跑一遍,输出正常。
所以答题的标准句式是这两句:单例 Bean 在类里声明了会读写的成员变量时 线程不安全,这叫「有状态」;把状态挪进方法参数、局部变量、返回值,Bean 就变成「无状态」,此时线程安全。视频最后那句提醒其实才是真正的答案落点:回想一下你平时写代码,很少会在 Bean 里声明会读写的成员变量------这也是为什么绝大多数 Spring Boot 应用跑在生产上没有踩到这个坑。
把场景铺开对照:
| 场景 | 是否安全 | 原因 | 处理办法 |
|---|---|---|---|
| 单例,无成员变量,只有方法内局部变量 | 安全 | 无共享可变量 | 默认写法,不用改 |
| 单例,成员变量只在初始化时写完、之后只读 | 安全 | 事实上的不可变 | 尽量 final,并保证发布安全 |
| 单例,成员变量存在读写 | 不安全 | 多线程读写同一实例字段 | 状态挪进方法,或用参数传递 |
| 单例,成员变量必须可写(计数、缓存) | 不安全 | 同上 | AtomicXxx / 加锁 / 换 ConcurrentHashMap |
多例(prototype),每次新实例 |
实例层面安全 | 线程各持一份,不共享 | 但 static 字段、外部资源照样共享 |
| 多例注入进单例里 | 不安全 | 单例只在装配时拿一次,之后所有线程共用那个实例 | ObjectFactory / @Lookup / 每请求手动 getBean |
这里要补一个官方口径。视频说「单例 Bean 肯定不是线程安全的」,这个说法要限定:容器不保证你业务字段的并发正确性,但也从没说 Bean 天生不安全 。作用域语义官方文档写得很清楚------singleton 的实例存在一张缓存里,后续所有对该名字的请求都返回这个缓存对象(这正是共享的来源);而 prototype 是「容器只负责实例化、配置、装配,然后交给调用方,之后不再记录这个实例」,所以初始化回调都会走,配置好的销毁回调不会走 ,需要客户端自己清理。@Bean 的 destroyMethod javadoc 里那句更直白:销毁回调「只在生命周期完全由容器托管的 Bean 上调用,singleton 一定满足,其他作用域不保证」。
Q4:既然有状态会出问题,为什么不干脆把 Bean 默认设成多例?
因为代价更贵。多例每次 getBean 都要走完实例化 → 属性赋值 → 初始化整条链,反射、依赖解析、AOP 代理都重来一遍;同时容器不留引用,销毁回调也不会执行,你持有的连接、线程池得自己关。而实际业务 Bean(Service、Dao)本来就是无状态的协作者,为极少数有状态场景把全部 Bean 改成多例,是把 99% 的开销买来给 1% 的场景兜底。真要按请求隔离状态,用 request 作用域或者把状态放到方法参数、ThreadLocal(用完记得 remove,线程池会复用线程)都比全量多例划算。
一句话理解三者的分工:容器负责「一个名字给你一个对象」,反射负责「把对象造出来」,线程安全是你自己的责任。
7. 面试现场:把这四问压成一段话
真面试里,这四道题大概率是连着一串追问下来的。我给一个可以直接背的收束版:
| 追问 | 30 秒答法 | 加分点 |
|---|---|---|
| 讲讲 Bean 生命周期 | 容器加载时按 BeanDefinition 创建,四大步:实例化、属性赋值、初始化、销毁;最后进 singletonObjects,容器 close() 才销毁 |
主动点出早期 Bean、循环依赖发生在属性赋值、AOP 代理在初始化之后 |
| 初始化里谁先谁后 | Aware 三件套 → @PostConstruct → 前置 BeanPostProcessor → afterPropertiesSet → init-method → 后置 BeanPostProcessor |
说明 @PostConstruct 本身也由一个 BeanPostProcessor 触发 |
| 单例 Bean 和单例模式区别 | 目标一样,维度不一样:单例模式以类为约束,单例 Bean 以 beanName 为主键 | 补一句同类不同名可以有多个实例 |
| 依赖注入注错对象 | 按类型找多个候选时,@Primary → @Priority → 名字兜底;名字匹配只是最后一级 |
指出 @Qualifier 是在候选过滤阶段生效,不是靠优先级 |
| Bean 线程安全吗 | 单例 + 可读写成员变量 = 有状态 = 不安全;状态挪进方法就是无状态、安全 | 顺势讲 prototype 的销毁回调不执行 |
背这段的时候注意一个节奏:先给结论骨架,再等面试官选方向深挖,别把六个回调一口气全倒出来------视频(P18)里那句「你回答得够细了,面试官一般不会追那么深」,反过来理解就是,你主动倒完反而容易被抓其中一条往深里问。
本文覆盖分 P
| 分 P | 视频原始标题 | 时长 | 本文对应小节 |
|---|---|---|---|
| P01 | Bean的生命周期 | 3:09 | §1、§2(早期 Bean、单例池、销毁时机)、§3(代理与入池) |
| P02 | 单例Bean和单例模型有什么区别 | 1:31 | §4 |
| P04 | 阿里程序员也一般啊,Bean的依赖注入问题很难吗 | 4:39 | §5 |
| P15 | Spring的Bean是线程安全的吗 | 6:44 | §6 |
| P17 | Bean的生命周期回调方法和执行顺序 | 6:20 | §3 |
| P18 | Bean的生命周期 | 10:22 | §1、§2、§7(四大步、Aware 回调、循环依赖锚点) |
最后总结
这组素材里最有价值的判断,是 P01 那句「Bean 不会自动销毁,GC 也管不到它」------它把单例池讲成了一个有主的东西,而不是抽象概念。很多人背生命周期背得滚瓜烂熟,却说不清 getBean 到底从哪里取对象,根子就在没把 singletonObjects 这张 Map 当回事。
我个人对两份答法的取舍是:短版(P01)适合当骨架,长版(P18)的四大步其实更适合口头输出 ,因为四大步能被追问拆开的层次刚好和源码里的 createBeanInstance / populateBean / initializeBean 对齐。唯一需要修正的是 P18 关于 AOP 代理创建时机的口误,它和 P01 打架,而 P01 是对的。
P04 让我觉得值得单独写一段的是:「by type 找不到就 by name」这种简化说法是有代价的 。真实规则里 @Primary 排在名字兜底之前,这正是那个线上「注入的不是我的 Bean」的成因;把简化版背熟的人解释不了这个现象,只有理解了候选过滤和候选决胜是两个阶段,才知道 @Qualifier 为什么能压住 @Primary。
至于 P15,我的结论比视频更收敛一点:这不是「单例不安全」的问题,而是「状态该放哪儿」的问题。Bean 是无状态协作者这个默认设计,是 Spring 有意为之的并发策略,不是巧合。
事实说明
-
视频里给的事实 (转写明确讲到,不代表我已核实):Bean 生命周期指从创建到销毁;
new ApplicationContext传配置类/XML 时开始逐个创建;实例化靠反射,另有静态工厂与实例工厂;早期 Bean 的待注入属性为null;初始化回调 Aware,之后一步创建 AOP 动态代理;单例池是名为singletonObjects的 Map,key 为 beanName,走 AOP 时 value 为代理对象;Bean 不会自动销毁也不受 GC 回收,需容器close();单例模式以类为维度、单例 Bean 以 beanName 为主键;自动装配先 by type 多个再 by name;@Qualifier指定名字后@Primary失效;构造注入时@Qualifier必须写在构造函数上,Lombok 生成的构造器可用@RequiredArgsConstructor+ 非final字段绕过;单例 Bean 有读写成员变量时线程不安全,挪进方法即安全;生命周期回调三种方式顺序为 注解 → 接口 → 指定属性,@Component无法配 init/destroy 属性。 -
官方文档 / 源码核实过的事实(本次亲自核对):
InitializingBean、@Primary、@Qualifier、@Bean四个类型的 javadoc 原文,版本 Spring Framework 5.3.39(链接见参考资料)。其中afterPropertiesSet()明确「在容器设完所有属性、并满足BeanFactoryAware、ApplicationContextAware之后调用」;@Bean的destroyMethod会自动推断公有无参的close/shutdown,destroyMethod=""关闭推断但不影响DisposableBean,且销毁回调只对容器完全托管的 Bean(singleton)生效。- 官方参考文档 Bean Scopes 一节:singleton 实例存于缓存并后续返回同一对象;prototype「容器实例化、配置、装配后交给客户端,此后不再记录该实例」,初始化回调所有作用域都执行、prototype 的销毁回调不执行、需客户端自行清理。
- v5.3.39 源码:
DefaultListableBeanFactory#determineAutowireCandidate的决胜顺序是@Primary→@Priority→ 候选名与注入点名匹配,@Qualifier的过滤发生在更早的findAutowireCandidates#isAutowireCandidate;DisposableBeanAdapter#destroy()的顺序是postProcessBeforeDestruction(@PreDestroy)→DisposableBean.destroy()→AutoCloseable.close()或自定义 destroy method;InitDestroyAnnotationBeanPostProcessor实现PriorityOrdered且默认 order 为LOWEST_PRECEDENCE。 - 源码骨架(v5.3.39 tag 拉取原文,并与本地 6.1.x、主干副本交叉比对,三者结构一致):
doCreateBean为createBeanInstance→ 单例工厂登记(addSingletonFactory+getEarlyBeanReference)→populateBean→initializeBean;initializeBean内为invokeAwareMethods(BeanName / BeanClassLoader / BeanFactory 三个接口)→applyBeanPostProcessorsBeforeInitialization→invokeInitMethods(先afterPropertiesSet,再自定义 init-method)→applyBeanPostProcessorsAfterInitialization;singletonObjects为ConcurrentHashMap;AbstractAutoProxyCreator在postProcessAfterInitialization调wrapIfNecessary,并在getEarlyBeanReference里也调它。 - 初始化侧的可背诵顺序:
setBeanName→setBeanFactory→setApplicationContext→@PostConstruct→postProcessBeforeInitialization→afterPropertiesSet→init-method→postProcessAfterInitialization;销毁侧为@PreDestroy→DisposableBean.destroy→destroy-method。大类次序(注解 → 接口 → 指定属性)与源码一致;其中setApplicationContext与@PostConstruct都落在前置 BeanPostProcessor 链内部,把它们单列成独立步骤是表达习惯,详见 Q1。三级缓存与 IoC 扩展点本篇未展开。
-
笔者判断 / 未核实:
- P01 与 P18 关于「AOP 代理在初始化之前还是之后创建」的口述互相矛盾,我按源码判定 P01 的「之后」为准,但这属于我的裁决,不是视频里明说的。
- 视频把自动装配简化为「by type 多个则 by name」,漏了
@Primary/@Priority排在名字兜底之前;我给出的四级顺序是从源码补全的。 - 「
@Qualifier让@Primary失效」的准确机制是候选过滤,属于我据源码得出的解释,视频只给了结论。 lombok.copyableAnnotations能把字段注解复制到生成的构造器参数上------机制存在,但我这次没能从官方文档逐字确认,未列入核实项。- P15 的实验输出依赖线程调度(视频里也是「以微弱优势/劣势」的说法),结果方向可复现、具体先后不可保证;我把代码写成
sleep(100)版本是笔者复述。 - 视频说「把成员变量声明在方法中单例就线程安全」,我的补充是这只解决共享问题,不解决
static字段、外部资源、以及 prototype 被单例持有后只装配一次的问题。 - Q2/Q3 里「
@Bean方法名与@Component默认值决定 beanName」「多个同类候选分不出胜负时抛NoUniqueBeanDefinitionException」是视频没讲的补充说明;异常类型与源码分支一致,但具体提示信息我这次没有跑起来验证。 @PostConstruct与自定义 BeanPostProcessor 的相对先后取决于注册顺序与 order 值;把流水线画成八个严格串行步骤是表达上的简化。- 单例 Bean 默认在容器启动时预实例化(
lazy-init例外)这一点本篇未展开核实。 javax.annotation→jakarta.annotation的命名空间迁移发生在 Spring 6 / Spring Boot 3,属常识性事实,本篇未附官方链接。- 本文代码块中标注「笔者补充/重写」的部分是我自己写的示意代码,视频里没有对应截图或运行结果。
参考资料 & 致谢
- 1 Spring面经-哔哩哔哩,77 个分 P 的 Spring 面经合集,本文取材其中 P01、P02、P04、P15、P17、P18 六段。
- 2 Spring Framework 官网 / Spring 项目主页。
- 3 Spring Framework 5.3.39 API:InitializingBean。
- 4 Spring Framework 5.3.39 API:Primary。
- 5 Spring Framework 5.3.39 API:Qualifier。
- 6 Spring Framework 5.3.39 API:Bean(initMethod / destroyMethod)。
- 7 Spring Framework Reference:Bean Scopes(singleton / prototype 生命周期)。
- 8 Spring Framework v5.3.39 源码:DefaultListableBeanFactory。
- 9 Spring Framework v5.3.39 源码:DisposableBeanAdapter。
- 10 Spring Framework v5.3.39 源码:InitDestroyAnnotationBeanPostProcessor。
- 11 Spring Framework v5.3.39 源码:AbstractAutowireCapableBeanFactory,以及主干上的 AbstractAutoProxyCreator 与 DefaultSingletonBeanRegistry。
- 12 Lombok 官方特性页,构造器生成与注解复制相关。
- 13 Spring Boot 项目主页。