发现服务
好的,我们来探索一个 NestJS 中非常“元” (Meta) 且强大的功能,它能让你像拥有“透视眼”一样,看穿整个应用的结构。这就是发现服务 (Discovery Service)。
NestJS 的“全息扫描仪”:发现服务 (Discovery Service) 深度解析
想象一下你是一个新上任的机器人军团的总司令。
你的军团里有成千上万个机器人,每个机器人都有自己的型号(Controller
或 Injectable
)、特殊装备(自定义元数据 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()
方法可以获取到所有注册的提供者。
代码示例:找出所有的“服务员”
假设我们有一些服务,它们的职责都是“服务”。
@Injectable()
export class WaiterService {
serve() {
/*...*/
}
}
@Injectable()
export class ButlerService {
serve() {
/*...*/
}
}
// 这是一个不相关的服务
@Injectable()
export class ChefService {
cook() {
/*...*/
}
}
现在,我们创建一个 RegistryService
,它会使用 DiscoveryService
来找出所有这些“服务员”。
src/registry.service.ts
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
MetadataScanner
是 DiscoveryService
的好搭档。它是一个辅助服务,可以扫描一个类实例上的所有方法,并找出哪些方法被特定的元数据装饰器标记过。
实战演练:自动注册命令行命令
我们的目标:创建一个 @Command()
装饰器。任何被它标记的方法,都将被自动发现并注册。
第一步:创建 @Command()
装饰器
这是一个元数据装饰器,我们用它来附加命令名称。
src/command.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const COMMAND_METADATA_KEY = 'COMMAND_METADATA_KEY';
export const Command = (name: string) =>
SetMetadata(COMMAND_METADATA_KEY, name);
第二步:创建一些带有命令的服务
@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...');
}
}
第三步:创建我们的“命令总线”服务
这个服务会组合使用 DiscoveryService
和 MetadataScanner
。
src/command-bus.service.ts
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 中最具创造力的工具之一。它能让你编写出真正“聪明”和“自动化”的模块,将你的应用架构提升到一个新的高度。