NestJS如何通过装饰器实现请求头值自动注入Service方法
实现方案
首先明确核心前提:NestJS 中 Service 默认是全局单例,和单次请求生命周期完全解绑,无法直接凭空获取当前请求的上下文信息,整套实现不需要手动在Controller透传请求头,分三步完成即可。
1. 全局存储请求上下文
用 Node.js 原生的 AsyncLocalStorage 做异步上下文存储即可,不需要额外引入第三方依赖,性能也远高于把Service改成请求作用域的方案。
首先新建上下文工具文件:
// src/context/request-context.ts import { AsyncLocalStorage } from 'async_hooks'; export interface RequestContext { item: string; // 对应请求头 HEADER-ITEM 的值 } export const requestContextStorage = new AsyncLocalStorage<RequestContext>(); // 工具方法:在任意位置获取当前请求的item值 export function getCurrentRequestItem(): string { const store = requestContextStorage.getStore(); if (!store) throw new Error('不能在请求生命周期外获取请求上下文'); return store.item; }
然后写一个全局中间件,在请求进入的第一时间把请求头的值存入上下文:
// src/context/request-context.middleware.ts import { Injectable, NestMiddleware } from '@nestjs/common'; import { Request, Response, NextFunction } from 'express'; import { requestContextStorage } from './request-context'; @Injectable() export class RequestContextMiddleware implements NestMiddleware { use(req: Request, res: Response, next: NextFunction) { const headerItem = req.headers['header-item'] as string; // 把请求头值存入异步上下文,后续所有异步调用链都能拿到 requestContextStorage.run({ item: headerItem }, () => next()); } }
在根模块注册全局中间件,让所有路由都生效:
// src/app.module.ts import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common'; import { RequestContextMiddleware } from './context/request-context.middleware'; @Module({ // 保留你原有的 imports、controllers、providers 配置 }) export class AppModule implements NestModule { configure(consumer: MiddlewareConsumer) { consumer.apply(RequestContextMiddleware).forRoutes('*'); } }
2. 实现@WithItemFromHeader()装饰器
注意:NestJS 原生的参数装饰器注入逻辑只在Controller层生效,Service层的参数装饰器需要搭配方法装饰器,重写原方法完成参数注入。
新建装饰器文件:
// src/decorators/with-item-from-header.decorator.ts import { getCurrentRequestItem } from '../context/request-context'; const INJECT_ITEM_META_KEY = Symbol('inject_item_param_index'); // 参数装饰器:标记哪个参数需要注入带item的record对象 export function WithItemFromHeader(): ParameterDecorator { return (target, propertyKey, parameterIndex) => { // 把需要注入的参数位置存在元数据上 Reflect.defineMetadata(INJECT_ITEM_META_KEY, parameterIndex, target, propertyKey); }; } // 方法装饰器:重写原方法,自动给标记的参数注入值 export function InjectRequestItem(): MethodDecorator { return (target, propertyKey, descriptor: PropertyDescriptor) => { const originalMethod = descriptor.value; const injectParamIndex = Reflect.getMetadata(INJECT_ITEM_META_KEY, target, propertyKey); descriptor.value = async function (...args: any[]) { const currentItem = getCurrentRequestItem(); // 保留原有传入的record字段,追加item值 args[injectParamIndex] = { ...(args[injectParamIndex] || {}), item: currentItem }; // 执行业务原逻辑 return originalMethod.apply(this, args); }; return descriptor; }; }
3. 改造业务代码
Controller层不需要再手动提取请求头、透传item参数,直接传业务参数即可:
@Controller() class UserController { @Post("/order") @ApiBearerAuth() async createOrder(@Body() order: Order) { // 直接传业务参数,不需要处理HEADER-ITEM return await this.adminService.setOrder(order); } }
Service层按期望的形式使用装饰器即可:
@Injectable() class AdminService { @InjectRequestItem() async setOrder(order: Order, @WithItemFromHeader() record: RecordObject) { // 这里record已经自动带上item字段,直接使用即可 } @InjectRequestItem() async setPurchase(purchase: Purchase, @WithItemFromHeader() record: RecordObject) { // 逻辑同上 } @InjectRequestItem() async setDate(date: Date, @WithItemFromHeader() record: RecordObject) { // 逻辑同上 } }
可选简化方案
如果不需要强制用参数注入的形式,甚至可以不用写装饰器,直接在Service方法里调用之前写的getCurrentRequestItem()方法就能拿到请求头的值,代码更精简:
@Injectable() class AdminService { async setOrder(order: Order) { const item = getCurrentRequestItem(); // 直接使用item做业务逻辑 } }
注意:不要为了拿请求上下文把Service改成请求作用域(
@Injectable({ scope: Scope.REQUEST })),这种方案会导致Service每次请求都重新实例化,不仅性能差,还会引发依赖注入链的作用域溢出问题,AsyncLocalStorage是目前Node.js生态下传递请求上下文的最优方案。
内容的提问来源于stack exchange,提问作者Heartbit
相关产品推荐
相关产品推荐

