你是不是也被这些场景烦过?
- 每个接口都要写“请求日志”,复制粘贴得手酸;
- 每个路由都要做“权限校验”,改一次权限要动十个接口;
- 每个参数都要“手动判空”,写满
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 有几个核心概念(用“日志逻辑”举例子):
- 切面(Aspect):要插入的通用逻辑模块(比如“日志切面”);
- 连接点(Join Point):程序执行中的“插入位置”(比如接口调用前、参数校验后);
- 通知(Advice):切面要执行的具体逻辑(比如“记录请求 URL 和时间”);
- 切点(Pointcut):指定“哪些连接点要插切面”(比如“所有/api 开头的接口”);
- 织入(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()效果
所有请求都会打印日志:
[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()装饰器:ts// 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():ts// 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():ts// 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) }
效果——每个接口都会打印执行时间:
接口耗时:12ms
接口耗时:8ms4. 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更强大):
先安装依赖:
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请求进来时:
- Middleware先接住请求,记录“[2024-05-20 15:00:00] GET /api/users”;
- Guard检查请求头的 JWT token,发现有效,放行;
- Interceptor的
before逻辑记录开始时间(比如startTime = 1716186000000); - Pipe校验
@Query('page')参数,发现是字符串“3”,自动转成数字 3; - Controller的
getUsers()方法执行,从数据库查用户列表; - Interceptor的
after逻辑计算耗时(Date.now() - startTime = 15ms),打印“接口耗时:15ms”; - ExceptionFilter没被触发(因为没异常),响应返回用户列表;
- 最终浏览器拿到
[{ "id": 1, "name": "张三" }]。
一句话总结:
- Middleware管“请求入口”;
- Guard管“能不能进路由”;
- Interceptor管“路由执行前后”;
- Pipe管“参数对不对”;
- ExceptionFilter管“出错了怎么办”。
四、实战:用 AOP 组件搭一个“用户管理接口”
光讲理论太抽象,我们用5 个 AOP 组件搭一个用户管理接口,覆盖“日志、权限、参数校验、耗时统计、异常处理”全流程!
1. 初始化项目与依赖
先建一个 Nest 项目:
nest new aop-demo && cd aop-demo安装需要的依赖(JWT、参数校验):
npm install @nestjs/jwt class-validator class-transformer2. 写 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位
}输出结果:
- Middleware 打印:
[2024-05-20 15:30:00] POST /api/users; - Guard 校验 token 有效,放行;
- Interceptor 记录开始时间;
- ValidationPipe 校验参数,发现两个错误;
- 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"
}输出结果:
- Middleware 打印请求日志;
- Guard 校验 token 有效;
- Interceptor 记录开始时间;
- ValidationPipe 校验通过;
- Controller 执行
createUser,返回用户信息; - Interceptor 打印:
[createUser] 耗时:15ms; - 响应返回:
{ "id": 1, "name": "张三", "email": "zhangsan@example.com" }。
五、AOP 的优势:为什么要“抠”通用逻辑?
做完这个案例,你应该能感受到 AOP 的核心价值:
- 无侵入:业务代码里没有日志、权限、校验的逻辑,只专注于“用户增删改查”;
- 可复用:一个
AuthGuard可以作用于 100 个接口,改一次权限逻辑只动一个文件; - 易维护:要加“接口耗时统计”,只需要写一个
TimeInterceptor,全局启用就行; - 灵活性:可以动态增删切面(比如测试环境关闭权限校验,生产环境打开)。
六、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 的魅力!
参考文档:
- Nest.js 官方 AOP 文档:https://docs.nestjs.com/fundamentals/aop
- class-validator 官方文档:https://github.com/typestack/class-validator
如果有疑问,欢迎在评论区留言,一起交流讨论! 😊
- 本文链接:https://fridolph.top/posts/2025-09-30__nest06-aop
- 版权声明:本博客所有文章除特别声明外,均默认采用 CC BY-NC-SA 许可协议。