Skip to content

Spring 常见高频问题与坑点 ​


❓ 面试官:Spring 框架是怎么解决循环依赖问题的? ​

频率:🔥🔥🔥🔥🔥

💡 一句话总结(先抛结论): Spring 只能解决单例模式(Singleton)下的属性注入(或 Setter 注入)导致的循环依赖。它的核心底层机制是 “三级缓存”。如果遇到构造器注入或者原型模式(Prototype)的循环依赖,Spring 依然会直接报错抛出异常。

📝 详细原理解析(三级缓存是什么?): 假设 A 依赖 B,B 又依赖 A。

  1. 第一级缓存(singletonObjects):存放完全初始化好的成品的 Bean(我们日常 getBean 拿到的就是它)。
  2. 第二级缓存(earlySingletonObjects):存放半成品的 Bean(已经实例化,但还没进行属性注入)。
  3. 第三级缓存(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 注解。

📝 详细解决方案:

  1. 方案一(官方推荐):重构设计
    • 出现循环依赖通常意味着设计不合理(比如订单 Service 和支付 Service 强耦合)。应该抽出一个公共的、不依赖这俩类的第三方 Service。
  2. 方案二:打上 @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。

📝 详细原理解析(坑点):

  1. @Autowired 的寻找逻辑:
    • 先根据类型去找。如果容器里只有一个该类型的实现类,直接注入成功。
    • 如果发现容器里有两个该类型的实现类(比如接口 PaymentService 有 AliPay 和 WechatPay 两个实现类),它就会降级,转而根据你的变量名去匹配。如果你变量名叫 aliPay,且刚好有个 Bean 的名字也是 aliPay,就注入成功;如果不匹配,直接抛异常。
    • 解决方案:通常配合 @Qualifier("beanName") 一起使用,强制指定 Bean 名字。
  2. @Resource 的寻找逻辑:
    • 如果你在注解里写了 @Resource(name = "aliPay"),就绝对按这个名字找,找不到就报错。
    • 如果你什么都不写,它先根据变量名当做 Bean 名字去找;如果变量名找不到,它就降级,按类型去找。

结论:在实际开发中,如果只有一个实现类,用哪个都行;如果有多个实现类,推荐使用 @Resource(name="xxx"),比写两个注解(@Autowired + @Qualifier)更简洁。