什么是循环依赖
循环依赖(Circular Dependency)指的是两个或多个模块/服务互相引用对方:
- Module 层面:
AModule的imports里有BModule,而BModule的imports里又有AModule - Provider 层面:
AService的构造函数里注入BService,而BService的构造函数里又注入AService
JavaScript 模块加载时,一旦遇到循环引用,后被加载的那一端拿到的会是 undefined。Nest 的 IoC 容器在解析依赖时也会因此报错:
1
A circular dependency between classes has been detected.
思路:先各自创建,再用 forwardRef 关联
Nest 给的解决办法是 forwardRef():先让两端都能独立完成 module / class 定义(延迟解析对方的引用),等容器构建完成后再把两者真正关联起来。
一句话:
forwardRef(() => X)= “我现在还拿不到 X,等用的时候再去问容器要”。
业务场景:用户与订单互相引用
电商场景里这种循环非常常见:
UserService要查”该用户的订单数”——依赖OrderServiceOrderService要在创建订单时校验”用户是否被冻结”——依赖UserService
1. Provider 之间的循环依赖
直接互相注入会启动失败:
1
2
3
4
5
6
7
8
9
10
// ❌ 会抛 circular dependency 错误
@Injectable()
export class UserService {
constructor(private readonly orderService: OrderService) {}
}
@Injectable()
export class OrderService {
constructor(private readonly userService: UserService) {}
}
用 forwardRef + @Inject 改造:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// user.service.ts
import { Injectable, Inject, forwardRef } from "@nestjs/common";
import { OrderService } from "../order/order.service";
@Injectable()
export class UserService {
constructor(
@Inject(forwardRef(() => OrderService))
private readonly orderService: OrderService,
) {}
async getProfile(userId: string) {
const orderCount = await this.orderService.countByUser(userId);
return { userId, orderCount };
}
async isFrozen(userId: string) {
return false;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// order.service.ts
import { Injectable, Inject, forwardRef } from "@nestjs/common";
import { UserService } from "../user/user.service";
@Injectable()
export class OrderService {
constructor(
@Inject(forwardRef(() => UserService))
private readonly userService: UserService,
) {}
async create(userId: string, dto: { skuId: string }) {
if (await this.userService.isFrozen(userId)) {
throw new Error("账号已冻结");
}
// ... 创建订单
}
async countByUser(_userId: string) {
return 0;
}
}
要点:
- 两端都要包
forwardRef,缺一不可 - 必须配合
@Inject(...)显式声明(光靠类型推断的构造函数注入不够,因为此时类还未完全定义)
2. Module 之间的循环依赖
UserModule 导出 UserService 给 OrderModule 用,反过来 OrderModule 也要导出 OrderService 给 UserModule 用:
1
2
3
4
5
6
7
8
9
10
11
// user.module.ts
import { Module, forwardRef } from "@nestjs/common";
import { UserService } from "./user.service";
import { OrderModule } from "../order/order.module";
@Module({
imports: [forwardRef(() => OrderModule)],
providers: [UserService],
exports: [UserService],
})
export class UserModule {}
1
2
3
4
5
6
7
8
9
10
11
// order.module.ts
import { Module, forwardRef } from "@nestjs/common";
import { OrderService } from "./order.service";
import { UserModule } from "../user/user.module";
@Module({
imports: [forwardRef(() => UserModule)],
providers: [OrderService],
exports: [OrderService],
})
export class OrderModule {}
要点:
imports数组里包forwardRef(() => OtherModule)exports不需要forwardRef(它只导出本模块自己的 provider)
3. Provider 注入也要 forwardRef
注意 Module 包 forwardRef 不会自动让 Provider 注入也生效——两层都要单独处理:
1
2
3
4
5
6
7
// 完整的 UserModule
@Module({
imports: [forwardRef(() => OrderModule)], // 模块层
providers: [UserService], // UserService 内部还要 @Inject(forwardRef(() => OrderService))
exports: [UserService],
})
export class UserModule {}
forwardRef 的原理(一句话版)
forwardRef(fn) 返回的不是 class 本身,而是一个 { forwardRef: fn } 包装对象。Nest 容器在第一遍扫描依赖图时遇到它就跳过;等所有 module / provider 都注册完后,再调用 fn() 拿到真正的引用,完成最终绑定。
1
2
3
4
// 简化的伪代码
function forwardRef(fn: () => Type) {
return { forwardRef: fn };
}
这就是为什么”先单独创建 2 个模块,再将 2 者关联起来”——forwardRef 把”关联”这一步从编译期/加载期推迟到了容器构建期。
QA: 能不能不用 forwardRef?
💬点击展开/收起
通常更好的办法是重构掉循环依赖,循环依赖往往意味着设计有问题:
- 抽出第三个共享服务:把
UserService和OrderService都依赖的逻辑抽到SharedService(或UserOrderFacadeService),让两者只依赖第三方 - 用事件解耦:
OrderService创建订单后发OrderCreatedEvent,UserService订阅事件更新用户状态,而不是直接调用 - 下沉到数据层:让
OrderService直接通过UserRepository查冻结状态,而不是经过UserService
forwardRef 是”实在拆不开时的兜底方案”,不是首选方案。
踩坑提示
- 两端必须都用
forwardRef:只在一端写不会生效 - Provider 注入循环依赖时必须配合
@Inject:纯靠类型反射的构造函数注入会失败 exports不要包forwardRef:只在imports和@Inject里用- 检查是否真的需要循环依赖:先尝试重构,
forwardRef是最后手段 - 启动报错信息要看清:
A circular dependency between classes has been detected通常会附带具体路径,按链路排查更快
小结
- 循环依赖时,JS 模块加载顺序导致一端拿到
undefined,IoC 容器无法解析 forwardRef(() => X)把”获取 X”延迟到容器构建完成后,两端都用即可解开死锁- Module 层:
imports: [forwardRef(() => OtherModule)] - Provider 层:
@Inject(forwardRef(() => OtherService)) - 优先重构(共享服务 / 事件 / 下沉数据层),
forwardRef作为兜底