Skip to content

@Injectable() 真的只是个标记吗?

在 NestJS 中,@Injectable() 是我们最常接触的装饰器之一。但你是否思考过,它真的只是一个简单的标记吗?它背后隐藏着怎样的机制,让我们的类能够被依赖注入容器识别和管理?

1. 表象:一个简单的装饰器

我们通常这样使用 @Injectable()

typescript
import { Injectable } from '@nestjs/common';

@Injectable()
export class UserService {
  findAll() {
    return ['user1', 'user2'];
  }
}

然后在其他地方注入使用:

typescript
import { Controller, Get } from '@nestjs/common';
import { UserService } from './user.service';

@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

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

看起来 @Injectable() 只是告诉 NestJS "这个类可以被注入",但真的是这样吗?

2. 深入源码:Injectable 的本质

让我们来看看 @Injectable() 的实际实现:

typescript
// nestjs/common/interfaces/injectable.interface.ts
export interface Injectable {
  /**
   * Optional name for the injectable
   */
  name?: string;
}

// nestjs/common/decorators/core/injectable.decorator.ts
import { SCOPE_OPTIONS_METADATA } from '../constants';
import { Injectable } from '../interfaces/injectable.interface';
import { Type } from '../interfaces/type.interface';

export function Injectable(options?: Injectable): ClassDecorator {
  return (target: object) => {
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
  };
}

从源码可以看出,@Injectable() 实际上是通过 Reflect.defineMetadata 在类上定义了一些元数据。这说明它不仅仅是一个标记,而是携带了实际信息。

3. 元数据的作用

3.1 Scope 信息

@Injectable() 可以接受一个 options 参数:

typescript
@Injectable({ scope: Scope.REQUEST })
export class UserService {
  // 每个请求都会创建一个新的实例
}

这个 scope 信息会被存储在元数据中,NestJS 容器在创建实例时会读取这个信息来决定如何管理实例的生命周期。

3.2 依赖注入的关键

更重要的是,NestJS 通过 TypeScript 的 emitDecoratorMetadata 功能,在编译时获取构造函数参数的类型信息:

typescript
// 当我们写这样的代码时
constructor(private readonly userService: UserService) {}

// TypeScript 会在编译时生成元数据
// Reflect.getMetadata('design:paramtypes', UserController) 
// 会返回 [UserService]

这就是为什么 NestJS 能够自动注入依赖的关键。

4. 不使用 @Injectable() 会发生什么?

让我们做个实验,去掉 @Injectable() 看看会发生什么:

typescript
// 没有 @Injectable()
export class UserService {
  findAll() {
    return ['user1', 'user2'];
  }
}

// 在 controller 中使用
@Controller('users')
export class UserController {
  constructor(private readonly userService: UserService) {}

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

在这种情况下,如果 UserService 没有被其他方式注册到模块中(比如在 providers 数组中直接列出),NestJS 将无法解析这个依赖,会抛出异常:

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

5. @Injectable() 的真正作用

通过以上分析,我们可以总结出 @Injectable() 的真正作用:

  1. 标记可注入性:告诉 NestJS 这个类可以作为依赖被注入到其他类中
  2. 携带元数据:通过 Reflect.defineMetadata 存储 scope 等配置信息
  3. 配合 TypeScript 编译器:与 emitDecoratorMetadata 配合,让框架能够获取构造函数参数类型

6. 特殊情况处理

6.1 不需要 @Injectable() 的情况

如果一个类不依赖其他服务,理论上可以不加 @Injectable()

typescript
export class MathUtil {
  add(a: number, b: number) {
    return a + b;
  }
}

// 在 service 中使用
@Injectable()
export class UserService {
  private mathUtil = new MathUtil(); // 直接实例化
  
  findAll() {
    return ['user1', 'user2'];
  }
}

6.2 必须使用 @Injectable() 的情况

如果一个类需要注入其他服务,就必须使用 @Injectable()

typescript
@Injectable()
export class DatabaseService {
  // 数据库操作
}

// 必须使用 @Injectable(),因为需要注入 DatabaseService
@Injectable()
export class UserService {
  constructor(private readonly databaseService: DatabaseService) {}
  
  findAll() {
    // 使用 databaseService 查询用户
    return this.databaseService.query('SELECT * FROM users');
  }
}

7. 最佳实践

7.1 一致使用 @Injectable()

即使一个服务当前不依赖其他服务,也建议加上 @Injectable() 装饰器,这样做的好处是:

  1. 保持代码一致性
  2. 为未来可能的依赖注入做准备
  3. 明确表达这个类是服务类

7.2 合理使用 Scope

默认情况下,NestJS 使用单例模式(Singleton Scope)管理服务实例。只有在特殊情况下才需要改变 scope:

typescript
// 请求作用域:每个请求创建一个新实例
@Injectable({ scope: Scope.REQUEST })
export class UserService {
  // 适用于需要存储请求特定数据的场景
}

// 临时作用域:每次注入都创建新实例
@Injectable({ scope: Scope.TRANSIENT })
export class UserService {
  // 适用于需要完全独立实例的场景
}

8. 与模块系统的协同工作

8.1 模块中的提供者注册

@Injectable() 标记的类需要在模块中注册才能被注入:

typescript
// user.module.ts
@Module({
  controllers: [UserController],
  providers: [UserService], // 注册 UserService
})
export class UserModule {}

8.2 跨模块注入

通过模块的 exports 机制实现跨模块注入:

typescript
// user.module.ts
@Module({
  providers: [UserService],
  exports: [UserService], // 导出 UserService
})
export class UserModule {}

// auth.module.ts
@Module({
  imports: [UserModule], // 导入 UserModule
  providers: [AuthService],
})
export class AuthModule {}

// auth.service.ts
@Injectable()
export class AuthService {
  constructor(private readonly userService: UserService) {} // 可以注入 UserService
}

9. 依赖注入容器的工作原理

9.1 实例创建过程

typescript
class InstanceLoader {
  async createInstancesOfDependencies() {
    // 遍历所有模块
    for (const module of this.container.getModules().values()) {
      // 创建模块中的提供者实例
      await this.createInstancesOfProviders(module);
    }
  }
  
  private async createInstancesOfProviders(module: Module) {
    // 遍历模块中的所有提供者
    for (const [token, wrapper] of module.providers) {
      // 解析依赖并创建实例
      await this.injector.loadInstance(wrapper, module);
    }
  }
}

9.2 依赖解析机制

typescript
class Injector {
  async resolveComponentInstance<T>(
    module: Module,
    token: string,
    contextId: ContextId,
  ): Promise<InstanceWrapper<T>> {
    // 获取提供者包装器
    const wrapper = module.providers.get(token);
    
    // 解析提供者的依赖
    const dependencies = await this.resolveDependencies(wrapper, module);
    
    // 创建实例
    const instance = await this.instantiateClass(wrapper, dependencies);
    
    return instance;
  }
}

10. 总结

@Injectable() 远不止是一个简单的标记装饰器。它是 NestJS 依赖注入系统的重要组成部分,通过元数据机制与 TypeScript 编译器配合,实现了自动化的依赖解析和管理。

理解 @Injectable() 的真正作用,有助于我们:

  1. 更好地设计服务类结构
  2. 理解依赖注入的工作原理
  3. 在遇到依赖解析问题时能够快速定位原因
  4. 合理使用不同的 Scope 来满足业务需求

在下一篇文章中,我们将深入探讨 DI 容器的本质:Map<token, instance>?了解 Nest 的 ContainerModuleRef 如何管理实例生命周期。