3. 服务与依赖注入:@Injectable 服务提供者、构造函数注入、Provider 作用域与自定义 Provider

在 NestJS 中,依赖注入(Dependency Injection, DI) 是框架的核心设计模式。它允许你将服务、存储库、配置等组件声明为“提供者(Provider)”,并通过构造函数自动注入到需要它们的类中。本章将深入剖析 @Injectable 装饰器、构造函数注入、Provider 作用域以及自定义 Provider 的完整用法。

3.1 @Injectable 服务提供者

@Injectable() 装饰器标记一个类为可以被 Nest 的 IoC(控制反转)容器管理的提供者。任何被装饰的类都可以被注入到其他类中。

// user.service.ts
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  private users = [];

  findAll() {
    return this.users;
  }

  create(user) {
    this.users.push(user);
  }
}

当 Nest 启动时,它会扫描所有模块的 providers 数组,并将 UserService 注册到 IoC 容器中。默认情况下,Provider 是单例(Singleton)的,即整个应用共享同一个实例。

3.2 构造函数注入

构造函数注入是 NestJS 中最常用的注入方式。通过在类的构造函数中声明参数,Nest 会自动解析并注入对应的依赖。

// user.controller.ts
import { Controller, Get, Post, Body } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
  // 构造函数注入:Nest 自动创建 UserService 实例并注入
  constructor(private readonly userService: UserService) {}

  @Get()
  findAll() {
    return this.userService.findAll();
  }

  @Post()
  create(@Body() user) {
    this.userService.create(user);
  }
}
最佳实践:始终使用 private readonly 或 public readonly 修饰注入的属性。这不仅能确保类型安全,还能防止意外修改依赖。

3.3 Provider 作用域

NestJS 支持三种 Provider 作用域,控制着实例的生命周期和共享范围:

作用域 枚举值 生命周期 适用场景
DEFAULT Scope.DEFAULT 单例(整个应用共享一个实例) 无状态服务、配置服务、数据库连接池
REQUEST Scope.REQUEST 每个请求创建一个新实例 请求级别的缓存、租户隔离、请求日志
TRANSIENT Scope.TRANSIENT 每次注入都创建一个新实例 有状态工具类、每次使用都需要独立状态的组件
// 设置 REQUEST 作用域
import { Injectable, Scope } from '@nestjs/common';

@Injectable({ scope: Scope.REQUEST })
export class RequestScopedService {
  private requestId: string;

  constructor() {
    this.requestId = Math.random().toString(36).substring(7);
  }

  getRequestId() {
    return this.requestId;
  }
}
注意:REQUEST 作用域会触发依赖链的“作用域冒泡”。如果一个 REQUEST 作用域的服务被注入到单例服务中,Nest 会抛出异常。确保注入链中所有依赖的作用域兼容。

3.4 自定义 Provider

除了使用 @Injectable 类作为 Provider,Nest 还支持多种自定义 Provider 方式,用于处理第三方库、配置值、工厂函数等场景。

3.4.1 值提供者(useValue)

用于注入常量、配置对象或第三方库实例。

// app.module.ts
import { Module } from '@nestjs/common';

const CONFIG = {
  apiKey: 'abc123',
  endpoint: 'https://api.example.com',
};

@Module({
  providers: [
    {
      provide: 'CONFIG',
      useValue: CONFIG,
    },
  ],
})
export class AppModule {}

// 在服务中使用
@Injectable()
export class ApiService {
  constructor(@Inject('CONFIG') private config: any) {}
}

3.4.2 工厂提供者(useFactory)

用于需要动态创建或依赖其他 Provider 的场景。

// 工厂提供者示例
import { Module } from '@nestjs/common';

@Module({
  providers: [
    {
      provide: 'DATABASE_CONNECTION',
      useFactory: (configService: ConfigService) => {
        const connection = createConnection({
          host: configService.get('DB_HOST'),
          port: configService.get('DB_PORT'),
        });
        return connection;
      },
      inject: [ConfigService], // 工厂函数的依赖
    },
  ],
})
export class DatabaseModule {}

3.4.3 别名提供者(useExisting)

用于为一个已有的 Provider 创建别名。

@Module({
  providers: [
    UserService,
    {
      provide: 'UserServiceAlias',
      useExisting: UserService,
    },
  ],
})
export class UserModule {}

3.5 Provider 注册与注入流程

下面的 SVG 图展示了 NestJS 中 Provider 从注册到注入的完整流程:

Provider 注册与注入流程 模块 providers 数组 @Module({ providers: [...] }) IoC 容器 实例化 & 缓存 构造函数注入 constructor(private svc) 作用域判断 DEFAULT(单例) REQUEST(请求级) TRANSIENT(瞬态) 自定义 Provider 类型 useValue useFactory useExisting useClass 常量/配置 动态工厂 别名 类映射

3.6 自定义 Provider 对比

类型 语法 适用场景 示例
useValue { provide: TOKEN, useValue: value } 注入常量、配置对象、第三方库实例 数据库配置、API 密钥
useFactory { provide: TOKEN, useFactory: factory, inject: [...] } 需要动态创建或依赖其他 Provider 数据库连接、HTTP 客户端
useExisting { provide: TOKEN, useExisting: ExistingProvider } 为已有 Provider 创建别名 接口抽象、多态注入
useClass { provide: TOKEN, useClass: SomeClass } 根据环境切换具体实现 开发/生产环境的不同服务实现

3.7 作用域选择策略

在实际项目中,合理选择 Provider 作用域对性能和正确性至关重要:

  • 优先使用 DEFAULT(单例):绝大多数服务应该是无状态的,单例模式性能最优。
  • 仅在需要请求隔离时使用 REQUEST:例如多租户应用中的租户上下文、请求级别的日志追踪。
  • 谨慎使用 TRANSIENT:每次注入都创建新实例,会增加内存开销。适用于需要独立状态且生命周期极短的工具类。
  • 避免作用域冒泡:不要将 REQUEST 作用域的服务注入到单例服务中,否则会引发运行时错误。
安全测试视角:在授权测试中,REQUEST 作用域常用于存储当前用户的身份信息。如果作用域配置不当(例如误用单例),可能导致用户 A 的认证信息被用户 B 获取,造成严重的安全漏洞。务必在测试中验证每个请求的上下文隔离性。

3.8 实战:自定义 Provider 实现配置注入

以下示例展示如何使用 useFactory 动态创建数据库连接配置:

// database.providers.ts
import { Provider } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { createConnection } from 'typeorm';

export const databaseProviders: Provider[] = [
  {
    provide: 'DATABASE_CONNECTION',
    useFactory: async (configService: ConfigService) => {
      const connection = await createConnection({
        type: 'mysql',
        host: configService.get('DB_HOST'),
        port: configService.get('DB_PORT'),
        username: configService.get('DB_USER'),
        password: configService.get('DB_PASS'),
        database: configService.get('DB_NAME'),
        entities: [__dirname + '/../**/*.entity{.ts,.js}'],
        synchronize: true,
      });
      return connection;
    },
    inject: [ConfigService],
  },
];

通过自定义 Provider,你可以将复杂的初始化逻辑封装在工厂函数中,同时保持模块的整洁和可测试性。