☰
Spring 面经系列 · 第 1 篇:Bean 生命周期、单例语义、依赖注入与线程安全
2026/10/20 12:25:39 网站建设 项目流程

💡「你讲讲 Bean 的生命周期」这道题,八成的人是从实例化背到销毁,背到「初始化」就断了——因为初始化这一步里塞了 Aware 回调、@PostConstruct、InitializingBean、initMethod、BeanPostProcessor、AOP 代理六个东西,谁先谁后说不清。更麻烦的是,视频里同一道题给了短版和长版两套答法,面试官接着追问的三刀往往在这:单例 Bean 和单例模式到底有什么区别、@Autowired为什么把别人家的 Bean 注进来了、单例 Bean 到底是不是线程安全的。这一篇把六个分 P 的素材合并成一条主线,顺序按 Spring Framework 5.3.x 的实际调用链核对过,能背也能解释。


1. Bean 生命周期到底指什么:一条主线,四大步

视频里短版(P01)和长版(P18)问的是同一件事,答案口径完全一致:Bean 的生命周期就是一个 Bean 从创建到销毁的整个流程,说白了是「从生到死」。长版给的记忆抓手是四大步:

实例化 ──► 属性赋值 ──► 初始化 ──► 销毁 (new 出对象) (DI 填依赖) (回调+AOP) (容器关闭)

短版则强调另外两个容易漏讲的点:什么时候开始创建,以及什么时候销毁。

关于创建时机,视频的表述是:容器加载的时候开始创建。你new一个AnnotationConfigApplicationContext(传配置类)或者ClassPathXmlApplicationContext(传 XML),底层就根据你配置的 Bean 定义一个一个地开始创建。这句话很重要,因为 Bean 生命周期本身就发生在 IoC 容器的加载过程里——IoC 加载过程说白了就是一堆 Bean 的创建过程,两道题是一回事。

四大步各自在干什么:

步骤容器做了什么关键动作你能感知到什么常见漏答点
实例化根据 Bean 的类,通过反射推断构造函数并newcreateBeanInstance拿到一个「早期对象」实例化不止反射一条路,静态工厂、实例工厂也在这一步
属性赋值解析依赖并注入populateBean@Autowired、@Resource、@Value生效循环依赖就是在这一步炸的
初始化回调一堆扩展点initializeBeanAware 接口、生命周期回调、AOP 代理这一步不是「调一个 init 方法」,是一整条链
销毁容器关闭时回调销毁方法destroy()@PreDestroy、DisposableBean、destroyMethod只发生在容器关闭,不发生在加载过程

这里有个概念必须单独点出来:早期 Bean 对象。视频(P01)给的说法是,实例化刚结束得到的那个对象叫早期对象,因为它还没经过依赖注入,要注入的属性还是null。这个命名不是学术词汇,它是为后面的循环依赖服务的——三级缓存要提前暴露出去的就是这个早期对象。三级缓存和 IoC 的扩展点属于系列其他分篇,这里只标个锚点。

一句话理解:四大步是骨架,initializeBean里的六个回调是血肉,背下骨架只能及格,讲清血肉才是看过源码。

关于销毁,短版里那段回答我认为是全篇最值钱的部分:Bean 不会自动销毁。它不像普通对象那样被垃圾回收器回收,因为默认作用域是单例,单例对象会被缓存进容器的一张 Map 里,这张 Map 一直强引用着它。除非你主动调用容器的close()方法,否则它一路活到进程结束。


2. 把四大步摊开:容器的真实调用顺序

四大步太粗,面试官会追「初始化里到底谁先谁后」。这一节给一条完整的横向流水线,顺序以 5.3.x 的实际创建链路为准。

容器加载(读 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 = "...")/ XMLinit-method@Bean(destroyMethod = "...")/ XMLdestroy-method只能用@Bean或 XML最后方法名随便取;@Component上没有这个属性可填

视频里给的操作细节有三条值得记:

  1. 销毁回调必须关容器才看得见。演示里@PreDestroy的输出是在调用容器close()之后才打出来的,这跟第 1 节讲的「单例不会自动销毁」是同一条事实的两个侧面。
  2. 属性式配置只能挂在@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接口那条路依然生效。
  3. 三种方式实测的输出顺序就是 注解 → 接口 → 属性,初始化侧和销毁侧各自按这个次序。销毁侧我用源码验证过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 的名字不一样,容器就会实例化出多个对象。

下面这段是我自己补的例子,视频里没有代码:

// 笔者补充示例:同一个类注册成两个单例 Bean@ConfigurationpublicclassDemoConfig{@Bean("cashierService")publicUserServicecashier(){returnnewUserService();}@Bean("auditService")publicUserServiceaudit(){returnnewUserService();}}

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。

// 笔者按视频思路重写的伪代码,视频里用的两个 bean 名是中文人名,这里换成中性名@ConfigurationpublicclassMenuConfig{@Bean("localMenuService")publicMenuServicelocalMenuService(){returnnewLocalMenuServiceImpl();}}@RestControllerpublicclassMenuController{@AutowiredprivateMenuServicelocalMenuService;// 名字对得上,但注进来的不是它}

调试结果(视频里的现象):注入点拿到的是第三方 jar 里的那个 Bean。原因是那个包的自动配置类也注册了一个同类型的MenuService,并且加了@Primary。

要解释这个现象,得把 Spring 的自动装配规则讲准。视频给的版本是「先按类型找,找到多个就按名字匹配,名字匹配上就注入,匹配不上就报多个候选无法装配」。这个说法方向对,但把@Primary的位置漏了。我把 v5.3.39 的DefaultListableBeanFactory源码拉下来对过,实际判定链是这样的:

findAutowireCandidates(按类型收集候选) │ isAutowireCandidate(candidate, descriptor) 逐个过滤 ──► @Qualifier 在这一步起作用 ▼ 候选 > 1 │ ├─ ① determinePrimaryCandidate ──► @Primary 命中就直接返回 ├─ ② determineHighestPriorityCandidate ──► 其次看 @Priority └─ ③ matchesBeanName(候选名, 注入点名字) ──► 最后才按名字兜底

关键点:按名字匹配是第 ③ 级兜底,排在@Primary后面。所以你只把字段名写成和 beanName 一样是不够的,只要候选里有一个@Primary,它先赢。这就把视频里那个「明明匹配上了为什么没注进来」解释干净了。

解法也是视频给的:在注入点加@Qualifier指定 beanName。它的原理不是「优先级更高」,而是它作用在候选收集阶段——带@Qualifier的注入点会把名字不匹配的候选直接从findAutowireCandidates的过滤里踢掉,@Primary那个候选连进决赛的资格都没有,自然谈不上生效。这个解释比「@Qualifier优先于@Primary」更准,也和@Primaryjavadoc 的描述一致(@Primary只在「多个候选都能装进这个单值依赖」时才给偏好,而@Qualifier已经把候选集缩到一个了)。

Q3:字段名和 beanName 完全一样,为什么不等于按名字注入成功?

因为「名字一样」只在两个地方有用:一是注册阶段,@Bean方法名或@Component的默认值决定这个 Bean 在容器里叫什么;二是决胜阶段的最后一级兜底。而决胜是分层的——@Primary的候选在第一层就返回了,压根轮不到名字比对。所以「我把字段名改成和 beanName 一样」这个操作,在没有@Primary干扰时有效,在有@Primary干扰时完全无效。真正表达「我就要这个名字」的语法是@Qualifier,不是靠同名字段的隐式约定。

第二个场景是视频里另一位同学踩的坑:@Qualifier加了,没生效。

// 构造注入:注解写哪儿才算数@ServicepublicclassOrderService{@Qualifier("localMenuService")// 无效:这里走的是构造器注入,字段注解不参与候选过滤privatefinalMenuServicemenuService;publicOrderService(MenuServicemenuService){this.menuService=menuService;}}

视频的结论是:@Qualifier得写在构造函数的参数上,因为依赖描述符(DependencyDescriptor)读的是参数位置的注解。改成手写构造器就对了:

publicOrderService(@Qualifier("localMenuService")MenuServicemenuService){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"/>。这条出自@Primary5.3.39 javadoc。


6. 单例 Bean 线程安全吗:问题从「有状态」开始

P15 这题的评分点在于你敢不敢先说「不是线程安全的」,再补那个「但是」。

视频给的实验(下面代码是我按实验描述重写的,思路与视频一致):一个默认单例的UserService,类里声明了一个成员变量并且存在读写操作;测试里从容器拿到同一个UserService,起两个线程分别调welcome("张三")和welcome("李四"),中间sleep(100ms)模拟业务耗时。

@ComponentpublicclassUserService{privateStringusername;// 共享成员变量:有状态的根源publicStringwelcome(Stringname){this.username=name;// 写try{Thread.sleep(100);}catch(InterruptedExceptionignored){}returnthis.username;// 读}}

结果是两个线程都拿到「李四」——线程一先以微弱优势写入张三,还没返回,线程二把username改成了李四,于是两次读都读到后者。这就是典型的脏读,线程不安全。

视频接着给的转折是:把成员变量挪进方法里,同一个单例又变成线程安全的。因为局部变量在每条线程自己的栈帧上,不存在共享。他改完再跑一遍,输出正常。

所以答题的标准句式是这两句:单例 Bean在类里声明了会读写的成员变量时线程不安全,这叫「有状态」;把状态挪进方法参数、局部变量、返回值,Bean 就变成「无状态」,此时线程安全。视频最后那句提醒其实才是真正的答案落点:回想一下你平时写代码,很少会在 Bean 里声明会读写的成员变量——这也是为什么绝大多数 Spring Boot 应用跑在生产上没有踩到这个坑。

把场景铺开对照:

场景是否安全原因处理办法
单例,无成员变量,只有方法内局部变量安全无共享可变量默认写法,不用改
单例,成员变量只在初始化时写完、之后只读安全事实上的不可变尽量final,并保证发布安全
单例,成员变量存在读写不安全多线程读写同一实例字段状态挪进方法,或用参数传递
单例,成员变量必须可写(计数、缓存)不安全同上AtomicXxx/ 加锁 / 换ConcurrentHashMap
多例(prototype),每次新实例实例层面安全线程各持一份,不共享但static字段、外部资源照样共享
多例注入进单例里不安全单例只在装配时拿一次,之后所有线程共用那个实例ObjectFactory/@Lookup/ 每请求手动getBean

这里要补一个官方口径。视频说「单例 Bean 肯定不是线程安全的」,这个说法要限定:容器不保证你业务字段的并发正确性,但也从没说 Bean 天生不安全。作用域语义官方文档写得很清楚——singleton 的实例存在一张缓存里,后续所有对该名字的请求都返回这个缓存对象(这正是共享的来源);而 prototype 是「容器只负责实例化、配置、装配,然后交给调用方,之后不再记录这个实例」,所以初始化回调都会走,配置好的销毁回调不会走,需要客户端自己清理。@Bean的destroyMethodjavadoc 里那句更直白:销毁回调「只在生命周期完全由容器托管的 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视频原始标题时长本文对应小节
P01Bean的生命周期3:09§1、§2(早期 Bean、单例池、销毁时机)、§3(代理与入池)
P02单例Bean和单例模型有什么区别1:31§4
P04阿里程序员也一般啊,Bean的依赖注入问题很难吗4:39§5
P15Spring的Bean是线程安全的吗6:44§6
P17Bean的生命周期回调方法和执行顺序6:20§3
P18Bean的生命周期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 项目主页。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询