依赖注入的核心问题
在 ConsulCoreModule 统一管理了所有 gRPC 客户端之后,业务 Service(如 AuthService)需要通过 NestJS 的 DI 系统获取对应的 gRPC 客户端实例。关键挑战是:如何让 DI 系统能够根据 service name 动态提供 gRPC 客户端。
动态 Provider 方案
在 ConsulCoreModule 中创建 gRPC Provider
// src/consul/consul-core.module.ts
private static addGrpcProvider(
serviceName: string,
consulService: ConsulService,
grpcProviders: Provider[],
): void {
grpcProviders.push({
provide: serviceName,
useFactory: () => {
// 从 ConsulService 的 grpcClients 中过滤出匹配的客户端
const clients = consulService.getGrpcClients();
const matchedClients = clients.filter(
(item) => item.serviceName === serviceName,
);
if (matchedClients.length === 0) {
return null;
}
// 随机返回一个客户端实例(简单负载均衡)
const randomIndex = Math.floor(
Math.random() * matchedClients.length,
);
return matchedClients[randomIndex];
},
});
}
typescript
遍历 services 创建所有 Provider
static forRoot(options: ConsulModuleOptions): DynamicModule {
const consulService = new ConsulService(options);
const grpcProviders: Provider[] = [];
// 为每个 service 创建对应的 DI Provider
if (Array.isArray(options.services)) {
options.services.forEach((service) => {
this.addGrpcProvider(service.serviceName, consulService, grpcProviders);
});
} else {
this.addGrpcProvider(
options.services.serviceName,
consulService,
grpcProviders,
);
}
return {
module: ConsulCoreModule,
providers: [...grpcProviders],
exports: [...grpcProviders],
};
}
typescript
在业务 Service 中使用
旧方式(直接注入 ConsulService)
// ❌ 旧方式:通过 ConsulService 获取 client
@Injectable()
export class AuthService {
constructor(private readonly consulService: ConsulService) {}
onModuleInit() {
this.client = this.consulService.getClient();
}
}
typescript
新方式(直接注入 gRPC 客户端)
// ✅ 新方式:通过 DI 系统直接注入
import { Inject, Injectable, OnModuleInit } from '@nestjs/common';
import { ClientGrpc } from '@nestjs/microservices';
@Injectable()
export class AuthService implements OnModuleInit {
private userService: any;
constructor(
@Inject('nestjs-user-service') // 使用 service name 作为注入 token
private readonly grpcClient: () => Promise<GrpcClientInterface>,
) {}
async onModuleInit() {
const clientInterface = await this.grpcClient();
if (!clientInterface?.client) return;
// 订阅 BehaviorSubject 获取实际的 gRPC 客户端
clientInterface.client.subscribe((client: ClientGrpc | null) => {
if (!client) return;
this.userService = client.getService('UserService');
});
}
}
typescript
调试过程中的关键发现
时序问题:Provider 初始化顺序
调试中发现 grpcClients 数组在 Provider 的 useFactory 执行时可能为空。原因是 ConsulService 的构造函数中调用了 initClients(),但异步的 getServiceInfo() 还没完成。
解决方案:将 push 操作移到 initGrpcClient 方法中 getServiceInfo 调用之前:
private async initGrpcClient(serviceOptions: ConsulServiceOptions): Promise<void> {
const item: GrpcClientInterface = {
client: new BehaviorSubject<ClientGrpc | null>(null),
// ...
};
// 先推入数组,确保 Provider 能找到
ConsulService.grpcClients.push(item);
// 再异步获取服务信息
const serviceInfo = await this.getServiceInfo(serviceOptions.serviceName);
if (serviceInfo) {
const client = this.createGrpcClient(serviceInfo, ...);
item.client.next(client);
}
}
typescript
BehaviorSubject 的首次订阅
BehaviorSubject 初始值为 null,首次订阅时 client 可能为 null。需要加入空值判断:
clientInterface.client.subscribe((client: ClientGrpc | null) => {
if (!client) return; // 首次可能为 null,跳过
this.userService = client.getService('UserService');
});
typescript
调试断点技巧
- 在
initGrpcClient的入口和出口都打上断点,确认 items 是否正确推入 - 在
useFactory内部打上断点,确认grpcClients数组是否有值 - 在
onModuleInit中打上断点,确认注入的值是否为undefined
注入后的调用链路
AuthService.onModuleInit()
→ await this.grpcClient() // 从 DI 容器获取 GrpcClientInterface
→ clientInterface.client.subscribe() // 订阅 BehaviorSubject
→ client.getService('UserService') // 获取 gRPC 服务实例
→ userService.findOne() // 调用微服务方法
text
测试验证
通过 Bruno 或 curl 发起接口请求,验证整个依赖注入链路是否正常:
# 请求 auth 接口
curl http://localhost:3040/api/v1/auth/login
# 预期:成功返回 access token
bash
如果返回 500 错误,按照以下顺序排查:
- Consul 中是否注册了健康的
nestjs-user-service实例 - Health 检查服务是否正常运行
- gRPC 客户端是否成功初始化(检查 BehaviorSubject 的值)
- DI 注入的 token 名称是否与 service name 一致
↑