NestJS 中的 AOP:五种方式解耦通用逻辑


NestJS 中的 AOP:五种方式解耦通用逻辑

NestJS 里的一个非常实用的设计思想——AOP(面向切面编程)。如果你写过一些后端代码,肯定遇到过这样的情况:很多地方都需要做同样的事,比如日志记录、权限校验、参数验证、异常处理……如果把这些代码到处复制粘贴,不仅累,还容易出错。AOP 就是用来解决这个问题的,它让我们能把这类“横切关注点”从业务逻辑中抽离出来,统一管理和复用。

在 NestJS 里,AOP 的实现非常优雅,一共有五种方式:Middleware、Guard、Pipe、Interceptor、ExceptionFilter。它们分别在请求处理的不同阶段发挥作用,而且可以灵活地在全局、控制器或路由级别启用。今天我们就来一一拆解,看看它们都是干什么的、怎么用,以及它们之间的执行顺序是怎样的。


什么是 AOP?

先简单说一下 AOP 的概念。我们传统的 MVC 架构中,请求先进 Controller,然后调 Service,最后返回响应。但在这个过程中,总有一些逻辑是“横跨”很多地方的,比如:

  • 打印请求日志
  • 检查用户是否登录
  • 验证参数格式
  • 捕获异常并返回统一格式的错误信息

这些逻辑和核心业务没有直接关系,但又不得不写。AOP 的思想就是把这些“横切”的逻辑单独提取出来,形成一个个“切面”,然后在合适的地方“织入”到主流程中。这样一来,业务代码就干净了,切面逻辑也能复用,还能动态增删。


NestJS 中的五种 AOP 机制

NestJS 底层基于 Express,但它在 Express 的中间件基础上又封装了 Guard、Pipe、Interceptor、ExceptionFilter,让我们可以更精细地控制请求处理流程。下面我们挨个来看。

1. Middleware(中间件)

中间件是 Express 里的老熟人,Nest 也保留了它。中间件在请求进入路由之前执行,可以拿到请求和响应对象,也能调用 next() 把控制权交给下一个中间件或路由处理函数。

主要作用:通用的请求预处理,比如日志记录、请求体解析、跨域设置等。

如何实现
需要实现 NestMiddleware 接口,写一个 use 方法:

import { Injectable, NestMiddleware } from '@nestjs/common';
import { Request, Response, NextFunction } from 'express';

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  use(req: Request, res: Response, next: NextFunction) {
    console.log('Request...', req.method, req.url);
    next(); // 别忘了调用 next,否则请求会卡住
  }
}

如何启用
在模块里实现 NestModule 接口的 configure 方法,指定哪些路由应用这个中间件。

import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common';

@Module({})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes('*'); // 所有路由
  }
}

也可以直接在 main.ts 里用 app.use() 注册全局中间件,但那样无法享受依赖注入。


2. Guard(守卫)

守卫顾名思义,就是守门员,它决定一个请求能不能继续往下走。通常用来做权限验证,比如检查用户是否登录、角色是否匹配。

主要作用:返回 true 放行,返回 false 拒绝请求(默认返回 403 禁止访问)。

如何实现
实现 CanActivate 接口,写 canActivate 方法,返回布尔值或包含布尔值的 Promise/Observable。

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    // 这里做你的校验逻辑,比如检查 token
    return !!request.headers.authorization;
  }
}

如何启用

  • 局部使用:在 Controller 或方法上用 @UseGuards(AuthGuard) 装饰器。
  • 全局使用:

    1. AppModuleproviders 里用 APP_GUARD 令牌注册,这样守卫就在 IoC 容器里,可以注入其他服务。
    2. main.ts 里用 app.useGlobalGuards(new AuthGuard()),但这种方式创建的实例不在容器内,无法注入依赖。

3. Pipe(管道)

管道有两个核心功能:参数转换参数验证。它会在参数被传递给控制器之前执行。

主要作用:把字符串转成数字(比如 ParseIntPipe)、验证对象结构(配合 class-validator)、设置默认值等。

如何实现
实现 PipeTransform 接口,写 transform 方法。

import { PipeTransform, Injectable, ArgumentMetadata, BadRequestException } from '@nestjs/common';

@Injectable()
export class ParseIntPipe implements PipeTransform<string, number> {
  transform(value: string, metadata: ArgumentMetadata): number {
    const val = parseInt(value, 10);
    if (isNaN(val)) {
      throw new BadRequestException('参数必须为数字');
    }
    return val;
  }
}

如何启用

  • 参数级别:@Param('id', ParseIntPipe) id: number
  • 方法级别:@UsePipes(SomePipe)
  • 控制器级别:同样用 @UsePipes()
  • 全局:app.useGlobalPipes(new SomePipe()) 或通过 APP_PIPE 令牌注入。

Nest 内置了很多实用的管道,比如 ValidationPipeParseBoolPipeParseUUIDPipe 等,直接用就行。


4. Interceptor(拦截器)

拦截器是 AOP 中最灵活的一环。它可以在控制器方法执行前后添加逻辑,甚至可以完全改写返回值或抛出异常。它基于 RxJS,所以你能用 RxJS 的操作符来处理响应流。

主要作用:日志记录、响应映射(比如统一包装成 { data, code, message })、超时处理、缓存等。

如何实现
实现 NestInterceptor 接口,写 intercept 方法。调用 next.handle() 就会触发控制器方法,返回的是一个 Observable,你可以用 RxJS 操作符对数据流进行各种操作。

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    return next.handle().pipe(
      tap(() => console.log(`Request took ${Date.now() - now}ms`)),
    );
  }
}

如何启用
类似 Guard,可以用 @UseInterceptors(LoggingInterceptor) 局部启用,或通过 APP_INTERCEPTOR 令牌全局注册,或者在 main.tsapp.useGlobalInterceptors()

拦截器 vs 中间件
拦截器能拿到目标控制器和方法的元数据(通过 context.getClass()context.getHandler()),还能结合自定义装饰器实现更精细的控制,这是中间件做不到的。所以如果逻辑和特定业务相关,用拦截器更合适;纯通用的请求预处理,中间件就够了。


5. ExceptionFilter(异常过滤器)

异常过滤器负责处理应用内抛出的所有异常(包括未捕获的),然后返回统一的错误响应。Nest 内置了很多 HTTP 异常类(如 BadRequestExceptionNotFoundException 等),但你可以自定义过滤器来完全控制响应格式。

主要作用:捕获异常,返回友好的 JSON 或页面,记录错误日志等。

如何实现
实现 ExceptionFilter 接口,用 @Catch() 指定要捕获的异常类型,然后在 catch 方法里处理。

import { ExceptionFilter, Catch, ArgumentsHost, HttpException } from '@nestjs/common';
import { Request, Response } from 'express';

@Catch(HttpException) // 只捕获 HttpException 及其子类
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    const ctx = host.switchToHttp();
    const response = ctx.getResponse<Response>();
    const request = ctx.getRequest<Request>();
    const status = exception.getStatus();

    response.status(status).json({
      statusCode: status,
      timestamp: new Date().toISOString(),
      path: request.url,
      message: exception.message,
    });
  }
}

如何启用

  • 方法级别:@UseFilters(HttpExceptionFilter)
  • 控制器级别
  • 全局:app.useGlobalFilters(new HttpExceptionFilter()) 或通过 APP_FILTER 令牌注入。

五种机制的调用顺序

这么多切面,它们到底按什么顺序执行?其实 NestJS 有一套清晰的流程:

  1. 请求进来,先经过中间件(Middleware)。
  2. 到达路由后,依次执行守卫(Guard)——判断是否有权限。
  3. 通过守卫后,进入拦截器intercept 方法前半部分(比如计时开始)。
  4. 然后管道(Pipe)对参数进行验证和转换。
  5. 参数准备好后,调用控制器方法。
  6. 控制器返回结果(可能是普通值或 Observable),然后回到拦截器的后半部分(比如计时结束、修改响应)。
  7. 如果整个过程中抛出异常,会被异常过滤器(ExceptionFilter)捕获并处理。

注意:守卫和管道都在拦截器的 next.handle() 之前执行,但守卫更靠前。中间件是最外层的,甚至可以在守卫之前。


总结

NestJS 提供的这五种 AOP 机制,覆盖了请求处理的整个生命周期,让我们可以把通用逻辑优雅地抽离出来。什么时候用哪个,可以这样简单记:

  • 中间件:通用的、和业务无关的预处理(比如日志、CORS)。
  • 守卫:权限校验,决定能不能访问。
  • 管道:参数校验和转换,确保数据正确。
  • 拦截器:对控制器前后进行细粒度扩展,可以操作响应流。
  • 异常过滤器:全局异常处理,统一错误格式。

声明:麋鹿与鲸鱼|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - NestJS 中的 AOP:五种方式解耦通用逻辑


Carpe Diem and Do what I like