Nest.js任意位置获取请求遇问题:请求作用域致onModuleInit失效
我需要在调用链深处的Service中获取Request对象,但不想通过参数传递(这会带来大量修改)。我尝试使用请求作用域提供者实现该需求:
@Injectable({ scope: Scope.REQUEST }) export class FetcherService { constructor( private defaultProductFetcher: DefaultProductFetcher, private loopbackFetcher: LoopbackFetcher, @Inject(REQUEST) private readonly request: Request, ) {} // ...use this.request here for proxying headers to other services }
但此举却导致应用中所有Service的onModuleInit方法无法被调用,进而导致应用无法正常运行。现提出以下问题:
- 为何所有
onModuleInit方法都会受到影响? - 是否存在更优的获取请求信息的方式?
问题1解答
当你把FetcherService标记为请求作用域(Scope.REQUEST)后,如果其他单例作用域的Service依赖了它,这些单例Service会被强制降级为请求作用域。
NestJS里,单例Service会在应用启动时初始化,此时会触发onModuleInit钩子。但如果单例被降级为请求作用域,它的实例只会在每次请求到来时才创建,应用启动阶段不会初始化这些Service实例,自然也就不会调用onModuleInit方法。如果你的应用里很多核心Service都依赖了FetcherService,就会出现所有onModuleInit都不执行的情况。
问题2解答
有两种更优的方案可以避免这种问题:
方案1:使用异步上下文(AsyncLocalStorage)
Node.js的async_hooks模块提供的AsyncLocalStorage,可以在整个请求生命周期中存储和获取请求上下文,不需要修改Service的作用域。
示例实现:
- 创建上下文管理Service:
import { Injectable } from '@nestjs/common'; import { AsyncLocalStorage } from 'async_hooks'; @Injectable() export class RequestContextService { private readonly als = new AsyncLocalStorage<Map<string, any>>(); createContext(request: Request) { const context = new Map(); context.set('request', request); return context; } run(context: Map<string, any>, callback: () => void) { this.als.run(context, callback); } getRequest(): Request { return this.als.getStore()?.get('request'); } }
- 创建拦截器,在请求进入时初始化上下文:
import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common'; import { Observable } from 'rxjs'; import { RequestContextService } from './request-context.service'; @Injectable() export class RequestContextInterceptor implements NestInterceptor { constructor(private readonly contextService: RequestContextService) {} intercept(context: ExecutionContext, next: CallHandler): Observable<any> { const request = context.switchToHttp().getRequest(); const ctx = this.contextService.createContext(request); return this.contextService.run(ctx, () => next.handle()); } }
- 在模块中全局注册拦截器,之后任何Service都可以注入
RequestContextService并调用getRequest()获取请求对象,所有Service依然保持单例作用域,onModuleInit正常执行。
方案2:使用请求作用域的中间Service
不要直接让核心单例Service依赖请求作用域的FetcherService,而是创建一个仅负责获取请求上下文的请求作用域Service,然后在需要使用请求信息的地方,通过ModuleRef动态获取这个请求作用域Service的实例,避免单例Service被降级。
示例:
- 创建请求作用域的上下文Service:
import { Injectable, Scope, Inject } from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { Request } from 'express'; @Injectable({ scope: Scope.REQUEST }) export class RequestScopedContext { constructor(@Inject(REQUEST) public readonly request: Request) {} }
- 在需要使用请求信息的Service中,注入
ModuleRef来动态获取实例:
import { Injectable, ModuleRef } from '@nestjs/common'; import { RequestScopedContext } from './request-scoped-context.service'; @Injectable() export class FetcherService { constructor(private readonly moduleRef: ModuleRef) {} async fetchProduct() { const context = await this.moduleRef.resolve(RequestScopedContext); const request = context.request; // 使用request处理逻辑 } }
这种方式下,FetcherService依然是单例,onModuleInit正常执行,只有在实际需要请求信息时才会创建请求作用域的上下文实例。
内容的提问来源于stack exchange,提问作者comonadd

