背景
在 Spring 项目中,我们经常会使用 Lombok 的 @RequiredArgsConstructor 来简化构造器注入代码,例如:
java
@RequiredArgsConstructor
@Service
public class BusinessOrderSplitServiceImpl implements BusinessOrderSplitService {
private final IMaterialInventoryService materialInventoryService;
}
这种写法本质上会由 Lombok 自动生成构造方法:
java
public BusinessOrderSplitServiceImpl(IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
}
如果项目中存在两个 Service 互相依赖,例如:
text
BusinessOrderSplitServiceImpl
-> IMaterialInventoryService
MaterialInventoryServiceImpl
-> BusinessOrderSplitService
就可能出现 Spring 构造器循环依赖问题。
为了解决这个问题,我们通常会想到给其中一个依赖加上 @Lazy:
java
@RequiredArgsConstructor
@Service
public class BusinessOrderSplitServiceImpl implements BusinessOrderSplitService {
@Lazy
private final IMaterialInventoryService materialInventoryService;
}
但这里有一个非常容易踩坑的地方:
字段上有
@Lazy,不代表构造器参数上也有@Lazy。
而对于 Spring 构造器注入来说,真正关键的是:
java
public BusinessOrderSplitServiceImpl(@Lazy IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
}
也就是说,@Lazy 必须出现在构造方法参数上,Spring 才能在构造器注入时为该依赖创建延迟代理,从而打破循环依赖。
问题代码
最初代码如下:
java
@RequiredArgsConstructor
@Service
public class BusinessOrderSplitServiceImpl implements BusinessOrderSplitService {
@Lazy
private final IMaterialInventoryService materialInventoryService;
}
看起来没问题,但实际启动时仍然可能出现循环依赖异常。
原因在于 Lombok 生成构造器时,默认不一定会把字段上的 @Lazy 复制到构造方法参数上。
也就是说,最终生成的字节码可能接近于:
java
public BusinessOrderSplitServiceImpl(IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
}
而不是:
java
public BusinessOrderSplitServiceImpl(@Lazy IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
}
这两者对 Spring 来说是完全不同的。
第一次排查:字段上的 @Lazy 有,但构造器参数上没有
通过反编译 .class 文件可以看到,第一次生成的 class 中字段上确实存在 @Lazy:
java
private final com.wuyuan.industry.service.IMaterialInventoryService materialInventoryService;
RuntimeVisibleAnnotations:
org.springframework.context.annotation.Lazy
这说明源码中的字段注解被保留了。
但是构造方法参数上没有看到类似下面的内容:
java
RuntimeVisibleParameterAnnotations:
parameter 13:
org.springframework.context.annotation.Lazy
这说明 Lombok 没有把字段上的 @Lazy 复制到构造器参数上。
此时生成效果相当于:
java
@RequiredArgsConstructor
@Service
public class BusinessOrderSplitServiceImpl implements BusinessOrderSplitService {
@Lazy
private final IMaterialInventoryService materialInventoryService;
public BusinessOrderSplitServiceImpl(IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
}
}
虽然字段上有 @Lazy,但构造器注入时 Spring 看到的参数并没有 @Lazy。
所以这个时候:
text
字段 @Lazy:有
构造器参数 @Lazy:没有
用于解决构造器循环依赖:没有真正生效
根因:lombok.config 放错了位置
一开始 lombok.config 放在了:
text
src/main/resources/lombok.config
这个位置通常是不对的。
因为 lombok.config 是编译期给 Lombok 读取的配置文件,不是 Spring 运行时读取的资源文件。
Lombok 在编译 Java 源码时,需要从源码所在目录向上查找 lombok.config。如果配置文件放在 src/main/resources 目录下,它通常无法覆盖到 src/main/java 下的源码。
错误结构:
text
your-module/
├── pom.xml
└── src/
└── main/
├── java/
│ └── com/example/service/BusinessOrderSplitServiceImpl.java
└── resources/
└── lombok.config
正确结构应该是:
text
your-module/
├── pom.xml
├── lombok.config
└── src/
└── main/
├── java/
│ └── com/example/service/BusinessOrderSplitServiceImpl.java
└── resources/
也就是:
text
lombok.config 和 src 平级
正确配置
在模块根目录新增或修改 lombok.config:
properties
config.stopBubbling = true
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy
完整目录结构如下:
text
your-module/
├── pom.xml
├── lombok.config
└── src/
└── main/
├── java/
└── resources/
其中关键配置是:
properties
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy
它的作用是告诉 Lombok:
当字段上有
@Lazy注解时,请把这个注解复制到生成的构造方法参数上。
修改后的代码
源码保持如下即可:
java
@RequiredArgsConstructor
@Service
public class BusinessOrderSplitServiceImpl implements BusinessOrderSplitService {
@Lazy
private final IMaterialInventoryService materialInventoryService;
}
java
@Lazy
private final IMaterialInventoryService materialInventoryService;
重新编译
修改 lombok.config 后,一定要重新 clean 编译。
Maven 项目执行:
bash
mvn clean compile
Gradle 项目执行:
bash
./gradlew clean compileJava
如果使用 IDEA,也建议执行一次:
text
Build -> Rebuild Project
必要时还可以删除 target 或 build 目录后重新编译。
如何验证是否真的生效?
可以使用 javap 查看编译后的 class 文件。
示例命令:
bash
javap -v -p target/classes/com/wuyuan/industry/service/impl/BusinessOrderSplitServiceImpl.class
重点观察构造方法部分。
如果没有生效,通常只能看到字段上的注解:
java
private final com.wuyuan.industry.service.IMaterialInventoryService materialInventoryService;
RuntimeVisibleAnnotations:
org.springframework.context.annotation.Lazy
但是构造器参数上没有:
java
RuntimeVisibleParameterAnnotations
如果生效了,应该能看到类似内容:
java
RuntimeVisibleParameterAnnotations:
parameter 13:
0: #1257()
org.springframework.context.annotation.Lazy
这就说明 @Lazy 已经被 Lombok 复制到了构造方法参数上。
本次最终验证结果
调整后,把 lombok.config 放到和 src 平级的位置,然后重新编译。
重新检查生成的 BusinessOrderSplitServiceImpl.class,可以看到:
java
RuntimeVisibleParameterAnnotations:
parameter 13:
0: #1257()
org.springframework.context.annotation.Lazy
其中 parameter 13 对应的是构造方法中的第 14 个参数。
因为参数索引是从 0 开始计算的:
text
parameter 0
parameter 1
parameter 2
...
parameter 13
而第 14 个参数正好是:
java
IMaterialInventoryService materialInventoryService
同时,字节码中也可以看到该参数被赋值给字段:
java
putfield materialInventoryService:Lcom/wuyuan/industry/service/IMaterialInventoryService;
因此最终结论是:
text
字段 @Lazy:有
构造器参数 @Lazy:有
@RequiredArgsConstructor + lombok.copyableAnnotations:已生效
也就是说,现在生成的构造方法效果已经接近于:
java
public BusinessOrderSplitServiceImpl(
...,
@Lazy IMaterialInventoryService materialInventoryService,
...
) {
this.materialInventoryService = materialInventoryService;
}
这才是 Spring 构造器注入场景下真正需要的形式。
为什么构造器参数上的 @Lazy 才关键?
对于构造器注入来说,Spring 创建 Bean 时会分析构造方法参数。
例如:
java
public BusinessOrderSplitServiceImpl(IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
}
Spring 会立即尝试创建 IMaterialInventoryService 对应的 Bean。
如果 IMaterialInventoryService 的实现类又依赖当前 Bean,就会形成循环:
text
BusinessOrderSplitServiceImpl
-> MaterialInventoryServiceImpl
-> BusinessOrderSplitServiceImpl
此时 Spring 无法完成构造器注入。
如果构造方法参数上加了 @Lazy:
java
public BusinessOrderSplitServiceImpl(@Lazy IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
}
Spring 注入的就不是目标 Bean 本身,而是一个延迟代理对象。
这个代理对象可以先注入进来,等真正调用方法时再去解析目标 Bean。
因此可以打破初始化阶段的循环依赖。
推荐解决方案一:使用 Lombok 配置复制注解
这是本次采用的方案。
lombok.config
properties
config.stopBubbling = true
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy
Java 代码
java
@RequiredArgsConstructor
@Service
public class BusinessOrderSplitServiceImpl implements BusinessOrderSplitService {
@Lazy
private final IMaterialInventoryService materialInventoryService;
}
生成效果
java
public BusinessOrderSplitServiceImpl(@Lazy IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
}
优点:
text
1. 保留 final 字段
2. 保留构造器注入
3. 保留 Lombok 简洁写法
4. 不需要手写构造方法
推荐解决方案二:手写构造方法
如果不想依赖 Lombok 配置,也可以直接手写构造方法。
java
@Service
public class BusinessOrderSplitServiceImpl implements BusinessOrderSplitService {
private final IMaterialInventoryService materialInventoryService;
public BusinessOrderSplitServiceImpl(
@Lazy IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
}
}
这种方式最直观,也最不容易有歧义。
Spring 一定可以看到构造方法参数上的 @Lazy。
缺点是如果依赖很多,构造方法会比较长。
常见错误写法
错误一:只在字段上加 @Lazy,但没有配置 Lombok
java
@RequiredArgsConstructor
@Service
public class BusinessOrderSplitServiceImpl implements BusinessOrderSplitService {
@Lazy
private final IMaterialInventoryService materialInventoryService;
}
如果没有配置:
properties
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy
那么 Lombok 生成的构造器参数上可能没有 @Lazy。
最终效果可能只是:
java
public BusinessOrderSplitServiceImpl(IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
}
这种情况下无法解决构造器循环依赖。
错误二:把 lombok.config 放到 resources 目录
错误位置:
text
src/main/resources/lombok.config
正确位置:
text
lombok.config
src/
pom.xml
也就是:
text
lombok.config 和 src 平级
错误三:修改配置后没有 clean 编译
修改 lombok.config 后,如果没有重新 clean 编译,旧的 class 文件可能仍然存在。
建议执行:
bash
mvn clean compile
或者:
bash
./gradlew clean compileJava
如果 @Lazy 已经生效,但循环依赖仍然报错怎么办?
如果通过 javap 已经确认构造器参数上有 @Lazy,但项目启动仍然报循环依赖,需要继续检查其他问题。
可能原因包括:
1. 循环依赖链路不是这一条
例如你只给这条依赖加了 @Lazy:
text
BusinessOrderSplitServiceImpl
-> IMaterialInventoryService
但实际报错链路可能是另一条:
text
AService
-> BService
-> CService
-> AService
这种情况需要根据 Spring 启动日志中的完整循环依赖链路继续排查。
2. 还有其他构造器循环依赖没有处理
一个类中可能依赖很多 Service。
即使 materialInventoryService 加了 @Lazy,其他依赖也可能存在循环。
需要结合错误日志里的依赖链路判断到底是哪一个 Bean 形成了循环。
3. 初始化阶段提前调用了懒加载对象
@Lazy 只能延迟 Bean 的真正解析。
但如果你在初始化阶段就调用了它的方法,仍然可能触发循环依赖。
例如:
java
@PostConstruct
public void init() {
materialInventoryService.doSomething();
}
或者在构造方法中调用:
java
public BusinessOrderSplitServiceImpl(@Lazy IMaterialInventoryService materialInventoryService) {
this.materialInventoryService = materialInventoryService;
this.materialInventoryService.doSomething();
}
这种写法会提前触发代理对象解析,可能导致循环依赖问题重新出现。
4. 设计上存在强耦合
如果两个 Service 长期互相调用,说明业务职责可能存在耦合。
例如:
text
订单拆分服务依赖库存服务
库存服务又依赖订单拆分服务
这种情况下可以考虑抽出一个公共服务:
text
BusinessOrderSplitServiceImpl
-> InventoryOperateService
MaterialInventoryServiceImpl
-> InventoryOperateService
或者拆分领域职责,避免两个核心 Service 互相依赖。
小结
本次问题的核心不是 @RequiredArgsConstructor 能不能解决循环依赖。
真正的核心是:
Spring 构造器注入时,
@Lazy必须出现在构造方法参数上。
使用 Lombok 时,字段上的 @Lazy 默认不一定会复制到构造器参数上。
因此需要在模块根目录配置:
properties
config.stopBubbling = true
lombok.copyableAnnotations += org.springframework.context.annotation.Lazy
并确保 lombok.config 和 src 平级:
text
your-module/
├── pom.xml
├── lombok.config
└── src/
最终通过 javap 验证,看到:
java
RuntimeVisibleParameterAnnotations:
parameter 13:
org.springframework.context.annotation.Lazy
才说明真正生效。
最终判断标准:
text
字段上有 @Lazy:不够
构造器参数上有 @Lazy:才有效
本次最终结果:
text
lombok.config 位置:正确
copyableAnnotations 配置:正确
构造器参数 @Lazy:已生成
循环依赖延迟注入:已生效