Spring 构造器循环依赖排查:@RequiredArgsConstructor + @Lazy 到底有没有生效

背景

在 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

必要时还可以删除 targetbuild 目录后重新编译。


如何验证是否真的生效?

可以使用 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.configsrc 平级:

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:已生成
循环依赖延迟注入:已生效
相关推荐
Database_Cool_1 小时前
AnalyticDB MySQL vs StarRocks/ByteHouse:云数仓选型指南——全托管 vs 自建方案
数据库·数据仓库·mysql·阿里云
小草cys1 小时前
NVIDIA 驱动(550版本)成功安装后安装支持 GPU 加速的 PyTorch
人工智能·pytorch·python
SilentSamsara2 小时前
Python 微服务全链路:gRPC + 链路追踪 + 服务网格接入
开发语言·分布式·python·微服务·架构
Omics Pro2 小时前
「自兹以往」动物肠道微生物组
数据库·人工智能·机器学习·语言模型·自然语言处理
zzz_23682 小时前
【Redis】分布式锁完整演进
数据库·redis·分布式
贺国亚2 小时前
Spring-AI与LangChain4j
java·人工智能·spring
Cloud_Shy6182 小时前
解读《Effective Python 3rd Edition》:从练气到老魔(第三章 Item 21 - 24)
开发语言·人工智能·笔记·python·迭代器模式
mN9B2uk172 小时前
数据库的约束简介
java·数据库·sql
计算机安禾2 小时前
【数据库系统原理】第4篇:关系数据结构的形式化定义:域、笛卡尔积与关系模式
数据结构·数据库·算法