nestjs 全栈进阶--aop面向切面编程

本文介绍了Nest.js框架中如何利用AOP(面向切面编程)实现日志记录、事务管理等横切关注点,通过中间件、拦截器、守卫和异常过滤器来管理和复用代码,提升开发效率和代码结构的清晰度。
摘要由CSDN通过智能技术生成
视频教程

13_nestjs-aop1_哔哩哔哩_bilibili

1. 概念

面向切面编程(Aspect-Oriented Programming,简称AOP)是一种编程范式,其核心理念在于将交叉-cutting concerns(横切关注点)从主业务逻辑中抽离出来,以便于管理和重用。这些横切关注点通常包括日志记录、事务管理、安全检查、性能监控等,它们贯穿于整个系统,可能会影响到多个类或多个方法。

在传统的面向对象编程(Object-Oriented Programming,简称OOP)中,尽管我们可以很好地组织和封装对象的内部状态和行为,但对于那些与业务逻辑并非紧密关联,却需要在多个地方重复调用或应用的功能,OOP无法直接有效地解决其分散和冗余的问题。AOP就是为了解决这一问题而提出的。

2. aop的关键概念

  1. 切面(Aspect): 切面是AOP的基本单元,它包含了横切关注点的全部实现。一个切面可以包含多个通知(Advice),并定义了何时何地执行这些通知。
  2. 通知(Advice): 通知是切面的具体实现,描述了在程序执行过程中何时以及如何插入额外的代码。常见的通知类型有前置通知(Before)、后置通知(After Returning/Throwing)、环绕通知(Around)等。
  3. 连接点(Join Point): 连接点是程序执行过程中的某个特定位置,比如方法调用、异常抛出等时刻。通知可以在这些连接点上织入(Weaving)到程序中。
  4. 切入点(Pointcut): 切入点是匹配连接点的一组规则,它定义了哪些连接点会被通知所织入。例如,我们可以定义一个切入点来表示所有以特定名字开头的方法。
  5. 织入(Weaving): 织入是指将切面应用到目标对象来创建新的代理对象的过程。织入可以在编译时、加载时或运行时进行。

3. 应用场景

  • 日志记录:在每个方法调用前后自动记录日志,无需在每个业务方法中手动编写日志代码。
  • 事务管理:跨多个业务方法进行事务控制,保证数据一致性,无需在每个涉及数据库操作的方法内显式开启、提交或回滚事务。
  • 性能监控:在方法执行前后统计耗时,方便分析系统瓶颈。
  • 权限验证:在访问敏感资源前进行权限检查,而不是在每个资源访问方法里嵌入验证代码。

4. 图示

一个请求过来,可能会经过 Controller(控制器)、Service(服务)、Repository(数据库访问) 的逻辑, 如果想在这个调用链路里加入一些通用逻辑该怎么加?比如日志记录、权限控制、异常处理等。

最直接的就是修改Controller 层代码,但是不优雅,所以我们应该考虑在Controller 之前或之后加入一个执行通用逻辑的阶段,比如:

这样的横向扩展点就叫做切面,这种透明的加入一些切面逻辑的编程方式就叫做 AOP (面向切面编程)。AOP 的好处是可以把一些通用逻辑分离到切面中,保持业务逻辑的纯粹性,这样切面逻辑可以复用,还可以动态的增删。

5. nest中实现AOP的主要方式

  1. 中间件 (middleware)
  • 中间件是 Express 里的概念,Nest 的底层是 Express,所以自然也可以使用中间件,但是做了进一步的细分,分为了全局中间件和路由中间件
  1. 拦截器(Interceptor)
  • 拦截器是AOP中的一个重要组件,它能够捕获并处理到特定服务或控制器方法的HTTP请求和响应。拦截器可以在请求到达实际处理方法前执行预处理逻辑(前置逻辑),并在响应发送给客户端之前执行后置处理逻辑。这种机制可用于实现诸如日志记录、权限验证、性能监控、统一错误处理等横切关注点。
  1. 守卫(Gurad)
  • 守卫(Guard)也是一种特殊的拦截器,主要用于控制路由访问权限。守卫可以根据特定条件决定是否允许请求继续执行到后续的处理器。这种特性在处理用户认证、授权等与业务逻辑分离的安全相关问题时非常有用。
  1. 异常过滤器(ExceptionFilter)
  • 异常过滤器专门用来处理全局或局部范围内的未捕获异常,它可以捕获并转换异常,甚至完全替代原始异常信息,以便向客户端提供更友好的错误响应。这确保了系统中有关异常处理的横切关注点得以集中管理。
  1. 管道(pipe)
  • 虽然管道主要负责参数转换和验证,但也可以看作是对AOP的一种变通实现,因为它能在执行链路中插入一个环节来处理所有传递给控制器方法的参数,从而实现对输入数据的统一处理和校验,这也是一种横切关注点的体现。

6. 代码体验

nest new aop -p pnpm
pnpm start:dev
6.1. 中间件
  1. 全局中间件

现在我们可以看到 当我们请求时,我们在当前请求前后动态的增加了一些可复用的逻辑。

  1. 路由中间件
nest g middleware logger --no-spec --flat

这是一个生成中间件的命令,上图是他生成的代码

然后我们在多加几个请求,并修改下他生成的中间件代码

然后在 AppModule 里启用,在 configure 方法里配置 LogMiddleware 在哪些路由生效。

可以看到,只有 aaa 的路由,中间件生效了

后面我们还会单独讲中间件,现在我们先体验一下

6.2. 守卫(Guard)

守卫是一个用 @Injectable() 装饰器注释的类,它实现了 CanActivate 接口。

守卫有单一的责任。它们根据运行时存在的某些条件(如权限、角色、ACL 等)确定给定请求是否将由路由处理程序处理。这通常称为授权。授权(及其通常与之合作的身份验证)通常由传统 Express 应用中的 中间件 处理。中间件是身份验证的不错选择,因为诸如令牌验证和将属性附加到 request 对象之类的事情与特定路由上下文(及其元数据)没有紧密联系。

tips: 守卫在每个中间件之后执行,但在任何拦截器或管道之前执行。

nest g guard login --no-spec --flat

生成的代码, 守卫要求实现CanActivate 给定参数context执行上下文 要求返回布尔值

我们加个打印语句,然后返回 false,之后在 AppController 里启用

在浏览器访问,因为我们返回的false,代表没有权限,所以返回了403

可以看到他是在中间件执行后执行的

  1. 全局守卫

使用方式1:

使用方式2:

注释掉我们刚刚在main.ts中添加的代码,然后

这两种全局守卫的区别:

第一种(main中那种):是手动 new 的 Guard 实例,不在 IoC 容器里

第二种:用 provider 的方式声明的 Guard 是在 IoC 容器里的,可以注入别的 provider,比如

6.3. 拦截器(Interceptor)

拦截器是用 @Injectable() 装饰器注释并实现 NestInterceptor 接口的类。

拦截器具有一组有用的功能,这些功能的灵感来自 面向方面编程 (AOP) 技术。它们可以:

  • 在方法执行之前/之后绑定额外的逻辑
  • 转换函数返回的结果
  • 转换函数抛出的异常
  • 扩展基本功能行为
  • 根据特定条件完全覆盖函数(例如,出于缓存目的)
nest g interceptor duration --no-spec --flat

生成的代码

我们改造一下,并屏蔽全局守卫,且修改守卫代码

我们再去启用这个拦截器

Interceptor 除了支持每个路由单独启用,也可以在 controller 级别启动,作用于下面的全部 handler:

也同样支持全局启用

Interceptor 和 Middleware的区别:

interceptor 可以拿到调用的 controller 和 handler:

以下是Nest.js中的中间件、守卫、拦截器对比的总结:

类型

功能与用途

工作流程与接口

示例场景

中间件

- 对HTTP请求和响应进行预处理和后处理

- 可修改请求或响应对象

- 可短路请求处理链

- 应用于全局、模块或路由层级

- 全局错误处理

- 日志记录

- 身份验证(基础层面)

- CORS配置

守卫(Guards)

- 决定是否允许执行后续的控制器方法

- 主要负责权限验证和资源访问保护

- 实现CanActivate等接口

- 用户登录验证

- 权限检查

- IP过滤

拦截器(Interceptors)

- 在调用服务或控制器方法前后插入额外行为

- 可修改请求参数、处理结果或响应本身

- 实现Interceptor接口

- 统一错误处理与响应格式化

- 添加请求/响应头

- 缓存策略

- 请求日志

6.4. 管道(pipe)

管道是用 @Injectable() 装饰器注释的类,它实现了 PipeTransform 接口。

管道有两个典型的用例:

转型:将输入数据转换为所需的形式(例如,从字符串到整数)

验证:评估输入数据,如果有效,只需将其原样传递;否则抛出异常

在这两种情况下,管道都在由 控制器路由处理器 处理的 arguments 上运行。Nest 在调用方法之前插入一个管道,管道接收指定给该方法的参数并对它们进行操作。任何转换或验证操作都会在此时发生,之后会使用任何(可能)转换的参数调用路由处理程序。

nest g pipe validate --no-spec --flat

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

修改下他生成的代码

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

然后应用这个 pipe

同样,Pipe 除了对某个参数生效,或者整个 Controller 都生效(注意,只对有参数的路由生效)

或者全局生效(注意,只对有参数的路由生效)

Nest 内置Pipe

  • ValidationPipe
  • ParseIntPipe
  • ParseBoolPipe
  • ParseArrayPipe
  • ParseUUIDPipe
  • DefaultValuePipe
  • ParseEnumPipe
  • ParseFloatPipe
  • ParseFilePipe
6.5. 异常过滤器(ExceptionFilter)

Nest 带有一个内置的异常层,负责处理应用中所有未处理的异常。当你的应用代码未处理异常时,该层会捕获该异常,然后自动发送适当的用户友好响应。

开箱即用,此操作由内置的全局异常过滤器执行,该过滤器处理 HttpException 类型(及其子类)的异常。当异常无法识别时(既不是 HttpException 也不是继承自 HttpException 的类),内置异常过滤器会生成以下默认 JSON 响应:

{
  "statusCode": 500,
  "message": "Internal server error"
}

还记得我们刚刚返回400错误的响应吗?他就是 Exception Filter 做的

nest g filter test --no-spec --flat

我们来修改一下,我们去实现 ExceptionFilter 接口,实现 catch 方法,这样就可以拦截异常了,拦截什么异常用 @Catch 装饰器来声明,然后在 catch 方法返回对应的响应,给用户更友好的提示。

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
    })
  }
}

应用一下

Nest 通过这样的方式实现了异常到响应的对应关系,代码里只要抛出不同的异常,就会返回对应的响应.

同样,ExceptionFilter 除了对单个路由生效也可以对整个整个 Controller 都生效

或者,全局生效

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

  • BadRequestException
  • UnauthorizedException
  • NotFoundException
  • ForbiddenException
  • NotAcceptableException
  • RequestTimeoutException
  • ConflictException
  • GoneException
  • PayloadTooLargeException
  • UnsupportedMediaTypeException
  • UnprocessableException
  • InternalServerErrorException
  • NotImplementedException
  • BadGatewayException
  • ServiceUnavailableException
  • GatewayTimeoutException
6.6. 执行顺序

Middleware 是 Express 的概念,在最外层,到了某个路由之后,会先调用 Guard,Guard 用于判断路由有没有权限访问,然后会调用 Interceptor,对 Contoller 前后扩展一些逻辑,在到达目标 Controller 之前,还会调用 Pipe 来对参数做检验和转换。所有的 HttpException 的异常都会被 ExceptionFilter 处理,返回不同的响应。

  • 29
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Spring Boot实现AOP面向切面编程的具体方法如下: 1. 首先,你需要在项目的pom.xml文件中添加spring-boot-starter-aop依赖。可以参考以下代码: ``` <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> ``` 2. 然后,你需要编写一个用于拦截的bean。这个bean将包含你想要在目标方法执行前后执行的逻辑。你可以使用注解或者编程方式来定义切面。例如,你可以使用@Aspect注解来定义一个切面,然后在切面的方法上使用@Before、@After等注解来定义具体的拦截行为。 3. 接下来,你需要将切面应用到目标对象上,创建代理对象。这个过程称为织入(Weaving)。在Spring Boot中,你可以使用@EnableAspectJAutoProxy注解来启用自动代理,它会根据切面定义自动创建代理对象。 总而言之,Spring Boot实现AOP面向切面编程的具体方法包括:添加依赖、编写用于拦截的bean,以及启用自动代理。这样就能实现在目标方法执行前后执行特定逻辑的效果了。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [SpringBoot整合aop面向切面编程过程解析](https://download.csdn.net/download/weixin_38689551/12743012)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [SpringBoot实现AOP面向切面编程](https://blog.csdn.net/weixin_52536274/article/details/130375560)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值