Nest AOP 是指在 Nest.js 框架中使用面向切面编程(Aspect-Oriented Programming)的技术。面向切面编程是一种编程范式,它允许开发者通过在代码中定义切面(Aspect),来将横切关注点(Cross-cutting Concerns)与核心业务逻辑进行解耦。
在 Nest.js 中,开发者可以使用 Nest AOP 模块来实现 AOP 功能。通过使用装饰器(Decorator)来定义切面,并使用注入依赖(Dependency Injection)来实现对切面的使用。Nest AOP 提供了一系列的装饰器和注解,开发者可以使用它们来在不修改原有代码的情况下,为应用程序的不同部分添加额外的行为。
使用 Nest AOP,开发者可以很方便地实现一些跟业务逻辑无关但又必要的功能,比如日志记录、性能监控、缓存等。同时,它也能提高代码的可维护性和可测试性,使应用程序更易于理解和扩展。
当请求过来,可能会经过 Controller(控制器)、Service(服务)、Repository(数据库访问) 的逻辑
当我们想在这些逻辑里加入一些通用逻辑该怎么加呢?
可能我们会直接想到在哪用到就在哪里增加代码,但是这样会造成通用的逻辑入侵到了业务代码里面,所以AOP (面向切面编程) 孕育而生,它的作用是一些通用逻辑分离到切面中,保持业务逻辑的纯粹性,这样切面逻辑可以复用,还可以动态的增删。
例如下图:
Nest 实现 AOP 的方式更多,一共有五种,包括 Middleware、Guard、Pipe、Interceptor、ExceptionFilter。
1、创建项目
首先我们先创建一个项目
nest new aop
2、中间件 Middleware
Nest中间件(Middleware)是一种用于处理HTTP请求和响应的中间层。它可以在处理请求之前和之后执行一些操作,例如验证请求、记录日志、处理异常等。
在Nest中,中间件是一个类或函数,它实现了NestMiddleware接口。在类中,可以使用use方法装饰器将该中间件注册到应用程序模块中。在函数中,可以使用@Middleware()装饰器将该中间件注册到相应的路由处理器上。
中间件可以被全局应用或只应用于特定的路由或控制器。全局中间件会应用于应用程序中所有的请求。而特定路由或控制器的中间件只会应用于指定的路由或控制器。
2.1、全局使用
在 main.ts 里通过 app.use 使用:
app.use(function (req: Request, res: Response, next: NextFunction) {
console.log('之前', req.url);
next();
console.log('之后');
})
在 app.controller.ts 中添加
@Get()
getHello(): string {
console.log('进入');
return this.appService.getHello();
}
接着启动项目
pnpm run start:dev
浏览器访问 http://localhost:3000/
可以看到控制台按照顺序打印出了 之前 进入 之后
这里其实和vue的router路由守卫差不多意思 在进入之前 可以进行一些逻辑判断 接着调用next() 就可进入 接着执行完成里面的内容之后 可以再进行一些操作
大家有兴趣可以多建几个接口测试一下 例如
@Get('www')
www(): string {
console.log('进入 www');
return '我调用了www'
}
接着我们游览器打开 http://localhost:3000/www 可以看到控制台
这就是全局中间件 Middleware 它可以让我们在调用接口之前 做一些逻辑复用 在处理请求之前和之后执行一些操作
2.1、路由使用
中间件 Middleware除了可以在全局使用 也支持路由中间件
首先 创建一个路由中间件:
nest g middleware log --no-spec --flat
–no-spec 是不生成测试文件 --flat 是平铺,不生成目录
可以看到生成以下代码
接着我们修改一下 log.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common';
@Injectable()
export class LogMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
console.log('之前', req.url);
next();
console.log('之后');
}
}
并把之前的全局中间件 Middleware 注释掉
我们在 app.module.ts 的 AppModule 中 implements NestModule{,在 configure 方法里配置
consumer.apply(LogMiddleware).forRoutes(‘aaa*’);
此处是配置哪些路由生效
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LogMiddleware } from './log.middleware';
@Module({
imports: [],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LogMiddleware).forRoutes('www*')
}
}
接着我们游览器打开 http://localhost:3000/www 可以看到控制台这就是全局中间件和路由中间件的区别。
3、路由守卫 Guard
NestJS 提供了一种称为 Guard 的路由守卫机制。Guard 可以用于保护路由,只有满足特定条件的请求才能继续执行路由处理程序。 可以用于在调用某个 Controller 之前判断权限,返回 true 或者 false 来决定是否放行
3.1、接口路由守卫
首先我们创建个 Guard:
nest g guard login --no-spec --flat
生成如下代码:
上面代码实现了CanActivate接口,用于判断用户是否具有激活路由的权限。canActivate方法接收一个ExecutionContext上下文对象作为参数,返回一个布尔值、Promise或Observable,用于表示是否可以激活对应的路由。
我们在代码这加入打印语句 接着把return 返回 false 代表只要调用了这个路由守卫 的接口 都是失败的
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class LoginGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
console.log('登录 校验');
return false;
}
}
接着我们在部分接口 使用 路由守卫 如下代码:(app.controller.ts)
import { Controller, Get, UseGuards } from '@nestjs/common';
import { AppService } from './app.service';
import { LoginGuard } from './login.guard';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) { }
@Get()
getHello(): string {
console.log('进入');
return this.appService.getHello();
}
@Get('www')
www(): string {
console.log('进入 www');
return '我调用了www'
}
/**
* 使用了路由守卫的接口
*/
@Get('test')
@UseGuards(LoginGuard)
test(): string {
return '调用成功'
}
}
接着我们打开 http://localhost:3000/test 调用test 接口
可以看到 没有调用test接口的权限 返回了403
其他接口是可以正常调用的 只有这个test接口设置了路由守卫 返回了false代表 用了路由守卫的接口都没有权限访问
3.2、全局路由守卫
首先我们把之前 app.controller.ts 的 @UseGuards(LoginGuard) 删除掉先
接着我们在 main.ts 中引入 await app.useGlobalGuards(new LoginGuard())
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NextFunction } from 'express';
import { LoginGuard } from './login.guard';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// app.use(function (req: Request, res: Response, next: NextFunction) {
// console.log('之前', req.url);
// next();
// console.log('之后');
// })
await app.useGlobalGuards(new LoginGuard())
await app.listen(3000);
}
bootstrap();
接着访问 http://localhost:3000/www http://localhost:3000/test 可以看到接口都403 没有权限访问 代表全局路由守卫使用成功
除了在main.ts中使用 await app.useGlobalGuards(new LoginGuard())的全局方式 还有另一种方式
我们先把第一种全局方式的使用先注释掉 如下图:
在 AppModule 里声明:
import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { LogMiddleware } from './log.middleware';
import { APP_GUARD } from '@nestjs/core';
import { LoginGuard } from './login.guard';
@Module({
imports: [],
controllers: [AppController],
providers: [
AppService,
{
provide: APP_GUARD,
useClass: LoginGuard
}
],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LogMiddleware).forRoutes('www*')
}
}
接着访问 http://localhost:3000/www http://localhost:3000/test 可以看到接口都403 没有权限访问 代表全局路由守卫使用成功
可以看到,全局路由守卫 Guard 依然是生效的
两种全局守卫的区别:
1、在main.ts 手动 new 的 Guard 实例: 不在 IoC 容器里
2、provider 的方式:在 IoC 容器里的,可以注入别的 provider
4、拦截器 Interceptor
在 NestJS 中,拦截器 (Interceptor) 是一种特殊的中间件,可以在请求处理流程中的各个阶段进行拦截和操作。拦截器可以用来实现一些通用的功能,比如在请求处理之前或之后进行日志记录、权限验证、数据转换等操作。它可以对请求和响应进行修改,也可以抛出异常来中断请求处理流程。
首先我们创建个 interceptor:
nest g interceptor time --no-spec --flat
该代码段是一个Nest.js框架中的拦截器(Interceptor)实现,用于拦截HTTP请求并记录处理时间。具体来说:
- TimeInterceptor类实现了NestInterceptor接口,表明它是一个拦截器。
- intercept方法是拦截器的核心方法,会在请求处理之前被调用。
- intercept方法接收两个参数:context提供了关于当前请求和路由的信息,next是CallHandler接口的实例,用于继续处理请求链。
- 方法内部通过调用next.handle()来继续处理请求链,并返回处理结果的Observable。
Controller 之前之后的处理逻辑可能是异步的。Nest 里通过 rxjs 来组织它们,所以可以使用 rxjs 的各种 operator。
time.interceptor.ts 内容更改如下:
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
/**
* TimeInterceptor 类实现了NestInterceptor接口,用于拦截请求并进行时间记录。
*/
@Injectable()
export class TimeInterceptor implements NestInterceptor {
/**
* intercept 方法会在请求处理之前被调用。
* @param context 执行上下文,提供了关于当前请求和路由的信息。
* @param next CallHandler 接口的实例,用于继续处理请求链。
* @returns 返回一个Observable,代表处理请求的结果。
*/
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now();
// 继续处理请求链,并返回结果
return next.handle().pipe(
tap(() => {
console.log('time: ', Date.now() - startTime)
})
);
}
}
先把之前的路由守卫注释掉:
接着在接口上使用拦截器 如下:(app.controller.ts)
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { LoginGuard } from './login.guard';
import { TimeInterceptor } from './time.interceptor';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) { }
@Get()
getHello(): string {
console.log('进入');
return this.appService.getHello();
}
@Get('www')
www(): string {
console.log('进入 www');
return '我调用了www'
}
/**
* 使用了路由守卫的接口
*/
@Get('test')
// @UseGuards(LoginGuard)
@UseInterceptors(TimeInterceptor)
test(): string {
return '调用成功'
}
}
调用 http://localhost:3000/test 接口
可以看到,interceptor 生效了
Interceptor 和 Middleware 的区别,参数的不同,interceptor 可以拿到调用的 controller 和 handler,如下 打印 context.getClass(), context.getHandler():
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
/**
* TimeInterceptor 类实现了NestInterceptor接口,用于拦截请求并进行时间记录。
*/
@Injectable()
export class TimeInterceptor implements NestInterceptor {
/**
* intercept 方法会在请求处理之前被调用。
* @param context 执行上下文,提供了关于当前请求和路由的信息。
* @param next CallHandler 接口的实例,用于继续处理请求链。
* @returns 返回一个Observable,代表处理请求的结果。
*/
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
console.log(context.getClass(), context.getHandler());
const startTime = Date.now();
// 继续处理请求链,并返回结果
return next.handle().pipe(
tap(() => {
console.log('time: ', Date.now() - startTime)
})
);
}
}
再次调用 http://localhost:3000/test 接口
Interceptor 支持每个路由单独启用,只作用于某个 handler,也可以在 controller 级别启动,作用于下面的全部 handler
import { Controller, Get, UseGuards, UseInterceptors } from '@nestjs/common';
import { AppService } from './app.service';
import { LoginGuard } from './login.guard';
import { TimeInterceptor } from './time.interceptor';
@Controller()
@UseInterceptors(TimeInterceptor)
export class AppController {
constructor(private readonly appService: AppService) { }
@Get()
getHello(): string {
console.log('进入');
return this.appService.getHello();
}
@Get('www')
www(): string {
console.log('进入 www');
return '我调用了www'
}
/**
* 使用了路由守卫的接口
*/
@Get('test')
// @UseGuards(LoginGuard)
test(): string {
return '调用成功'
}
}
这样 这个 controller 下的所有接口都会经过拦截器
同时也支持两种 全局的方式
1、第一种
app.useGlobalInterceptors(new TimeInterceptor())
2、第二种
{
provide: APP_INTERCEPTOR,
useClass: TimeInterceptor
}
除了路由的权限控制、目标 Controller 之前之后的处理这些都是通用逻辑外,对参数的处理也是一个通用的逻辑,所以 Nest 也抽出了对应的切面,也就是 Pipe
5、管道 Pipe
nestjs中的Pipe是一种中间件,它用于在处理请求之前或之后对请求进行过滤、验证或转换。Pipe可以用来检查输入数据的有效性、转换数据格式、应用默认值等。
在nestjs中,可以使用自定义Pipe来处理请求的输入数据。自定义Pipe必须实现PipeTransform接口,并实现transform方法。transform方法接收两个参数:value和metadata。其中,value是请求输入的数据,metadata是包含有关装饰器元数据的对象。
nestjs还提供了一些内置的Pipe,用于处理常见的验证和转换场景。例如,ValidationPipe用于验证输入数据是否符合指定的验证规则,并返回具体的验证错误信息。ParseIntPipe用于将输入数据转换为整数类型。
在nestjs中,可以在控制器中使用Pipe装饰器来应用Pipe。例如,可以使用@UsePipes装饰器将一个或多个Pipe应用于整个控制器或特定的方法。可以使用@Pipe装饰器将单个Pipe应用于控制器或方法。
首先创建个 pipe:
nest g pipe validate --no-spec --flat
以实现 transform 方法 为例:
里面可以对传入的参数值 value 做参数验证,比如格式、类型是否正确,不正确就抛出异常。也可以做转换,返回转换后的值。 这里的 value 就是传入的参数,如果不能转成数字,就返回参数错误,否则乘 2 再传入
import { ArgumentMetadata, BadRequestException, Injectable, PipeTransform } from '@nestjs/common';
@Injectable()
export class ValidatePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
if(Number.isNaN(parseInt(value))){
throw new BadRequestException(`参数${metadata.data}错误`)
}
return typeof value === 'number' ? value * 0.1 : parseInt(value) * 2 ;
}
}
5.1、接口级别Pipe
接着在 app.controller.ts 新增一个接口
@Get('ccc')
ccc(@Query('num', ValidatePipe) num: number) {
return num + 1;
}
http://localhost:3000/ccc?num=5
http://localhost:3000/ccc?num=%E5%BC%82%E5%B8%B8
可以看到,参数错误的时候返回了 400 响应,参数正确的时候也乘 10 传入了 handler。
这就是 Pipe 的作用。
5.2、controller级别Pipe
import { Controller, Get, Query, UseGuards, UseInterceptors, UsePipes } from '@nestjs/common';
import { AppService } from './app.service';
import { LoginGuard } from './login.guard';
import { TimeInterceptor } from './time.interceptor';
import { ValidatePipe } from './validate.pipe';
@Controller()
// @UseInterceptors(TimeInterceptor)
@UsePipes(ValidatePipe)
export class AppController {
constructor(private readonly appService: AppService) { }
@Get()
getHello(): string {
console.log('进入');
return this.appService.getHello();
}
@Get('www')
www(): string {
console.log('进入 www');
return '我调用了www'
}
@Get('test')
// @UseGuards(LoginGuard)
// @UseInterceptors(TimeInterceptor)
test(): string {
return '调用成功'
}
@Get('ccc')
ccc(@Query('num') num: number) {
return num + 1;
}
}
http://localhost:3000/ccc?num=5
http://localhost:3000/ccc?num=%E5%BC%82%E5%B8%B8
可看到如果定义在controller级别 则整个controller中的接口都有此校验
5.2、全局级别Pipe
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { NextFunction } from 'express';
import { LoginGuard } from './login.guard';
import { TimeInterceptor } from './time.interceptor';
import { ValidatePipe } from './validate.pipe';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
// app.use(function (req: Request, res: Response, next: NextFunction) {
// console.log('之前', req.url);
// next();
// console.log('之后');
// })
// app.useGlobalGuards(new LoginGuard())
app.useGlobalPipes(new ValidatePipe())
await app.listen(3000);
}
bootstrap();
Nest 内置了一些 Pipe 如下
- ValidationPipe 是一个预定义的管道,用于验证请求体、请求参数、请求头等数据的有效性
ValidationPipe可以在控制器路由处理程序执行之前,通过使用装饰器将其应用在相应的控制器方法上。它会自动根据装饰器提供的元数据将输入数据校验为特定的类。
- ParseIntPipe 用于将路径参数或查询参数中的字符串转换为整数类型。
当定义一个路由处理程序(controller)并指定路径参数或查询参数时,可以使用ParseIntPipe来确保接收到的参数是整数类型。
- ParseBoolPipe 用于将传入的字符串参数解析为布尔值
ParseBoolPipe的作用是将传入的字符串参数解析为布尔值。如果传入的字符串参数是"true"或"t"或"1",则解析为true;如果传入的字符串参数是"false"或"f"或"0",则解析为false。
- ParseArrayPipe 用于将输入数据解析为数组。它可以用于验证和转换输入数据。
使用ParseArrayPipe可以方便地将输入数据转换为数组,无论输入数据是字符串、JSON字符串还是其他格式,都可以通过ParseArrayPipe进行解析。
- ParseUUIDPipe 是nestjs中的一种预定义管道,主要用于将传入的UUID字符串转换为UUID对象,并进行验证。
当使用 ParseUUIDPipe 时,nestjs会自动将传入的UUID字符串转换为UUID对象,并进行验证。如果传入的字符串不是合法的UUID格式,则会抛出一个异常。这样,我们就可以在控制器中使用 UUID 对象而不是字符串来处理数据。
- DefaultValuePipe 用于在控制器方法参数中提供默认值。
使用DefaultValuePipe非常简单,只需在控制器方法参数上加上@Query()装饰器,并在装饰器中传递一个对象参数,对象中包含一个名为’defaultValue’的属性,该属性的值即为默认值。
以下是一个使用DefaultValuePipe的示例:
@Get()
async getUsers(@Query('page', new DefaultValuePipe(1)) page: number) {
// 如果没有传递page参数,它的值将会是默认值1
// ...
}
- ParseEnumPipe 用于将请求参数转换为枚举类型。
当我们接收到一个请求参数,并希望将其转换为枚举类型时,可以使用ParseEnumPipe。它将自动尝试将请求参数值转换为指定的枚举类型,如果无法转换,则抛出一个错误。
- ParseFloatPipe 用于将输入的字符串转换为浮点数类型。它会尝试解析输入字符串,并返回对应的浮点数值。
该管道的作用是在处理输入数据之前,对输入进行验证和转换。通过使用ParseFloatPipe,开发者可以确保输入的字符串可以正确解析为浮点数,并在解析失败时抛出错误。
- ParseFilePipe 用于解析文件上传请求中的文件数据。
使用ParseFilePipe时,可以在控制器的参数上使用@ParseFilePipe装饰器,指定要解析的文件字段。例如:
@Post('upload')
uploadFile(@UploadedFile(@ParseFilePipe('file')) file: any) {
// 处理上传的文件
}
5.3、异常处理 ExceptionFilter
ExceptionFilter 是一个可用于全局异常处理的模块,它允许开发人员自定义处理应用程序中发生的所有异常的行为。通过实现自定义的 ExceptionFilter,可以捕获应用程序中未处理的异常,并针对不同的异常类型执行特定的操作,如记录日志、返回错误信息等。它可以用于处理应用程序抛出的任何类型的异常,包括 HTTP 请求异常和自定义异常。可以将 ExceptionFilter 注册为全局过滤器,或者在特定的控制器或路由上使用局部过滤器。
创建一个 filter
nest g filter test --no-spec --flat
修改代码如下:(test.filter.ts)
import { ArgumentsHost, BadRequestException, Catch, ExceptionFilter } from '@nestjs/common';
import { Response } from 'express';
@Catch(BadRequestException)
export class TestFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const response: Response = host.switchToHttp().getResponse();
response.status(400).json({
statusCode: 400,
message: 'test: ' + exception.message
})
}
}
以及:(app.controller.ts)
import { Controller, Get, Query, UseFilters, UseGuards, UseInterceptors, UsePipes } from '@nestjs/common';
import { AppService } from './app.service';
import { LoginGuard } from './login.guard';
import { TimeInterceptor } from './time.interceptor';
import { ValidatePipe } from './validate.pipe';
import { TestFilter } from './test.filter';
@Controller()
// @UseInterceptors(TimeInterceptor)
// @UsePipes(ValidatePipe)
export class AppController {
constructor(private readonly appService: AppService) { }
@Get()
getHello(): string {
console.log('进入');
return this.appService.getHello();
}
@Get('www')
www(): string {
console.log('进入 www');
return '我调用了www'
}
@Get('test')
// @UseGuards(LoginGuard)
// @UseInterceptors(TimeInterceptor)
test(): string {
return '调用成功'
}
@Get('ccc')
@UseFilters(TestFilter)
ccc(@Query('num') num: number) {
return num + 1;
}
}
http://localhost:3000/ccc?num=asdad
以下是Nestjs中内置的所有HTTP相关异常:
- BadRequestException:表示客户端发出了一个无效的请求。
- UnauthorizedException:表示未经授权的请求。
- NotFoundException:表示请求的资源不存在。
- ForbiddenException:表示禁止访问请求的资源。
- NotAcceptableException:表示请求的内容不被服务器接受。
- RequestTimeoutException:表示请求超时。
- ConflictException:表示请求的资源与现有资源冲突。
- GoneException:表示请求的资源已经不存在。
- PayloadTooLargeException:表示请求的负载过大。
- UnsupportedMediaTypeException:表示请求的媒体类型不受支持。
- UnprocessableEntityException:表示请求的实体无法被处理。
- InternalServerErrorException:表示服务器内部发生了错误。
- NotImplementedException:表示请求的功能尚未实现。
- BadGatewayException:表示网关或代理服务器接收到无效的响应。
- GatewayTimeoutException:表示网关或代理服务器在请求处理时超时。
- ServiceUnavailableException:表示服务器暂时不可用。
- GatewayTimeoutException:表示网关或代理服务器在请求处理时超时。
这些是一些常见的HTTP异常,但您也可以根据您的需求自定义自己的异常。
6、AOP 机制的顺序
Middleware、Guard、Pipe、Interceptor、ExceptionFilter 都可以透明的添加某种处理逻辑到某个路由或者全部路由,这就是 AOP 的好处。
在 NestJS 应用中,Middleware、Guard、Pipe、Interceptor 和 ExceptionFilter 的执行顺序如下:
- Middleware:NestJS 中的中间件按照注册的顺序依次执行。每个中间件可以在请求被处理之前或之后执行一些操作。在请求到达控制器之前,中间件按照注册的顺序依次执行。在请求处理完成后,中间件按照相反的顺序依次执行。
- Guard:守卫层在中间件执行之后,控制器处理之前运行。它可以根据一些条件决定是否允许请求通过。守卫可以是同步的也可以是异步的,可以影响请求的生命周期。
- Pipe:管道用于处理请求数据的转换和验证。管道可以在请求到达控制器之前或之后应用。在控制器处理请求之前,NestJS 会将请求数据传递给所有注册的管道并进行处理。
- Interceptor:拦截器在控制器处理请求之前和之后运行。拦截器可以用于对请求和响应做一些处理,比如在处理前添加、修改或删除一些数据,或在处理后进行日志记录。
- ExceptionFilter:异常过滤器用于处理控制器中出现的异常。它可以捕获并处理控制器中抛出的异常,然后返回适当的响应。
总结起来,NestJS 应用中这些组件的执行顺序是:Middleware -> Guard -> Pipe -> Interceptor -> Controller -> Interceptor -> Pipe -> ExceptionFilter。