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 } |
'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 实例。
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 类型与作用域,你可以精确控制服务实例的创建时机、共享范围和生命周期,从而在复杂业务场景中实现高效的依赖管理。