Spring 常见高频问题与坑点
❓ 面试官:Spring 框架是怎么解决循环依赖问题的?
频率:🔥🔥🔥🔥🔥
💡 一句话总结(先抛结论): Spring 只能解决单例模式(Singleton)下的属性注入(或 Setter 注入)导致的循环依赖。它的核心底层机制是 “三级缓存”。如果遇到构造器注入或者原型模式(Prototype)的循环依赖,Spring 依然会直接报错抛出异常。
📝 详细原理解析(三级缓存是什么?): 假设 A 依赖 B,B 又依赖 A。
- 第一级缓存(singletonObjects):存放完全初始化好的成品的 Bean(我们日常
getBean拿到的就是它)。 - 第二级缓存(earlySingletonObjects):存放半成品的 Bean(已经实例化,但还没进行属性注入)。
- 第三级缓存(singletonFactories):存放可以生成半成品 Bean 的工厂(Factory / Lambda 表达式)。
经典推演流程(破局之道:提前暴露半成品):
- 实例化 A(
new A()),此时 A 是个空壳子(半成品)。 - Spring 把 A 包装进一个对象工厂(第三级缓存)里提前暴露出去。
- A 开始属性注入,发现自己需要 B。此时去单例池找,没找到 B。
- Spring 转头去实例化 B(
new B()),同样是个空壳子。 - B 开始属性注入,发现自己需要 A。
- B 去找 A,发现在一二级缓存都没有,最终在第三级缓存里找到了 A 的工厂。
- B 通过执行该工厂方法,把 A 的早期引用提前拿了出来,放入第二级缓存。此时 B 成功拿到了“还没完全装配好的 A”,完成了属性注入。
- B 完成所有初始化,变成成品,放入第一级缓存。
- 回到 A 的注入过程,此时 A 发现 B 已经在第一级缓存里了,直接拿到成品 B 注入。
- A 也完成初始化,放入第一级缓存,完美闭环!
🌟 面试加分项(为什么非要三级?二级不行吗?): "其实如果只是普通的循环依赖,二级缓存就足够破局了(提前暴露半成品)。之所以必须用三级缓存(对象工厂),完全是为了处理 AOP(代理对象)。 如果 A 被切面代理了,最终放进单例池的应该是 A 的代理对象,而不是原生 A。 但 AOP 代理逻辑通常是在 Bean 生命周期的最后一步(初始化后)才执行的。如果在循环依赖中,B 提前要用 A,这时候 B 拿到的是原生 A。等 A 最后自己走完 AOP 变成了代理对象,这就乱套了。 所以三级缓存的工厂方法起到了一个兜底作用:如果有 AOP,该工厂会在 B 需要 A 的时候,立刻、提前去执行代理生成逻辑,把 A 的代理对象提前造出来返回给 B。这样就保证了单例的绝对一致性。"
❓ 面试官:如果我必须用构造器注入,又发生了循环依赖,该怎么解决?【中级】
频率:🔥🔥🔥
💡 一句话总结(先抛结论): 因为构造器注入在 new 对象的那一瞬间就需要所有依赖,所以 Spring 的“提前暴露半成品”机制完全失效(连半壳子都没造出来)。解决办法通常是:重构代码解耦 或者 使用 @Lazy 注解。
📝 详细解决方案:
- 方案一(官方推荐):重构设计
- 出现循环依赖通常意味着设计不合理(比如订单 Service 和支付 Service 强耦合)。应该抽出一个公共的、不依赖这俩类的第三方 Service。
- 方案二:打上
@Lazy注解- 做法:在其中一个类的构造器参数上加上
@Lazy(比如在 A 依赖 B 的时候:public A(@Lazy B b))。 - 原理:加上
@Lazy后,Spring 在给 A 注入 B 的时候,并不会立刻去真实创建 B,而是塞给 A 一个基于 CGLib 生成的 B 的代理对象。等 A 在以后真正调用b.doSomething()时,这个代理对象才会去触发真实的 B 的实例化。通过这种“拖延战术”完美避开了启动时的死结。
- 做法:在其中一个类的构造器参数上加上
❓ 面试官:Spring 里的 @Autowired 和 @Resource 有什么区别?
频率:🔥🔥🔥🔥
💡 一句话总结(先抛结论):
@Autowired是 Spring 官方自己提供的注解;它默认按类型(byType)去匹配寻找 Bean。@Resource是 JDK/J2EE(JSR-250 规范) 提供的注解;它默认按名称(byName)去匹配寻找 Bean。
📝 详细原理解析(坑点):
@Autowired的寻找逻辑:- 先根据类型去找。如果容器里只有一个该类型的实现类,直接注入成功。
- 如果发现容器里有两个该类型的实现类(比如接口
PaymentService有AliPay和WechatPay两个实现类),它就会降级,转而根据你的变量名去匹配。如果你变量名叫aliPay,且刚好有个 Bean 的名字也是aliPay,就注入成功;如果不匹配,直接抛异常。 - 解决方案:通常配合
@Qualifier("beanName")一起使用,强制指定 Bean 名字。
@Resource的寻找逻辑:- 如果你在注解里写了
@Resource(name = "aliPay"),就绝对按这个名字找,找不到就报错。 - 如果你什么都不写,它先根据变量名当做 Bean 名字去找;如果变量名找不到,它就降级,按类型去找。
- 如果你在注解里写了
结论:在实际开发中,如果只有一个实现类,用哪个都行;如果有多个实现类,推荐使用 @Resource(name="xxx"),比写两个注解(@Autowired + @Qualifier)更简洁。