AOP架构的应用

前言

后端框架基本都是 MVC(Model View Controller) 架构,在这个架构中,请求会先发送给Controller,由它调度Model层的Service来完成业务逻辑,然后返回对应的View。

在这个流程中,Nest还提供了 AOP(Aspect Oriented Programming)的能力,也就是面向切面编程的能力。切面编程可以让我们在请求链路中加入一些通用逻辑,例如日志记录,权限控制,异常处理等,并且可以保证业务逻辑的纯粹性。切面逻辑还可以复用和动态增删。

Nest一共有五种实现AOP的方式,分别是:MiddlewareGuardPipeInterceptorExceptionFilter

Middleware

Nest底层是express,所以也可以使用中间件,但做了进一步细分,分为全局中间件和路由中间件

全局中间件就是 Express 的那种中间件,在请求之前和之后加入一些处理逻辑,每个请求都会走到这里:

1
2
3
const app = await NestFactory.create(AppModule);
app.use(logger)
await app.listen(3000);

路由中间件则是针对某个路由来说的,范围更小一些:

1
2
3
4
5
6
7
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer){
consumer
.apply(LoggerMiddleware)
.forRoutes('cats');
}
}

Grard

Guard 是路由守卫的意思,可以用于在调用某个 Controller 之前判断权限,返回 true 或者 false 来决定是否放行:

1
2
3
4
5
6
7
8
@Injectable()
export class RolesGuard implements CanActivate {
CanActivate(
context: ExecutionContext
): boolean | Promise<boolean> | Observable<boolean> {
return true;
}
}

Guard 要实现 CanActivate 接口,实现 canActivate 方法,可以从 context 拿到请求的信息,然后做一些权限验证等处理之后返回 true 或者 false。

通过 @Injectable 装饰器加到 IOC 容器中,然后就可以在某个 Controller 启用了:

1
2
3
@Controller('cats')
aUseGuards(RolesGuard)
export class CatsController {}

Controller 本身不需要做啥修改,却透明的加上了权限判断的逻辑,这就是 AOP 架构的好处。

而且,就像 Middleware 支持全局级别和路由级别一样,Guard 也可以全局启用:

1
2
const app = await NestFactory.create(AppModule)
app.useGlobalGuards(new RolesGuard());

Guard 可以抽离路由的访问控制逻辑,但是不能对请求、响应做修改,这种逻辑可以使用 Interceptor:

Interceptor

Interceptor 是拦截器的意思,可以在目标 Controller 方法前后加入一些逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Observable } from 'rxjs';
import { tap } from'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext,next: CallHandler): Observable<any> {
console.log('Before...');
const now = Date.now();

return next
.handle()
.pipe(
tap(() => console.log(`After...${Date.now() - now}ms`))
);
}
}

Interceptor 要实现 NestInterceptor 接口,实现 intercept 方法,调用 next.handle() 就会调用目标 Controller,可以在之前和之后加入一些处理逻辑。

Controller 之前之后的处理逻辑可能是异步的。Nest 里通过 rxjs 来组织它们,所以可以使用 rxjs 的各种 operator。

Interceptor 支持每个路由单独启用,只作用于某个 controller,也同样支持全局启用,作用于全部 controller:

1
2
@UseInterceptors(new LoggingInterceptor())
export class CatsController {}
1
2
const app = await NestFactory.create(ApplicationModule);
app.useGlobalInterceptors(new LoggingInterceptor());

除了路由的权限控制、目标 Controller 之前之后的处理这些都是通用逻辑外,对参数的处理也是一个通用的逻辑,所以 Nest 也抽出了对应的切面,也就是 Pipe:

Pipe

Pipe 是管道的意思,用来对参数做一些检验和转换:

1
2
3
4
5
6
@Injectable()
export class ValidationPipe implements PipeTransform {
transform(value: any,metadata: ArgumentMetadata) {
return value
}
}

Pipe 要实现 PipeTransform 接口,实现 transform 方法,里面可以对传入的参数值 value 做参数验证,比如格式、类型是否正确,不正确就抛出异常。也可以做转换,返回转换后的值。

内置的有 9 个 Pipe,从名字就能看出它们的意思:

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe
  • ParseFilePipe

同样,Pipe 可以只对某个参数生效,某个路由生效,也可以对每个路由都生效:

1
2
3
4
5
6
7
8
9
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
hello(@Param('aaa', ParseIntPipe) aaa: number) {
return "hello";
}
}
1
2
3
4
5
6
7
8
9
10
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}

@Get()
@UsePipes(ValidationPipe)
hello(@Body() createCatDto: CreateCatDto) {
this.cateService.create(createCatDto)
}
}
1
2
3
4
5
6
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
await app.listen(3000);
}
bootstrap();

不管是 Pipe、Guard、Interceptor还是最终调用的 Controller,过程中都可以抛出一些异常,如何对某种异常做出某种响应呢?

这种异常到响应的映射也是一种通用逻辑,Nest 提供了 ExceptionFilter 来支持:

ExceptionFilter

ExceptionFilter 可以对抛出的异常做处理,返回对应的响应:

创建 ExceptionFilter的形式是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Catch(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
})
}
}

首先要实现 ExceptionFilter 接口,实现 catch 方法,就可以拦截异常了,但是要拦截什么异常还需要用 @Catch 装饰器来声明,拦截了异常之后,可以返回对应的响应,给用户更友好的提示。

Nest 内置了很多 http 相关的异常,都是 HttpException 的子类:

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableException
  • InternalServerErrorException
  • NotImplementedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException

同样,ExceptionFilter 也可以选择全局生效或者某个路由生效:

1
2
3
4
5
@Post()
@UseFilters(new HttpExceptionFilter())
async create(@Body() createCatDto: CreateCatDto) {
throw new ForbiddenException();
}
1
2
3
4
5
async function bootstrap(){
const app = await NestFactory.create(AppModule)
app.useGlobalFilters(new HttpExceptionFilter())await app.listen(3000);
}
bootstrap();

几种AOP机制的执行顺序

Comments