Skip to content

发现服务

好的,我们来探索一个 NestJS 中非常“元” (Meta) 且强大的功能,它能让你像拥有“透视眼”一样,看穿整个应用的结构。这就是发现服务 (Discovery Service)

NestJS 的“全息扫描仪”:发现服务 (Discovery Service) 深度解析

想象一下你是一个新上任的机器人军团的总司令。

你的军团里有成千上万个机器人,每个机器人都有自己的型号ControllerInjectable)、特殊装备(自定义元数据 Metadata,比如 @Command('attack')),以及它们所属的部队Module)。

作为总司令,你当然不关心每个机器人的具体实现(它内部的电路是如何工作的)。但你迫切需要一张实时的、可查询的花名册,这张花名册能让你:

  • “扫描全军,找出所有型号为‘突击兵’的机器人。” (getProviders())
  • “在所有‘突击兵’中,找出所有装备了‘激光剑’的机器人。” (根据元数据进行过滤)
  • “让所有装备了‘激光剑’的‘突击兵’执行‘冲锋’动作!” (获取实例并调用方法)

在 NestJS 中,DiscoveryService 就是你手中那台能生成实时花名册的“全息扫描仪”。它允许你在应用运行时,以编程方式发现 (discover)遍历 (iterate) 所有被 NestJS DI 容器管理的提供者 (Providers) 和控制器 (Controllers),并读取附加在它们之上的元数据。

1. 为什么我需要一台“扫描仪”?—— “元编程”的威力

这个功能非常高级,你可能在日常的业务开发中用不到它。但它是一些框架级插件式功能的基石。DiscoveryService 的核心用途是实现元编程 (Metaprogramming) —— 编写能够分析、操作甚至生成其他代码的代码。

典型应用场景

  • 自动注册命令:开发一个命令行工具 (CLI),你希望所有在服务方法上使用了 @Command() 装饰器的方法,都能被自动注册为可执行的命令,而无需手动去列表里一个个添加。
  • 构建事件总线:创建一个事件系统,所有使用了 @OnEvent('user_created') 装饰器的方法,都会被自动订阅到 user_created 事件上。
  • 生成 API 文档:扫描所有的控制器和路由处理函数,自动生成 API 文档。这正是 @nestjs/swagger 模块的核心原理!
  • 实现插件系统:让你的应用支持插件。插件开发者只需要编写一个带有特定装饰器(如 @Plugin()) 的服务,你的主应用就能通过 DiscoveryService 发现并加载它。

2. 如何使用“扫描仪”?—— 核心 API 探索

DiscoveryService 提供了几组核心方法,用于“扫描”不同类型的组件。

扫描提供者 (Providers)

getProviders() 方法可以获取到所有注册的提供者。

代码示例:找出所有的“服务员”

假设我们有一些服务,它们的职责都是“服务”。

typescript
@Injectable()
export class WaiterService {
  serve() {
    /*...*/
  }
}

@Injectable()
export class ButlerService {
  serve() {
    /*...*/
  }
}

// 这是一个不相关的服务
@Injectable()
export class ChefService {
  cook() {
    /*...*/
  }
}

现在,我们创建一个 RegistryService,它会使用 DiscoveryService 来找出所有这些“服务员”。

src/registry.service.ts

typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, MetadataScanner } from '@nestjs/core';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';

@Injectable()
export class RegistryService implements OnModuleInit {
  constructor(private readonly discoveryService: DiscoveryService) {}

  onModuleInit() {
    console.log('🔍 开始扫描所有提供者...');

    // 1. 获取所有提供者的包装器 (InstanceWrapper)
    const providers: InstanceWrapper[] = this.discoveryService.getProviders();

    providers.forEach((wrapper) => {
      // 2. 从包装器中获取实例和元类型 (类名)
      const { instance, metatype } = wrapper;

      // 包装器可能是空的,或者实例不存在 (例如,请求作用域的提供者在应用启动时还没有实例)
      if (!instance || !metatype) {
        return;
      }

      // 3. 我们可以根据类名来筛选
      if (metatype.name.endsWith('Service')) {
        console.log(`  ✅ 发现一个服务: ${metatype.name}`);
      }
    });
  }
}

控制台输出:

🔍 开始扫描所有提供者...
  ✅ 发现一个服务: WaiterService
  ✅ 发现一个服务: ButlerService
  ✅ 发现一个服务: ChefService
  ✅ 发现一个服务: RegistryService

前置知识:InstanceWrapper

你可能注意到了,getProviders() 返回的不是服务实例本身,而是一个 InstanceWrapper[] 数组。InstanceWrapper 是 NestJS 内部用来包裹每个提供者实例的对象,它包含了关于这个实例的所有信息,比如:

  • instance: 真正的服务实例(如果是单例的话)。
  • metatype: 服务的类构造函数(如 WaiterService 类本身)。
  • name: 令牌(Token)。
  • isResolved: 是否已经被实例化。
  • 等等...

我们通常通过操作这个 InstanceWrapper 来获取我们需要的信息。

3. “高精度扫描”:结合元数据进行发现

只按类型扫描还不够酷。DiscoveryService 的真正威力在于和元数据的结合。

前置知识:MetadataScanner

MetadataScannerDiscoveryService 的好搭档。它是一个辅助服务,可以扫描一个类实例上的所有方法,并找出哪些方法被特定的元数据装饰器标记过。

实战演练:自动注册命令行命令

我们的目标:创建一个 @Command() 装饰器。任何被它标记的方法,都将被自动发现并注册。

第一步:创建 @Command() 装饰器

这是一个元数据装饰器,我们用它来附加命令名称。

src/command.decorator.ts

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

export const COMMAND_METADATA_KEY = 'COMMAND_METADATA_KEY';

export const Command = (name: string) =>
  SetMetadata(COMMAND_METADATA_KEY, name);

第二步:创建一些带有命令的服务

typescript
@Injectable()
export class ArmyService {
  @Command('attack')
  attackTarget(target: string) {
    console.log(`⚔️ Attacking ${target}!`);
  }

  @Command('defend')
  defendBase() {
    console.log(`🛡️ Defending the base!`);
  }

  // 这是一个普通方法,没有被标记
  retreat() {
    console.log('Retreating...');
  }
}

第三步:创建我们的“命令总线”服务

这个服务会组合使用 DiscoveryServiceMetadataScanner

src/command-bus.service.ts

typescript
import { Injectable, OnModuleInit } from '@nestjs/common';
import { DiscoveryService, MetadataScanner, Reflector } from '@nestjs/core';
import { InstanceWrapper } from '@nestjs/core/injector/instance-wrapper';
import { COMMAND_METADATA_KEY } from './command.decorator';

@Injectable()
export class CommandBusService implements OnModuleInit {
  // 我们需要这三个“神器”
  constructor(
    private readonly discoveryService: DiscoveryService,
    private readonly metadataScanner: MetadataScanner,
    private readonly reflector: Reflector
  ) {}

  onModuleInit() {
    this.registerAllCommands();
  }

  registerAllCommands() {
    console.log('🤖 开始扫描并注册所有命令...');
    const providers: InstanceWrapper[] = this.discoveryService.getProviders();

    providers.forEach((wrapper) => {
      const { instance } = wrapper;
      if (!instance) {
        return;
      }

      // 1. 获取实例的原型,以便扫描方法
      const prototype = Object.getPrototypeOf(instance);

      // 2. 使用 MetadataScanner 扫描实例上的所有方法名
      const methodNames = this.metadataScanner.scanFromPrototype(
        instance,
        prototype,
        (name) => name // 返回方法名本身
      );

      // 3. 遍历所有方法,检查哪个方法有我们的 @Command 元数据
      methodNames.forEach((methodName) => {
        const commandName = this.reflector.get<string>(
          COMMAND_METADATA_KEY,
          instance[methodName] // 获取方法的引用
        );

        if (commandName) {
          console.log(
            `  👍 发现命令: '${commandName}' -> 处理器: ${wrapper.name}.${methodName}`
          );
          // 在这里,你可以将 commandName 和处理函数 (instance[methodName])
          // 存入一个 Map 中,以便后续调用
          // this.commands.set(commandName, () => instance[methodName]());
        }
      });
    });
  }
}
  • Reflector: 之前学过,它是用来读取元数据的标准工具。

控制台输出:

🤖 开始扫描并注册所有命令...
  👍 发现命令: 'attack' -> 处理器: ArmyService.attackTarget
  👍 发现命令: 'defend' -> 处理器: ArmyService.defendBase

大功告成!我们成功地创建了一个插件化的命令系统。现在,任何开发者只要创建一个服务,并在方法上使用 @Command() 装饰器,这个命令就会被我们的 CommandBusService 自动发现,完全无需手动注册。

总结

DiscoveryService 是一个提供“上帝视角”的强大工具,让你能够从内部洞察和操作你的 NestJS 应用。

它是什么?一个让你在运行时遍历所有提供者和控制器,并读取其元数据的服务。
黄金搭档?MetadataScanner (扫描实例上的方法) 和 Reflector (读取元数据)。
核心用途?元编程。实现自动注册、插件化、事件总线等框架级功能。
注意事项?这是高级功能。在日常业务逻辑中,你几乎用不到它。它的舞台在于构建可扩展、可配置的底层系统。

虽然 DiscoveryService 的概念比较抽象,但一旦你理解了它,就等于掌握了 NestJS 中最具创造力的工具之一。它能让你编写出真正“聪明”和“自动化”的模块,将你的应用架构提升到一个新的高度。