4. 提供者与服务层:@Injectable 装饰器、自定义 Provider、作用域与生命周期

在 NestJS 中,提供者(Provider)是依赖注入系统的核心概念。几乎所有可被注入的类——服务(Service)、仓库(Repository)、工厂(Factory)、辅助类(Helper)——都被视为提供者。本章将深入剖析 @Injectable 装饰器的工作原理、自定义 Provider 的多种注册方式,以及作用域与生命周期对服务实例行为的影响。

4.1 @Injectable 装饰器:标记可注入类

@Injectable() 是 NestJS 中最基础的装饰器,用于告诉依赖注入容器:该类可以被 IoC 容器实例化并注入到其他类中。任何未标记 @Injectable 的类,即使被注册在模块的 providers 数组中,也无法被正确注入。

核心规则:只有被 @Injectable() 装饰的类,NestJS 的运行时注入器才会为其解析构造函数参数并创建实例。
// user.service.ts
import { Injectable } from '@nestjs/common';
import { UserRepository } from './user.repository';

@Injectable()
export class UserService {
  constructor(private readonly userRepository: UserRepository) {}

  async findByEmail(email: string) {
    return this.userRepository.findOne({ where: { email } });
  }
}

当 UserService 被注册到模块的 providers 中时,NestJS 会自动分析其构造函数,发现 UserRepository 依赖,并从容器中查找对应的提供者实例完成注入。

4.2 自定义 Provider:超越类注册的灵活性

除了标准的类注册,NestJS 提供了四种自定义 Provider 模式,用于处理第三方库、动态值、工厂逻辑或别名场景。

Provider 类型 Token 标识 适用场景 示例
值提供者(Value Provider) 字符串 / Symbol 注入常量、配置对象、第三方库实例 { provide: 'CONFIG', useValue: config }
类提供者(Class Provider) 类 / 字符串 动态选择实现类(如 Mock 替换) { provide: UserService, useClass: MockUserService }
工厂提供者(Factory Provider) 类 / 字符串 需要异步初始化或依赖其他 Provider 的复杂逻辑 { provide: 'DB_CONNECTION', useFactory: (config) => createConnection(config), inject: ['CONFIG'] }
别名提供者(Alias Provider) 字符串 / Symbol 为已有 Provider 创建别名,便于重构或跨模块引用 { provide: 'ALIAS_SERVICE', useExisting: UserService }
最佳实践:当需要注入非类 Token(如字符串 'CONFIG')时,建议使用 @Inject('CONFIG') 装饰器显式声明注入 Token,避免与类名冲突。
// 工厂提供者示例:动态创建 Redis 客户端
import { createClient } from 'redis';

export const redisProvider = {
  provide: 'REDIS_CLIENT',
  useFactory: async (configService: ConfigService) => {
    const client = createClient({
      url: configService.get('REDIS_URL'),
    });
    await client.connect();
    return client;
  },
  inject: [ConfigService],
};

4.3 作用域与生命周期:控制实例的存活范围

NestJS 提供三种作用域(Scope),决定 Provider 实例在应用中的存活时长和共享范围。默认作用域为 DEFAULT(单例),适用于大多数无状态服务。

作用域 枚举值 实例生命周期 适用场景
单例(Singleton) Scope.DEFAULT 整个应用生命周期内只创建一个实例,所有请求共享 数据库连接池、配置服务、日志服务
请求作用域(Request) Scope.REQUEST 每个 HTTP 请求或 WebSocket 消息创建一个新实例,请求结束后销毁 请求级别的缓存、租户上下文、请求日志追踪
瞬态(Transient) Scope.TRANSIENT 每次注入时都创建一个全新的实例,不共享 无状态工具类、每次需要独立状态的辅助对象
性能警告:使用 Scope.REQUEST 或 Scope.TRANSIENT 会显著增加实例创建和垃圾回收的开销。在压测场景中,大量请求作用域 Provider 可能导致吞吐量下降 20%-40%。仅在确实需要请求隔离状态时才使用非默认作用域。
// 请求作用域示例:每个请求独立的请求 ID 追踪
import { Injectable, Scope } from '@nestjs/common';

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

  setRequestId(id: string) {
    this.requestId = id;
  }

  getRequestId(): string {
    return this.requestId;
  }
}

4.4 生命周期钩子:在实例创建与销毁时执行逻辑

NestJS 提供 OnModuleInit、OnApplicationBootstrap、OnModuleDestroy 等生命周期接口,允许 Provider 在特定阶段执行初始化或清理逻辑。

  • OnModuleInit:模块依赖解析完成后调用,适合建立数据库连接、加载缓存。
  • OnApplicationBootstrap:所有模块初始化完成后调用,适合启动后台任务或健康检查。
  • OnModuleDestroy:模块销毁前调用,适合关闭连接、释放资源。
  • BeforeApplicationShutdown:应用关闭信号触发时调用,适合优雅关闭。
import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';

@Injectable()
export class DatabaseService implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    console.log('DatabaseService: 连接数据库...');
    // 实际连接逻辑
  }

  async onModuleDestroy() {
    console.log('DatabaseService: 断开数据库连接...');
    // 清理逻辑
  }
}

4.5 提供者注册与注入流程

下图展示了从模块注册到依赖注入的完整链路,帮助理解 IoC 容器如何管理 Provider 实例。

NestJS 提供者注册与注入流程 Provider 注册 → 解析 → 注入流程 模块注册 providers IoC 容器注册 构造函数依赖分析 递归解析依赖 创建实例(作用域控制) 注入到目标类 触发生命周期钩子 作用域影响实例数 DEFAULT: 1 个实例 REQUEST: 每请求 1 个

4.6 作用域与生命周期对比总结

下表从实例数量、内存占用、注入性能三个维度对比三种作用域,帮助你在架构设计时做出合理选择。

维度 DEFAULT(单例) REQUEST(请求) TRANSIENT(瞬态)
实例数量 全局 1 个 每请求 1 个 每次注入 1 个
内存占用 低(常驻) 中(请求结束后 GC) 高(频繁创建/销毁)
注入性能 最快(直接返回缓存) 中等(需创建新实例) 较慢(每次创建)
状态隔离 无隔离(共享状态) 请求级别隔离 完全隔离
典型场景 数据库服务、日志服务 请求上下文、租户数据 无状态工具类、DTO 转换器
压测建议:在压测场景中,优先使用 DEFAULT 作用域。如果必须使用 REQUEST 作用域,请确保 Provider 的构造函数中不包含耗时操作(如数据库查询),否则每个请求都会增加额外的延迟开销。

4.7 自定义 Provider 与作用域组合实践

自定义 Provider 同样支持作用域配置。例如,工厂提供者可以结合 Scope.REQUEST 实现每个请求动态创建不同的实例:

// 请求作用域的工厂提供者
import { Scope } from '@nestjs/common';

export const requestScopedFactory = {
  provide: 'REQUEST_SCOPED_SERVICE',
  useFactory: (configService: ConfigService) => {
    return new DynamicService(configService.get('tenant'));
  },
  inject: [ConfigService],
  scope: Scope.REQUEST, // 每个请求重新执行工厂函数
};

通过灵活组合自定义 Provider 类型与作用域,你可以精确控制服务实例的创建时机、共享范围和生命周期,从而在复杂业务场景中实现高效的依赖管理。