Skip to main content

AOP

AOP

以一个HTTP请求为例,客户端发送请求时会经过 Controller、Service、DB 等模块,如果想要在这些模块中加入一些操作,例如数据验证、权限校验或者日志统计等等,该如何操作呢?

一般情况下可能会在 Controller 中加入参数校验或者权限校验逻辑,如果不通过,就直接返回错误,很多情况下这样可以解决一些问题。但是,如果有多个功能模块都需要进行校验,并且校验的逻辑几乎相同,那就意味着,需要在多个 Controller 中重复加入这段逻辑。

这样做会导致公共逻辑与业务逻辑耦合,所以,我们需要一个方式,做到统一管理。

如上,在 Controller 层的前后,可以以切一刀的方式来统一处理公共逻辑,这样,就可以减少对 Controller、Service等业务代码的入侵。

这样的方式叫做AOP(Aspect-Oriented Programming),即面向切面编程。它是一种编程范式,它通过分离关注点来增强代码的模块化。AOP 允许开发者在不改变原有代码的前提下,在程序的特定位置添加行为,通常用于实现横切关注点,如日志记录、性能监控、安全检查、事务管理等。

在一些面向对象的后端系统中,AOP还有很多的概念:

  • 切面(Aspect) 切面是用来定义横切关注点的模块。它包含了额外的行为和逻辑,可以在程序的不同部分被动态织入。
  • 连接点(Join Point) 连接点是程序执行过程中可以插入切面的特定点。例如:方法调用、方法执行、属性访问等。
  • 切入点(Pointcut) 切入点定义了在什么条件下插入切面逻辑。它通常通过表达式来匹配连接点。
  • 通知(Advice) 通知是切面在连接点上执行的实际行为。通知可以在连接点的前、后或环绕执行。
    • 前置通知(Before Advice):在方法执行前执行。
    • 后置通知(After Advice):在方法执行后执行。
    • 环绕通知(Around Advice):在方法执行前后都执行,开发者可以完全控制方法的执行流程。
  • 目标对象(Target Object) 目标对象是被切面增强的原始对象。
  • 织入(Weaving) 织入是将切面应用到目标对象的过程,有以下几种方式:
    • 编译期织入:在编译阶段将切面代码织入目标类。
    • 加载期织入:在类加载时使用类加载器动态地将切面织入。
    • 运行期织入:在运行时使用代理模式将切面织入目标对象。

在 Nest 中,请求流程可以换一种角度来看:

中间件

中间件是 Express 中的概念,Nest 的底层默认是 Express,它在请求流程中的位置大致如下:

中间件可以在路由处理程序之前或者之后插入需要执行的任务,Nest做了进一步细分,主要分为全局中间件和局部中间件。

生成中间件文件:

// nest g mi middleware_name
nest g mi person --no-spec

中间件文件大致为:

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

@Injectable()
export class PersonMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
next();
}
}

在模块中注册中间件:

@Module({
controllers: [PersonController],
providers: [PersonService],
})
export class PersonModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
// 对整个 PersonController 生效
consumer.apply(PersonMiddleware).forRoutes(PersonController);

// 对指定路由生效
consumer.apply(PersonMiddleware).forRoutes({
path: '/person',
method: RequestMethod.GET,
});
}
}

在 Nest 中,类中间件不仅可以处理 HTTP 请求和响应,还能够实现依赖注入。这意味着可以在中间件中注入特定的依赖项,并且调用这些依赖项内部的方法:

@Injectable()
export class PersonMiddleware implements NestMiddleware {
@Inject(PersonService)
private personService: PersonService;

use(req: Request, res: Response, next: NextFunction) {
next();
}
}

如果不需要依赖注入的话,也能使用轻量的函数中间件:

export function PersonMiddleware(
req: Request,
res: Response,
next: NextFunction,
) {
next();
}

除了在局部使用,也能直接在全局引用,作为全局中间件使用,对所有 Controller 生效。

全局中间通常在 AppModule 中注册:

import { LoggerMiddleware } from './logger.middleware';

@Module({
imports: [UserModule, PersonModule],
controllers: [AppController],
providers: [AppService],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer.apply(LoggerMiddleware).forRoutes('*');
}
}

守卫

守卫的职责一般很明确,通常用于权限、角色等授权操作。守卫所在的位置与中间件类似,可以对请求进行拦截和过滤,在调用某个 Controller 之前判断权限,返回 true 或者 false 来决定是否放行,分为全局守卫和局部守卫。

创建守卫文件:

nest g gu guard_name -no-spec

要作为一个守卫,必须实现 CanActive 接口中的 canActivate() 方法:

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

@Injectable()
export class PersonGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}

如果进行局部绑定,可以直接在 Controlle r类上添加装饰器 @UseGuards

@Controller('person')
@UseGuards(PersonGuard)
export class PersonController {
// ......
}

全局守卫通常在 AppModule 中注册:

import { APP_GUARD } from '@nestjs/core';
import { PersonGuard } from './person/person.guard';

@Module({
imports: [UserModule, PersonModule],
controllers: [AppController, PersonController],
providers: [
AppService,
PersonService,
{
provide: APP_GUARD,
useClass: PersonGuard,
},
],
})
export class AppModule {}

拦截器

拦截器不同于中间件和守卫,它在路由请求之前和之后都可以进行逻辑处理,能够充分操作 RequestResponse 对象。拦截器通常用于记录请求日志、转换或者格式化相应数据等。

创建拦截器文件:

nest g itc interceptor_name --no-spec

拦截器通过 @Injectable() 来声明,并且需要实现 NestInterceptor 接口的 intercept 方法,接收两个参数:ExecutionContext 上下文对象和CallHandler 处理程序。

ExecutionContext 上下文对象能够访问当前请求的详细信息,包括路由、HTTP方法、请求体以及响应体数据,以下几个场景会用到它:

  • 记录请求和响应日志,用于追踪、监控和调试。
  • 进行身份验证和权限检查。
  • 根据请求头或路由信息来设置缓存策略。
  • 修改或者转换响应数据,比如对响应进行包装、格式化、加密等操作。

CallHandler 实现了 handle 方法,必须在拦截器中调用handle 方法,才能执行路由处理方法:

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

@Injectable()
export class TimeoutInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle()
}
}

拦截器可以设置为作用于某个控制器,或者某个控制器的某个方法,也可以作用于整个应用中

作用于某个控制器上:

@Controller('person')
@UseInterceptors(TimeoutInterceptor)
export class PersonController {......}

作用于某个方法上:

@Get()
@UseInterceptors(TimeoutInterceptor)
findAll() {
return this.personService.findAll();
}

作用于全局:

@Module({
imports: [UserModule, PersonModule],
controllers: [AppController, PersonController],
providers: [
AppService,
PersonService,
{
provide: APP_INTERCEPTOR,
useClass: TimeoutInterceptor,
},
],
})
export class AppModule {}

拦截器的请求和响应流程遵循先进后出的顺序。请求首先通过全局拦截器,然后是控制器级别的拦截器,最后是路由级别的拦截器进行处理。而响应流程则相反,从路由级别的拦截器开始,经过控制器级别的拦截器,最终到达全局拦截器。这样的设计允许在处理请求的任何阶段,包括由管道、控制器或服务抛出的错误。

管道

Pipe 即管道,其主要作用是解析和验证请求数据。

在后端开发中,数据库表的字段类型在创建式就已经被明确定义了,任何不符合预期类型的数据保存操作都会导致错误。为了确保传入的数据满足预期的格式和规范,Nest 会在客户端发起请求的时候,将请求数据传递给管道进行预处理。这些预处理操作包括数据验证、转换或者过滤等,以确保数据的准确性。处理之后的数据会被传递给路由处理程序。

通常情况下,会将管道绑定在方法参数上,这样就能与特定的路由方法关联,这种方式称为参数级别管道。此外,管道也能绑定在全局作用域,使其适用于每个控制器和路由方法。

生成管道文件:

nest g pipe pipe_name --no-spec --flat

Pipe 需要要实现 PipeTransform 接口,并实现 transform 方法:

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 : parseInt(value);
}
}

在 Controller 中使用:

@Delete(':id')
remove(@Param('id', ValidatePipe) id: string) {
return this.personService.remove(+id);
}

如果参数验证不通过,Nest 会直接抛出错误,并结束请求。

Nest 内置了 9 种开箱即用的管道验证器:

  • ValidationPipe
  • ParseIntPipe
  • ParseFloatPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • ParseEnumPipe
  • DefaultValuePipe
  • ParseFilePipe
  • ParseDatePipe
@Delete(':id')
remove(@Param('id', ParseIntPipe) id: string) {
return this.personService.remove(+id);
}

自带的管道验证,错误响应式默认定义的,开发者也能自定义,验证器允许传递下面两个参数:

  • errorHttpStatusCode:验证器失败时抛出的 HTTP 状态码,默认为 400(错误请求)。
  • exceptionFactory:工厂函数,用于接收错误信息并返回相应的错误对象。
 @Delete(':id')
remove(
@Param(
'id',
// new ParseIntPipe({ errorHttpStatusCode: HttpStatus.NOT_ACCEPTABLE }),
new ParseIntPipe({
exceptionFactory: (error: string) => {
throw new HttpException('参数id类型错误', HttpStatus.BAD_REQUEST);
}
})
)
id: string,
) {
return this.personService.remove(+id);
}

作用于 Controller:

@Controller('person')
@UsePipes(ParseIntPipe)
export class PersonController {......}

作用于全局:

@Module({
imports: [UserModule, PersonModule],
controllers: [AppController, PersonController],
providers: [
AppService,
PersonService,
{
provide: APP_PIPE,
useClass: ParseIntPipe,
},
],
})
export class AppModule {}

过滤器

Nest 中最常见的是 HTTP 异常过滤器,通常用于在后端服务发生异常时向客户端报告异常的类型。

目前内置的 HTTP 异常包括:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • HttpVersionNotSupportedException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableEntityException
  • InternalServerErrorException
  • NotImplementedException
  • ImATeapotException
  • MethodNotAllowedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
  • PreconditionFailedException

生成过滤器文件:

nest g filter filter_name --no-spec --flat

实现 ExceptionFilter 接口并实现 catch 方法,就可以拦截异常了。

import { ArgumentsHost, Catch, ExceptionFilter } from '@nestjs/common';

@Catch(HTTPException)
export class MyExceptionFilter<T> implements ExceptionFilter {
catch(exception: T, host: ArgumentsHost) {}
}
  • ArgumentsHost 能够获取不同平台的传输协议上下文,用于访问 requestresponse 对象。
  • @Catch() 装饰器用于声明要拦截的异常类型。
@Delete(':id')
@UseFilters(MyExceptionFilter)
remove(@Param('id', ParseIntPipe) id: string) {
return this.personService.remove(+id);
}

作用于 Controller:

@Controller('person')
@UseFilters(MyExceptionFilter)
export class PersonController {
// ......
}

作用于全局:

@Module({
imports: [UserModule, PersonModule],
controllers: [AppController, PersonController],
providers: [
AppService,
PersonService,
{
provide: APP_FILTER,
useClass: MyExceptionFilter,
},
],
})
export class AppModule {}