【NestJS】06 AOP:用“切面思维”把通用逻辑从业务代码里“刨”出来

4376 字
22 分钟
【NestJS】06 AOP:用“切面思维”把通用逻辑从业务代码里“刨”出来

你是不是也被这些场景烦过?

  • 每个接口都要写“请求日志”,复制粘贴得手酸;
  • 每个路由都要做“权限校验”,改一次权限要动十个接口;
  • 每个参数都要“手动判空”,写满if (!params.id) return 400
  • 每个异常都要“单独处理”,相同的错误提示复制五遍……

这些通用逻辑像“狗皮膏药”一样贴在业务代码里,不仅让代码变丑,还难维护——改一处逻辑要动十个文件,堪称“牵一发而动全身”。

这时候,AOP(面向切面编程) 就得登场了!它能把这些通用逻辑“切”成独立的模块,像“插插件”一样贴到业务代码上,既不侵入业务,又能复用。

而 Nest.js 作为 Node.js 生态中最“像 Spring”的框架,把 AOP 玩出了花——它提供了5 种 AOP 组件(Middleware、Guard、Interceptor、Pipe、ExceptionFilter),覆盖了从请求入口到响应出口的全链路。今天我们就来手把手教你:用 Nest.js 的 AOP 搞定所有通用逻辑!

一、先搞懂 AOP:到底什么是“面向切面编程”?#

在讲 Nest 的实战之前,得先把 AOP 的核心概念掰碎了讲——不然你可能会问:“切面?到底切的是啥?”

AOP 的本质是**“横向扩展”**:传统的 OOP(面向对象)是“纵向继承”,比如UserService继承BaseService;而 AOP 是“横向切一刀”,在多个对象的相同位置插入通用逻辑(比如所有接口的日志、所有参数的校验)。

AOP 有几个核心概念(用“日志逻辑”举例子):

  1. 切面(Aspect):要插入的通用逻辑模块(比如“日志切面”);
  2. 连接点(Join Point):程序执行中的“插入位置”(比如接口调用前、参数校验后);
  3. 通知(Advice):切面要执行的具体逻辑(比如“记录请求 URL 和时间”);
  4. 切点(Pointcut):指定“哪些连接点要插切面”(比如“所有/api 开头的接口”);
  5. 织入(Weaving):把切面逻辑“缝”到业务代码里的过程(Nest 帮你做了)。

简单来说:AOP 就是“在指定位置,插入指定的通用逻辑”——而 Nest 的 5 种组件,就是帮你实现这个目标的“工具包”。

二、Nest.js 的 5 种 AOP 组件:从请求到响应全链路覆盖#

Nest 的 AOP 组件覆盖了请求处理的全流程,我们从“请求进入”到“响应返回”挨个讲:

1. Middleware(中间件):最外层的“全局拦截”#

Middleware 是Express 的遗产,但 Nest 做了增强——它能在所有请求进入路由前插入逻辑,比如全局日志、跨域处理。

场景示例:全局请求日志#

我们写一个 Middleware,记录每个请求的 URL、方法和时间:

src/log.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'
@Injectable()
export class LogMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[${new Date().toLocaleString()}] ${req.method} ${req.url}`)
next() // 必须调用next(),否则请求会被拦截
}
}

使用方式:全局启用#

main.ts里用app.use()注册:

src/main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { LogMiddleware } from './log.middleware'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.use(new LogMiddleware()) // 全局启用日志中间件
await app.listen(3000)
}
bootstrap()

效果#

所有请求都会打印日志:

Terminal window
[2024-05-20 14:30:00] GET /api/users
[2024-05-20 14:30:01] POST /api/login

进阶:路由级 Middleware#

如果只想让 Middleware 作用于特定路由(比如/api/admin开头的接口),可以在AppModule里配置:

src/app.module.ts
import { Module, NestModule, MiddlewareConsumer } from '@nestjs/common'
import { LogMiddleware } from './log.middleware'
import { AppController } from './app.controller'
@Module({
controllers: [AppController],
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(LogMiddleware) // 应用中间件
.forRoutes('api/admin*') // 作用于所有/api/admin开头的路由
}
}

2. Guard(守卫):路由的“权限守门员”#

Guard 是路由级的权限校验工具——它能在进入 Controller 前判断“这个请求有没有权限访问该路由”,返回true(放行)或false(拦截)。

场景示例:JWT 权限校验#

我们写一个 Guard,校验请求头中的 JWT token:

src/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Request } from 'express'
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {} // 注入JWT服务
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest<Request>()
const token = req.headers.authorization?.split(' ')[1] // 从请求头拿token
if (!token) return false // 没有token,拦截
try {
this.jwtService.verify(token) // 校验token有效性
return true // 校验通过,放行
} catch (e) {
return false // 校验失败,拦截
}
}
}

使用方式:局部/全局启用

  • 局部启用:在 Controller 或路由上用@UseGuards()装饰器:

    src/app.controller.ts
    import { Controller, Get, UseGuards } from '@nestjs/common'
    import { AuthGuard } from './auth.guard'
    @Controller('api')
    export class AppController {
    @Get('users')
    @UseGuards(AuthGuard) // 该路由需要权限校验
    getUsers() {
    return ['user1', 'user2']
    }
    }
  • 全局启用:在main.ts里用useGlobalGuards(),或在AppModule里注册为 provider(支持依赖注入):

src/app.module.ts
import { Module } from '@nestjs/common'
import { APP_GUARD } from '@nestjs/core'
import { AuthGuard } from './auth.guard'
@Module({
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard, // 全局启用AuthGuard
},
],
})
export class AppModule {}
  • 没有 token 的请求:返回 403 Forbidden;
  • 无效 token 的请求:返回 403 Forbidden;
  • 有效 token 的请求:正常返回数据。

3. Interceptor(拦截器):接口的“前后置处理器”#

Interceptor 是最灵活的 AOP 组件——它能在Controller 方法执行前后插入逻辑,比如接口耗时统计、响应格式化、日志增强。

场景示例:接口耗时统计#

我们写一个 Interceptor,计算接口的执行时间:

src/time.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { tap } from 'rxjs/operators'
@Injectable()
export class TimeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now() // 记录开始时间
return next.handle().pipe(
tap(() => {
const endTime = Date.now()
console.log(`接口耗时:${endTime - startTime}ms`) // 打印耗时
})
)
}
}

关键点说明 next.handle():调用后续的拦截器或 Controller 方法(相当于“执行业务逻辑”); tap():rxjs 的操作符,用于在 Observable 发出值后执行副作用(比如打印耗时)。

使用方式:局部 / 全局启用#

  • 局部启用:在 Controller 或路由上用@UseInterceptors()

    src/app.controller.ts
    import { Controller, Get, UseInterceptors } from '@nestjs/common'
    import { TimeInterceptor } from './time.interceptor'
    @Controller('api')
    @UseInterceptors(TimeInterceptor) // 整个Controller都用耗时统计
    export class AppController {
    @Get('users')
    getUsers() {
    return ['user1', 'user2']
    }
    }
  • 全局启用:在main.ts里用useGlobalInterceptors()

    src/main.ts
    import { NestFactory } from '@nestjs/core'
    import { AppModule } from './app.module'
    import { TimeInterceptor } from './time.interceptor'
    async function bootstrap() {
    const app = await NestFactory.create(AppModule)
    app.useGlobalInterceptors(new TimeInterceptor()) // 全局启用耗时统计
    await app.listen(3000)
    }

效果——每个接口都会打印执行时间:

Terminal window
接口耗时:12ms
接口耗时:8ms

4. Pipe(管道):参数的“自动校验器”#

Pipe 是参数处理的“神器”——它能在Controller 方法执行前对参数做校验、转换,比如“把字符串转数字”“校验参数是否必填”“验证邮箱格式”。

场景示例:参数校验与转换#

我们写一个 Pipe,校验“num 参数必须是数字”,并自动乘 10:

src/validate.pipe.ts
import { PipeTransform, Injectable, BadRequestException } from '@nestjs/common'
import { ArgumentMetadata } from '@nestjs/common/interfaces'
@Injectable()
export class ValidatePipe implements PipeTransform {
transform(value: any, metadata: ArgumentMetadata) {
// metadata.data:参数名(比如@Query('num')中的'num')
if (isNaN(Number(value))) {
throw new BadRequestException(`参数${metadata.data}必须是数字`)
}
return Number(value) * 10 // 转换参数(乘10)
}
}

使用方式:参数级/局部/全局启用

  • 参数级启用:在参数上直接用@UsePipes()或 Pipe 类:
src/app.controller.ts
import { Controller, Get, Query, UsePipes } from '@nestjs/common'
import { ValidatePipe } from './validate.pipe'
@Controller('api')
export class AppController {
@Get('calc')
// 对num参数应用ValidatePipe
calc(@Query('num', ValidatePipe) num: number) {
return num + 1 // 接收的num已经是乘10后的值
}
}
  • 全局启用:用 Nest 内置的ValidationPipe(结合class-validator更强大):
    先安装依赖:
Terminal window
npm install class-validator class-transformer

然后在main.ts里启用:

src/main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ValidationPipe } from '@nestjs/common'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
// 全局启用ValidationPipe(自动校验DTO)
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 过滤掉DTO中没有的属性
forbidNonWhitelisted: true, // 有非DTO属性时抛异常
transform: true, // 自动转换参数类型
})
)
await app.listen(3000)
}

效果:

  • 请求/api/calc?num=abc:返回 400,提示“参数 num 必须是数字”;
  • 请求/api/calc?num=5:返回5*10+1=51
  • ValidationPipe时,DTO 中的@IsString()@IsNumber()等装饰器会自动校验参数。

5. ExceptionFilter(异常过滤器):异常的“统一处理器”#

ExceptionFilter 是异常的“最终守门员”——它能捕获所有未处理的异常,并返回统一格式的响应(比如{ code: 400, message: '参数错误', data: null })。

场景示例:自定义异常响应#

我们写一个 Filter,统一处理BadRequestException(参数错误):

src/bad-request.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
BadRequestException,
} from '@nestjs/common'
import { Response } from 'express'
@Catch(BadRequestException) // 捕获BadRequestException
export class BadRequestFilter implements ExceptionFilter {
catch(exception: BadRequestException, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const res = ctx.getResponse<Response>()
const status = exception.getStatus() // 异常的HTTP状态码(400)
res.status(status).json({
code: status,
message: `参数错误:${exception.message}`, // 自定义错误信息
data: null,
})
}
}

使用方式:局部/全局启用

  • 局部启用:在 Controller 或路由上用@UseFilters()
src/app.controller.ts
import { Controller, Get, UseFilters } from '@nestjs/common'
import { BadRequestFilter } from './bad-request.filter'
@Controller('api')
@UseFilters(BadRequestFilter) // 该Controller的异常用自定义Filter处理
export class AppController {
@Get('calc')
calc(@Query('num') num: number) {
if (isNaN(num)) throw new BadRequestException('num必须是数字')
return num + 1
}
}
  • 全局启用:在main.ts里用useGlobalFilters()
src/main.ts
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { BadRequestFilter } from './bad-request.filter'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalFilters(new BadRequestFilter()) // 全局启用自定义异常Filter
await app.listen(3000)
}

效果 —— 请求/api/calc?num=abc:返回:

{
"code": 400,
"message": "参数错误:num必须是数字",
"data": null
}

三、Nest.js AOP 组件的调用顺序:从请求到响应全流程#

光说顺序不够直观,我们用**“请求生命周期”**再理一遍:

当一个GET /api/users请求进来时:

  1. Middleware先接住请求,记录“[2024-05-20 15:00:00] GET /api/users”;
  2. Guard检查请求头的 JWT token,发现有效,放行;
  3. Interceptorbefore逻辑记录开始时间(比如startTime = 1716186000000);
  4. Pipe校验@Query('page')参数,发现是字符串“3”,自动转成数字 3;
  5. ControllergetUsers()方法执行,从数据库查用户列表;
  6. Interceptorafter逻辑计算耗时(Date.now() - startTime = 15ms),打印“接口耗时:15ms”;
  7. ExceptionFilter没被触发(因为没异常),响应返回用户列表;
  8. 最终浏览器拿到[{ "id": 1, "name": "张三" }]

一句话总结

  • Middleware管“请求入口”;
  • Guard管“能不能进路由”;
  • Interceptor管“路由执行前后”;
  • Pipe管“参数对不对”;
  • ExceptionFilter管“出错了怎么办”。

四、实战:用 AOP 组件搭一个“用户管理接口”#

光讲理论太抽象,我们用5 个 AOP 组件搭一个用户管理接口,覆盖“日志、权限、参数校验、耗时统计、异常处理”全流程!

1. 初始化项目与依赖#

先建一个 Nest 项目:

Terminal window
nest new aop-demo && cd aop-demo

安装需要的依赖(JWT、参数校验):

Terminal window
npm install @nestjs/jwt class-validator class-transformer

2. 写 5 个 AOP 组件(按调用顺序)#

(1)Middleware:请求日志#

src/log.middleware.ts
import { Injectable, NestMiddleware } from '@nestjs/common'
import { Request, Response, NextFunction } from 'express'
@Injectable()
export class LogMiddleware implements NestMiddleware {
use(req: Request, res: Response, next: NextFunction) {
console.log(`[${new Date().toLocaleString()}] ${req.method} ${req.url}`)
next()
}
}

作用:记录所有请求的时间、方法、URL。

(2)Guard:JWT 权限校验#

src/auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { Request } from 'express'
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
canActivate(context: ExecutionContext): boolean {
const req = context.switchToHttp().getRequest<Request>()
const token = req.headers.authorization?.split(' ')[1]
if (!token) throw new Error('缺少token') // 会被ExceptionFilter捕获
try {
this.jwtService.verify(token, { secret: 'your-secret-key' }) // 替换成你的JWT密钥
return true
} catch (e) {
throw new Error('无效的token')
}
}
}

作用:校验请求是否携带有效 JWT token。

(3)Interceptor:接口耗时统计#

src/time.interceptor.ts
import {
Injectable,
NestInterceptor,
ExecutionContext,
CallHandler,
} from '@nestjs/common'
import { Observable } from 'rxjs'
import { tap } from 'rxjs/operators'
@Injectable()
export class TimeInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const startTime = Date.now()
return next.handle().pipe(
tap(() => {
console.log(
`[${context.getHandler().name}] 耗时:${Date.now() - startTime}ms`
)
})
)
}
}

作用:记录每个接口的执行时间(比如[getUsers] 耗时:12ms)。

(4)Pipe:用户参数校验(结合 DTO)#

先定义一个用户参数 DTO(用class-validator做注解校验):

src/dto/create-user.dto.ts
import { IsString, IsEmail, MinLength } from 'class-validator'
export class CreateUserDto {
@IsString({ message: '姓名必须是字符串' }) // 校验类型
name: string
@IsEmail({}, { message: '邮箱格式错误' }) // 校验邮箱格式
email: string
@MinLength(6, { message: '密码至少6位' }) // 校验长度
password: string
}

然后写 Pipe(直接用 Nest 内置的ValidationPipe,更强大):

// src/main.ts(全局启用)
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import { ValidationPipe } from '@nestjs/common'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
app.useGlobalPipes(
new ValidationPipe({
whitelist: true, // 过滤DTO中没有的属性(比如多余的"age"参数)
forbidNonWhitelisted: true, // 有多余属性时抛异常
transform: true, // 自动转换参数类型(比如字符串转数字)
})
)
await app.listen(3000)
}
bootstrap()

作用:自动校验CreateUserDto的参数,不用手动写if (!name) return 400

(5)ExceptionFilter:统一异常响应#

src/global-exception.filter.ts
import {
ExceptionFilter,
Catch,
ArgumentsHost,
HttpException,
} from '@nestjs/common'
import { Response } from 'express'
@Catch() // 捕获所有异常(包括HttpException和自定义异常)
export class GlobalExceptionFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
const ctx = host.switchToHttp()
const res = ctx.getResponse<Response>()
const req = ctx.getRequest<Request>()
// 判断异常类型
const status =
exception instanceof HttpException ? exception.getStatus() : 500
const message =
exception instanceof Error ? exception.message : '服务器内部错误'
res.status(status).json({
code: status,
message,
path: req.url,
timestamp: new Date().toLocaleString(),
})
}
}

全局启用

src/main.ts
app.useGlobalFilters(new GlobalExceptionFilter())

作用:所有异常都会返回统一格式的响应,比如:

{
"code": 400,
"message": "邮箱格式错误",
"path": "/api/users",
"timestamp": "2024-05-20 15:30:00"
}

3. 写业务代码:用户管理 Controller#

现在,业务代码里没有任何通用逻辑,只需要专注于“增删改查”:

src/user.controller.ts
import {
Controller,
Post,
Get,
Body,
UseGuards,
UseInterceptors,
} from '@nestjs/common'
import { CreateUserDto } from './dto/create-user.dto'
import { AuthGuard } from './auth.guard'
import { TimeInterceptor } from './time.interceptor'
@Controller('api/users')
@UseGuards(AuthGuard) // 该Controller的所有接口都需要权限校验
@UseInterceptors(TimeInterceptor) // 该Controller的所有接口都统计耗时
export class UserController {
@Post()
createUser(@Body() dto: CreateUserDto) {
// Body参数自动用ValidationPipe校验
// 业务逻辑:插入数据库
return { id: 1, name: dto.name, email: dto.email }
}
@Get()
getUsers() {
// 业务逻辑:查询数据库
return [
{ id: 1, name: '张三' },
{ id: 2, name: '李四' },
]
}
}

4. 测试:看 AOP 组件如何协同工作#

用 Postman 调用POST /api/users,传入错误参数

{
"name": "张三",
"email": "zhangsan", // 邮箱格式错误
"password": "123" // 密码不足6位
}

输出结果

  1. Middleware 打印:[2024-05-20 15:30:00] POST /api/users
  2. Guard 校验 token 有效,放行;
  3. Interceptor 记录开始时间;
  4. ValidationPipe 校验参数,发现两个错误;
  5. GlobalExceptionFilter 捕获异常,返回:
{
"code": 400,
"message": "邮箱格式错误; 密码至少6位",
"path": "/api/users",
"timestamp": "2024-05-20 15:30:00"
}

Interceptor 打印:[createUser] 耗时:8ms(虽然参数错误,但还是记录了耗时)。

再调用正确参数

{
"name": "张三",
"email": "zhangsan@example.com",
"password": "123456"
}

输出结果

  1. Middleware 打印请求日志;
  2. Guard 校验 token 有效;
  3. Interceptor 记录开始时间;
  4. ValidationPipe 校验通过;
  5. Controller 执行createUser,返回用户信息;
  6. Interceptor 打印:[createUser] 耗时:15ms
  7. 响应返回:{ "id": 1, "name": "张三", "email": "zhangsan@example.com" }

五、AOP 的优势:为什么要“抠”通用逻辑?#

做完这个案例,你应该能感受到 AOP 的核心价值

  1. 无侵入:业务代码里没有日志、权限、校验的逻辑,只专注于“用户增删改查”;
  2. 可复用:一个AuthGuard可以作用于 100 个接口,改一次权限逻辑只动一个文件;
  3. 易维护:要加“接口耗时统计”,只需要写一个TimeInterceptor,全局启用就行;
  4. 灵活性:可以动态增删切面(比如测试环境关闭权限校验,生产环境打开)。

六、AOP 的“避坑指南”:不要滥用#

AOP 不是“银弹”,以下场景不适合用 AOP

  • 小项目:只有 3 个接口,用 AOP 反而增加复杂度;
  • 业务强相关的逻辑:比如“用户注册后发送短信”,属于业务逻辑,不能放切面里;
  • 性能敏感的逻辑:比如高频接口的“毫秒级耗时统计”,AOP 的微小开销可能被放大(但 Nest 的 AOP 性能很好,一般不用担心)。

七、最后:用“切面思维”重新审视代码#

AOP 的本质不是“用什么组件”,而是**“将通用逻辑与业务逻辑分离”**的思维方式——当你写代码时,先问自己:

  • 这个逻辑是“只有当前接口需要”还是“所有接口都需要”?
  • 这个逻辑是“业务核心”还是“辅助功能”?

如果是通用、辅助的逻辑,就把它“切”成切面;如果是业务核心逻辑,就留在 Controller/Service 里。这样写出来的代码,会像“剥洋葱”一样层次清晰:

  • 最外层:Middleware(请求日志);
  • 第二层:Guard(权限校验);
  • 第三层:Interceptor(耗时统计);
  • 第四层:Pipe(参数校验);
  • 最内层:业务逻辑(用户增删改查)。

总结#

Nest.js 的 AOP 组件,是帮你“把通用逻辑从业务代码里抠出来”的利器——它用 5 种组件覆盖了请求的全链路,让你不用再写重复的日志、权限、校验代码。

现在,你可以试着把项目里的通用逻辑“切”成切面,比如:

  • 把“操作日志”放 Middleware 里;
  • 把“接口限流”放 Guard 里;
  • 把“响应格式化”放 Interceptor 里;
  • 把“参数校验”放 Pipe 里;
  • 把“异常统一处理”放 ExceptionFilter 里。

最后送你一句话:“好的代码,是业务逻辑里没有通用逻辑”——这就是 AOP 的魅力!


参考文档

如果有疑问,欢迎在评论区留言,一起交流讨论! 😊

支持与分享

如果这篇文章对你有帮助,欢迎分享给更多人或赞助支持!

【NestJS】06 AOP:用“切面思维”把通用逻辑从业务代码里“刨”出来
https://blog.fridolph.top/posts/2025-09-30__nest06-aop/
作者
Fridolph
发布于
2025-09-30
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
Fridolph
热爱 Coding、音乐和羽毛球的 90 后全栈工程师
公告
欢迎访问我的小站 ^_^ 我是昇哥,热爱Coding,喜爱音乐、羽毛球和摄影的 90后全栈工程师
分类
标签

文章目录