You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何简便从NestJS DI容器获取指定基类的所有子类实例

NestJS获取所有基类子类实例并实现并行优雅关停方案

核心实现思路

不依赖NestJS内置串行执行的生命周期钩子,通过两种方式从DI容器拿到所有继承BasePoller的实例,在收到SIGTERM/SIGINT信号时用Promise.all并行触发所有实例的关停逻辑,大幅缩短多轮询器场景下的关停耗时。

方案1:装饰器自动收集(推荐,无扫描开销)

通过自定义装饰器给所有轮询器子类绑定统一的多提供者注入令牌,启动时直接注入即可拿到全量实例,零运行时扫描成本。

  1. 首先定义基类、装饰器和注入令牌:
import { SetMetadata, Injectable, Inject } from '@nestjs/common';
import { ModuleRef } from '@nestjs/core';

export const POLLER_INSTANCES = 'POLLER_INSTANCES';
const IS_POLLER_META_KEY = 'is_poller_implementation';

// 轮询器类装饰器,所有继承BasePoller的服务需加该装饰器
export function Poller() {
  return (target: any) => {
    SetMetadata(IS_POLLER_META_KEY, true)(target);
    Reflect.defineMetadata(
      'poller_self_provider',
      {
        provide: POLLER_INSTANCES,
        useExisting: target,
        multi: true,
      },
      target
    );
  };
}

// 轮询器基类
@Injectable()
export abstract class BasePoller {
  abstract start(): Promise<void>;
  abstract stop(): Promise<void>;
}
  1. 所有轮询器实现类加@Poller()装饰器:
@Injectable()
@Poller()
export class OrderStatusPoller extends BasePoller {
  // 实现start、stop逻辑
}
  1. 在根模块中注入实例、绑定信号监听:
import { Module, OnApplicationBootstrap } from '@nestjs/common';

@Module({
  // 导入业务模块、注册所有轮询器服务
})
export class AppModule implements OnApplicationBootstrap {
  constructor(
    @Inject(POLLER_INSTANCES) private readonly pollers: BasePoller[],
    private readonly moduleRef: ModuleRef
  ) {}

  onApplicationBootstrap() {
    // 应用启动完成后绑定系统信号
    process.on('SIGTERM', () => this.parallelShutdown());
    process.on('SIGINT', () => this.parallelShutdown());
  }

  private async parallelShutdown() {
    console.log(`Received shutdown signal, stopping ${this.pollers.length} pollers in parallel`);
    // 并行执行所有轮询器关停逻辑,单个失败不阻塞整体流程
    await Promise.all(
      this.pollers.map(poller => 
        poller.stop().catch(err => console.error('Poller stop failed:', err))
      )
    );
    // 所有轮询器停完再关闭整个Nest应用
    await this.moduleRef.close();
    process.exit(0);
  }
}

方案2:DiscoveryService动态扫描(零侵入,无需装饰器)

如果不想给每个轮询器类单独加装饰器,可以用Nest内置的DiscoveryService扫描DI容器中所有已实例化的提供者,过滤出继承自BasePoller的实例。

  1. 根模块导入DiscoveryModule:
import { Module, OnApplicationBootstrap, DiscoveryModule } from '@nestjs/core';

@Module({
  imports: [DiscoveryModule],
  // 其他模块、服务注册
})
  1. 启动阶段扫描获取轮询器实例:
import { DiscoveryService } from '@nestjs/core';

export class AppModule implements OnApplicationBootstrap {
  private pollers: BasePoller[] = [];

  constructor(
    private readonly discoveryService: DiscoveryService,
    private readonly moduleRef: ModuleRef
  ) {}

  onApplicationBootstrap() {
    // 过滤所有DI容器中的提供者,拿到BasePoller子类实例
    this.pollers = this.discoveryService.getProviders()
      .filter(wrapper => {
        if (!wrapper.instance || !wrapper.metatype) return false;
        return wrapper.metatype.prototype instanceof BasePoller;
      })
      .map(wrapper => wrapper.instance as BasePoller);

    // 信号绑定、并行关停逻辑和方案1完全一致
    process.on('SIGTERM', () => this.parallelShutdown());
    process.on('SIGINT', () => this.parallelShutdown());
  }

  // 复用方案1中的parallelShutdown方法即可
}

优化建议

  • 给单个轮询器的关停逻辑加超时控制,避免单个轮询器卡死导致整个关停流程挂起:
    const withTimeout = (task: Promise<void>, timeout = 5000) => 
      Promise.race([task, new Promise(resolve => setTimeout(resolve, timeout))]);
    
    // 调用时替换为
    await Promise.all(
      this.pollers.map(poller => 
        withTimeout(poller.stop().catch(err => console.error('Poller stop failed:', err)))
      )
    );
    
  • 不要在Nest内置的OnModuleDestroy/OnApplicationShutdown生命周期中写轮询器关停逻辑,这两个钩子会按模块加载顺序串行执行,轮询器数量越多关停越慢。
  • 必须等所有轮询器关停完成后再调用moduleRef.close(),避免数据库连接、HTTP服务提前断开引发业务报错。

内容的提问来源于stack exchange,提问作者Max Heinritz

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 10:33:10