Spring 面经系列 · 第 1 篇:Bean 生命周期、单例语义、依赖注入与线程安全

💡 「你讲讲 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 上没有这个属性可填

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

  1. 销毁回调必须关容器才看得见。 演示里 @PreDestroy 的输出是在调用容器 close() 之后才打出来的,这跟第 1 节讲的「单例不会自动销毁」是同一条事实的两个侧面。
  2. 属性式配置只能挂在 @Bean 或 XML 上。 因为 @Component 注解本身没有 initMethod、destroyMethod 可以填;@Bean 有。我查了 5.3.39 的 @Bean javadoc,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 的名字不一样,容器就会实例化出多个对象。

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

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 小时前
Java 程序员的 IoT 协议入门(一):物联网平台到底在做什么
java
const_qiu1 小时前
第四章 07:4.7 本章小结
java·运维·服务器·人工智能
鶴哥只手遮天1 小时前
Spring AI 2.0.1 深度解析-第 22 章 Spring Boot 集成:自动配置、Properties 与条件装配
java·人工智能·开源
himobrinehacken2 小时前
位运算揭秘:左右移与符号位陷阱
c语言·c++·算法·安全
WJ.Polar2 小时前
final、单例类、枚举类、抽象类、接口
java·开发语言
江湖有缘2 小时前
Docker实战 | 使用Docker部署Imagor Studio图片编辑工具
java·docker·eureka
mmsx2 小时前
Android 安全校验下沉到原生层:防 patch 与构建期自动化
android·安全·自动化
上海广测检测科技有限公司2 小时前
无线鼠标出口欧盟CE认证准备要点:RED指令下的射频、安全与电池义务梳理
经验分享·安全