Skip to content

依赖注入与 IoC 容器

一、IoC 概念

控制反转(Inversion of Control,IoC):把"我主动创建依赖"变成"容器替我创建依赖,我只声明需要什么"。

没有 IoC:

typescript
class UserService {
  // 硬编码:UserService 需要知道如何创建 UserRepository 和 DatabaseConnection
  private repo = new UserRepository(
    new DatabaseConnection('postgres://localhost:5432/mydb')
  );
}

问题:

  1. 耦合:更换数据库驱动需要修改 UserService
  2. 不可测试:无法注入 Mock Repository
  3. 资源浪费:每个 UserService 实例都创建独立的数据库连接

有 IoC:

typescript
@Injectable()
class UserService {
  constructor(private repo: UserRepository) {}
  // UserService 不知道也不关心 UserRepository 如何创建
  // 由 IoC 容器负责创建并注入
}

二、NestJS DI 容器工作原理

工作流程

1. 启动时扫描模块树
   AppModule → UsersModule → [UserService, UserRepository]

2. 读取依赖元数据
   Reflect.getMetadata('design:paramtypes', UserService)
   → [UserRepository]

3. 递归解析依赖
   UserRepository 依赖 DataSource
   DataSource 依赖 ConfigService
   ConfigService 无依赖(叶子节点)

4. 按依赖顺序实例化(叶子先)
   ConfigService → DataSource → UserRepository → UserService

5. 注入
   new UserService(userRepositoryInstance)

容器存储结构

每个 Module 都有自己的容器上下文:

typescript
// 简化的容器结构(概念图)
{
  "UsersModule": {
    providers: Map {
      UserService → { instance: UserService{...}, scope: 'SINGLETON' },
      UserRepository → { instance: UserRepository{...}, scope: 'SINGLETON' },
    }
  },
  "AppModule": {
    providers: Map {
      ConfigService → { instance: ConfigService{...}, scope: 'SINGLETON' },
    }
  }
}

三、Provider 的四种形式

1. 类 Provider(最常用)

typescript
providers: [UserService]
// 完整形式等价于:
providers: [{
  provide: UserService,   // Token(用于查找)
  useClass: UserService,  // 实现(用于实例化)
}]

Token 和 Class 可以不同,实现接口替换:

typescript
// 生产环境用真实服务
providers: [{
  provide: UserRepository,
  useClass: PostgresUserRepository,
}]

// 测试环境用内存实现
providers: [{
  provide: UserRepository,
  useClass: InMemoryUserRepository,
}]

2. 值 Provider(注入常量/配置)

typescript
providers: [
  {
    provide: 'APP_CONFIG',
    useValue: {
      apiUrl: 'https://api.example.com',
      timeout: 5000,
    },
  },
  {
    provide: 'NODE_ENV',
    useValue: process.env.NODE_ENV,
  },
]

// 注入时用 @Inject()
@Injectable()
class SomeService {
  constructor(
    @Inject('APP_CONFIG') private config: { apiUrl: string; timeout: number },
  ) {}
}

3. 工厂 Provider(需要异步或条件创建)

typescript
providers: [
  {
    provide: 'DB_CONNECTION',
    useFactory: async (config: ConfigService): Promise<DataSource> => {
      const dataSource = new DataSource({
        type: 'postgres',
        url: config.get('DATABASE_URL'),
        entities: [User, Post],
        synchronize: false,
      });
      await dataSource.initialize();
      return dataSource;
    },
    inject: [ConfigService],  // 声明工厂函数的依赖
  },
]

工厂 Provider 是实现异步初始化的关键——useFactory 可以是 async 函数,NestJS 会等待它 resolve 后再继续。

4. 别名 Provider(一个 Token 指向另一个)

typescript
providers: [
  UserService,
  {
    provide: 'USER_SERVICE',  // 字符串 Token
    useExisting: UserService,  // 指向已有的 Provider,复用同一实例
  },
]

// 适用于:需要兼容旧代码中使用字符串 Token 注入的场景

四、作用域(Scope)

作用域生命周期使用场景
DEFAULT(单例)整个应用共享一个实例,从启动到关闭无状态 Service(默认,推荐)
REQUEST每个 HTTP 请求创建新实例需要每请求独立状态(租户 ID、用户上下文)
TRANSIENT每次注入到消费者时创建新实例需要独立内部状态、不可共享
typescript
import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
  // 每个请求有自己的实例
  private requestData: Record<string, any> = {};
}

@Injectable({ scope: Scope.TRANSIENT })
export class TransientLogger {
  private prefix: string;

  setPrefix(prefix: string) { this.prefix = prefix; }
  log(msg: string) { console.log(`[${this.prefix}] ${msg}`); }
}

REQUEST 作用域的传染性

⚠️ 重要: REQUEST 作用域会向上传染。

如果一个 REQUEST 作用域的 Provider 被注入到另一个 Provider,那个 Provider 也会变成 REQUEST 作用域:

Controller(默认 DEFAULT)
  └── UserService(DEFAULT)
        └── TenantService(REQUEST)  ← 传染

          UserService 被强制变为 REQUEST 作用域

          Controller 也被强制变为 REQUEST 作用域

影响: 每次请求都会创建新的 Controller 和 UserService 实例,带来一定的性能开销(GC 压力增大)。

建议: 若只需要读取请求上下文(如 userId),优先使用 AsyncLocalStoragecls-hooked,而非直接使用 REQUEST 作用域。


五、注入 Token 的选择

NestJS 支持三种 Token 类型:

typescript
// 1. 类(最推荐,有类型推断)
providers: [UserService]
constructor(private userService: UserService) {}

// 2. 字符串(需要 @Inject,易于拼写错误)
providers: [{ provide: 'DB', useValue: db }]
constructor(@Inject('DB') private db: Database) {}

// 3. Symbol(避免字符串冲突,推荐用于库开发)
export const DB_TOKEN = Symbol('DB');
providers: [{ provide: DB_TOKEN, useValue: db }]
constructor(@Inject(DB_TOKEN) private db: Database) {}

// 4. InjectionToken(官方推荐方式,有类型安全)
import { InjectionToken } from '@nestjs/common';
export const DB_TOKEN = new InjectionToken<Database>('DB');

最佳实践: 在同一文件中导出 Token 常量(字符串/Symbol/InjectionToken),避免到处重复字符串字面量。


六、循环依赖的处理

当 A 依赖 B,B 又依赖 A 时,NestJS 无法正常解析依赖顺序。

解决方案:使用 forwardRef()

typescript
// users.service.ts
@Injectable()
export class UsersService {
  constructor(
    @Inject(forwardRef(() => AuthService))
    private authService: AuthService,
  ) {}
}

// auth.service.ts
@Injectable()
export class AuthService {
  constructor(
    @Inject(forwardRef(() => UsersService))
    private usersService: UsersService,
  ) {}
}

Module 之间的循环依赖:

typescript
@Module({
  imports: [forwardRef(() => AuthModule)],
  exports: [UsersService],
})
export class UsersModule {}

@Module({
  imports: [forwardRef(() => UsersModule)],
  exports: [AuthService],
})
export class AuthModule {}

⚠️ forwardRef() 是代码坏味道。 循环依赖通常意味着模块边界划分不合理,应考虑提取公共依赖到第三个模块。


七、异步 Provider 与应用初始化

当应用启动前需要完成异步操作(如连接数据库、读取配置),使用异步 Provider:

typescript
// 方式一:工厂 Provider
{
  provide: 'DATABASE',
  useFactory: async (config: ConfigService) => {
    const conn = await createConnection(config.get('DB_URL'));
    return conn;
  },
  inject: [ConfigService],
}

// 方式二:onModuleInit 钩子(更显式)
@Injectable()
export class DatabaseService implements OnModuleInit {
  private connection: DataSource;

  async onModuleInit() {
    this.connection = new DataSource({ ... });
    await this.connection.initialize();
    console.log('数据库连接成功');
  }
}

NestJS 会等待所有 onModuleInit() 完成后再开始监听 HTTP 请求,确保应用完全就绪。


八、常见错误与排查

错误 1:Nest can't resolve dependencies

Nest can't resolve dependencies of the UserService (?).
Please make sure that the argument UserRepository at index [0] is available in the UsersModule context.

原因: UserRepository 没有在 UsersModuleproviders 中注册,或所在模块没有 exports

排查步骤:

  1. 检查 UserRepository 是否在 providers: []
  2. 如果在另一个 Module 中,检查那个 Module 是否 exports: [UserRepository]
  3. 检查当前 Module 是否 imports: [ThatModule]

错误 2:A circular dependency detected

A circular dependency between modules. Please, make sure that each side of a bidirectional relationships are using forwardRef()

解决: 先用 forwardRef(),再重构模块边界消除循环。

错误 3:注入时得到 undefined

typescript
// ❌ 忘记 @Inject() 注解,字符串/Symbol Token 无法自动识别
constructor(private db: Database) {}  // design:paramtypes 是 Database 类,找不到 'DB' Token

// ✅ 必须用 @Inject()
constructor(@Inject('DB') private db: Database) {}

可运行 Demo: practice/01-ioc-demo — 手动实现 IoC 容器,与 NestJS 内置 IoC 对比

NestJS 深度学习体系