Spring Bean的一些Tricky问题

本文最后更新于 2025年9月13日 下午

事先声明:我没有看过什么网上的视频Spring教程,苍穹外卖项目的视线我也不是跟着教程走的,基本上就只看了第一部分讲解怎么搞CRUD和各个层的交互,后面就只看了具体模块需求,然后剩下的就自己做了。所以,我不确定我下面讲的这些东西是否已经在其他很热门的教程中已经讲过了。但是,毕竟是在实践过程中发现的问题,所以这里给大家分享一下。

Spring Bean 的循环依赖

Spring的循环依赖是一个很热门的问题,面试中也经常会被问到相关概念和解决方案。相信大家基本上都是会答以下内容:

  • 一般都会会答:三级缓存
    • 只对单例Bean有效,prototype模式下的Bean会直接失效(从三个缓存Mapper的命名就能看出来)
  • 更了解的可能会回答:Spring的默认配置中把allow-circular-references设置为false了,所以在默认情况下一见到循环依赖就会直接报错终止程序。将其设为true后才会进行三级缓存的流程。

当然,都没有毛病。不过我们平常做项目的时候,可能会在定义一个对象时调用其他库的内容,比如Lombok的的@Data。想象一下,以下代码的运行结果是什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Data
@Component
public class ObjectA{
@Autowired
private ObjectB objectB;
}

@Data
@Component
public class ObjectB{
@Autowired
private ObjectA ObjectA;
}

看起来没什么问题,不是吗?但是在运行过后,会出现以下报错:

1
2
3
4
5
6
// 前略,总体就是说Bean A的创建失败
Caused by: java.lang.StackOverflowError: null
at com.clain.study.Entity.ObjectA.hashCode(ObjectA.java:10) ~[classes/:na]
at com.clain.study.Entity.ObjectB.hashCode(ObjectB.java:10) ~[classes/:na]
at com.clain.study.Entity.ObjectA.hashCode(ObjectA.java:10) ~[classes/:na]
at com.clain.study.Entity.ObjectB.hashCode(ObjectB.java:10) ~[classes/:na]

能看出来是hashCode()这个方法发生了循环。为什么?

@Data会自动为对象生成

  • getter / setter 方法
  • equals() 方法
  • hashCode() 方法
  • toString() 方法

Bean初始化后的步骤:在Bean完全初始化后,Spring或其他组件(可能是日志、监控、或容器本身的某些后期处理)可能会调用Bean的 toString()hashCode() 方法。

Lombok 的默认行为@Data 生成的 hashCode()toString() 方法会递归地包含所有字段

  • 当调用 objectA.hashCode() 时,Lombok生成的代码会去计算其字段 objectB 的哈希码。
  • 这就调用了 objectB.hashCode()
  • objectB.hashCode() 又会去计算其字段 objectA 的哈希码。
  • 这就又调用了 objectA.hashCode()
  • …如此无限循环,直到栈溢出 (StackOverflowError)。

toString() 方法也是同样的原理,会尝试递归地拼接所有字段的字符串表示。

因此,虽然问题并不是发生在三级缓存没有生效上,但是依然会出现无限递归的问题。

解决方法

方法一:使用 @ToString@EqualsAndHashCode 注解替代 @Data

1
2
3
4
5
6
7
8
9
10
11
12
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
//@Data // 移除这个
@ToString(exclude = "objectB") // 生成toString时排除objectB字段
@EqualsAndHashCode(exclude = "objectB") // 生成hashCode和equals时排除objectB字段
@Getter // 生成getter
@Setter // 生成setter
@Slf4j
public class ObjectA {
@Autowired
private ObjectB objectB;
}

方法二:手动实现 toString()hashCode()

如果您不想依赖Lombok的魔法,可以自己手动实现这些方法,避免循环引用。

java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Component
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
@Data // 保留@Data
@Slf4j
public class ObjectA {
@Autowired
private ObjectB objectB;

// 手动覆盖toString,避免循环
@Override
public String toString() {
return "ObjectA{" +
"objectB=" + (objectB != null ? "ObjectB@” + System.identityHashCode(objectB) : null) +
'}';
}

// 手动覆盖hashCode,避免循环
@Override
public int hashCode() {
return super.hashCode(); // 直接使用对象的默认hashCode,不基于字段计算
}
}

也许还有其他方法,但这里只讲这两种。


Spring Bean的一些Tricky问题
http://example.com/2025/09/13/Spring学习/Spring Bean的一些Tricky问题/
作者
Clain Chen
发布于
2025年9月13日
许可协议