AOP 编程:守卫、拦截器、装饰器如何实现横切关注点?
面向切面编程(Aspect-Oriented Programming,AOP)是一种编程范式,旨在提高模块化程度,通过将横切关注点(cross-cutting concerns)与业务逻辑分离来增加代码的可读性和可维护性。在 NestJS 中,守卫(Guards)、拦截器(Interceptors)和自定义装饰器是实现 AOP 的核心机制。本文将深入探讨这些组件如何协同工作,实现日志、权限、缓存、性能监控等横切关注点的统一注入。
1. AOP 基础概念
1.1 什么是横切关注点?
横切关注点是指那些跨越多个模块或组件的功能需求,它们通常不是核心业务逻辑的一部分,但却是系统运行所必需的:
typescript
// 传统方式:横切关注点与业务逻辑混合
@Controller('users')
export class UserController {
@Get(':id')
async findOne(@Param('id') id: string, @Request() req: Request) {
// 1. 日志记录(横切关注点)
console.log(`[${new Date().toISOString()}] GET /users/${id} - IP: ${req.ip}`);
// 2. 权限检查(横切关注点)
const authHeader = req.headers.authorization;
if (!authHeader || !this.validateToken(authHeader)) {
throw new UnauthorizedException('Invalid token');
}
// 3. 核心业务逻辑
const user = await this.userService.findById(id);
if (!user) {
throw new NotFoundException('User not found');
}
// 4. 性能监控(横切关注点)
console.log(`Request processed in ${Date.now() - startTime}ms`);
return user;
}
}1.2 AOP 的优势
使用 AOP 可以将横切关注点与业务逻辑分离:
typescript
// AOP 方式:业务逻辑与横切关注点分离
@UseGuards(AuthGuard)
@UseInterceptors(LoggingInterceptor, TimingInterceptor)
@Controller('users')
export class UserController {
@Get(':id')
async findOne(@Param('id') id: string) {
// 纯粹的业务逻辑
const user = await this.userService.findById(id);
if (!user) {
throw new NotFoundException('User not found');
}
return user;
}
}2. 守卫实现安全关注点
2.1 认证守卫
typescript
// 认证守卫实现
@Injectable()
export class AuthGuard implements CanActivate {
constructor(
private readonly jwtService: JwtService,
private readonly userService: UserService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// 提取认证令牌
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('Authentication token required');
}
try {
// 验证令牌
const payload = this.jwtService.verify(token);
// 获取用户信息
const user = await this.userService.findById(payload.sub);
if (!user) {
throw new UnauthorizedException('User not found');
}
// 将用户信息附加到请求对象
request.user = user;
return true;
} catch (error) {
throw new UnauthorizedException('Invalid token');
}
}
private extractToken(request: any): string | undefined {
const authHeader = request.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return undefined;
}
return authHeader.substring(7);
}
}2.2 权限守卫
typescript
// 权限守卫实现
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 获取所需的权限
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
// 如果没有权限要求,允许访问
if (!requiredRoles || requiredRoles.length === 0) {
return true;
}
// 获取当前用户
const { user } = context.switchToHttp().getRequest();
if (!user) {
return false;
}
// 检查用户是否具有所需权限
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// 使用权限守卫
@Controller('admin')
@UseGuards(RolesGuard)
export class AdminController {
@Get('users')
@SetMetadata('roles', ['admin'])
getAllUsers() {
return this.userService.findAll();
}
@Get('settings')
@SetMetadata('roles', ['admin', 'moderator'])
getSettings() {
return this.settingsService.getSettings();
}
}3. 拦截器实现监控关注点
3.1 日志拦截器
typescript
// 日志拦截器实现
@Injectable()
export class LoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(LoggingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const { method, url, ip } = request;
const startTime = Date.now();
// 记录请求开始
this.logger.log(`→ ${method} ${url} - IP: ${ip}`);
return next
.handle()
.pipe(
tap({
next: (data) => {
const duration = Date.now() - startTime;
this.logger.log(`← ${method} ${url} - ${duration}ms - Response: ${JSON.stringify(data).substring(0, 100)}...`);
},
error: (error) => {
const duration = Date.now() - startTime;
this.logger.error(`← ${method} ${url} - ${duration}ms - Error: ${error.message}`);
},
}),
);
}
}3.2 性能监控拦截器
typescript
// 性能监控拦截器
@Injectable()
export class TimingInterceptor implements NestInterceptor {
private readonly logger = new Logger(TimingInterceptor.name);
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const startTime = process.hrtime();
return next
.handle()
.pipe(
tap(() => {
const endTime = process.hrtime(startTime);
const duration = endTime[0] * 1000 + endTime[1] / 1000000; // 转换为毫秒
// 记录性能指标
this.logger.log(`${request.method} ${request.url} - ${duration.toFixed(2)}ms`);
// 可以将性能数据发送到监控系统
this.sendMetrics({
method: request.method,
url: request.url,
duration,
timestamp: new Date(),
});
}),
);
}
private sendMetrics(metrics: any) {
// 发送性能指标到监控系统(如 Prometheus、Datadog 等)
// 这里可以集成具体的监控服务
}
}3.3 缓存拦截器
typescript
// 缓存拦截器实现
@Injectable()
export class CacheInterceptor implements NestInterceptor {
private readonly cache = new Map<string, { data: any; timestamp: number }>();
private readonly ttl = 60000; // 60秒缓存时间
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// 只缓存 GET 请求
if (request.method !== 'GET') {
return next.handle();
}
const cacheKey = this.generateCacheKey(request);
const cached = this.cache.get(cacheKey);
// 检查缓存是否有效
if (cached && Date.now() - cached.timestamp < this.ttl) {
return of(cached.data);
}
// 执行实际处理并缓存结果
return next
.handle()
.pipe(
tap((data) => {
this.cache.set(cacheKey, {
data,
timestamp: Date.now(),
});
}),
);
}
private generateCacheKey(request: any): string {
return `${request.method}:${request.url}`;
}
}4. 装饰器实现元数据关注点
4.1 自定义权限装饰器
typescript
// 自定义权限装饰器
export const RequireRoles = (...roles: string[]) => SetMetadata('roles', roles);
export const Public = () => SetMetadata('isPublic', true);
// 使用自定义装饰器
@Controller('users')
export class UserController {
@Get('profile')
@RequireRoles('user', 'admin')
getProfile(@Request() req) {
return req.user;
}
@Get('public-info')
@Public()
getPublicInfo() {
return { info: 'This is public information' };
}
}4.2 资源所有权装饰器
typescript
// 资源所有权检查装饰器
export const RequireOwnership = (resourceParam: string) =>
applyDecorators(
UseGuards(AuthGuard, OwnershipGuard),
SetMetadata('resourceParam', resourceParam),
);
@Injectable()
export class OwnershipGuard implements CanActivate {
constructor(
private reflector: Reflector,
private userService: UserService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user;
// 获取资源参数名
const resourceParam = this.reflector.get<string>('resourceParam', context.getHandler());
if (!resourceParam) {
return true;
}
// 获取资源ID
const resourceId = request.params[resourceParam];
if (!resourceId) {
return false;
}
// 检查用户是否拥有该资源
return this.userService.userOwnsResource(user.id, resourceId);
}
}
// 使用资源所有权装饰器
@Controller('users')
export class UserController {
@Delete(':id')
@RequireOwnership('id')
deleteUser(@Param('id') id: string) {
return this.userService.delete(id);
}
}5. 综合应用示例
5.1 完整的 AOP 实现
typescript
// 综合应用示例
@UseGuards(AuthGuard)
@UseInterceptors(LoggingInterceptor, TimingInterceptor, CacheInterceptor)
@Controller('users')
export class UserController {
constructor(private userService: UserService) {}
@Get('profile')
@RequireRoles('user', 'admin')
getProfile(@Request() req) {
return req.user;
}
@Get('public-info')
@Public()
@UseInterceptors(CacheInterceptor) // 为特定路由添加额外拦截器
getPublicInfo() {
return { info: 'This is public information' };
}
@Delete(':id')
@RequireOwnership('id')
@UseInterceptors(TimingInterceptor) // 为特定路由添加性能监控
deleteUser(@Param('id') id: string) {
return this.userService.delete(id);
}
}5.2 全局 AOP 配置
typescript
// 全局 AOP 配置
@Module({
imports: [UserModule],
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard,
},
{
provide: APP_GUARD,
useClass: RolesGuard,
},
{
provide: APP_INTERCEPTOR,
useClass: LoggingInterceptor,
},
{
provide: APP_INTERCEPTOR,
useClass: TimingInterceptor,
},
],
})
export class AppModule {}6. AOP 最佳实践
6.1 分层关注点管理
typescript
// 分层的 AOP 设计
// 1. 全局层 - 应用级横切关注点
// - 全局日志记录
// - 全局异常处理
// - 全局性能监控
// 2. 模块层 - 模块级横切关注点
// - 模块特定的权限检查
// - 模块特定的缓存策略
// 3. 路由层 - 路由级横切关注点
// - 路由特定的验证逻辑
// - 路由特定的业务规则
// 全局配置
@Injectable()
export class GlobalLoggingInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
// 全局日志记录逻辑
return next.handle();
}
}
// 模块特定配置
@Module({
providers: [
{
provide: APP_INTERCEPTOR,
useClass: ModuleSpecificInterceptor,
},
],
})
export class UserModule {}
// 路由特定配置
@Controller('users')
export class UserController {
@Get(':id')
@UseInterceptors(RouteSpecificInterceptor)
findOne(@Param('id') id: string) {
return this.userService.findById(id);
}
}6.2 性能优化考虑
typescript
// 高性能 AOP 实现
@Injectable()
export class OptimizedLoggingInterceptor implements NestInterceptor {
private readonly logger = new Logger(OptimizedLoggingInterceptor.name);
private readonly ignoredRoutes = ['/health', '/metrics'];
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
// 忽略特定路由的日志记录
if (this.ignoredRoutes.includes(request.url)) {
return next.handle();
}
// 异步记录日志以避免阻塞
this.logRequest(request);
return next.handle();
}
private logRequest(request: any) {
// 使用异步方式记录日志
setImmediate(() => {
this.logger.log(`${request.method} ${request.url}`);
});
}
}7. 总结
NestJS 通过守卫、拦截器和装饰器实现了强大的 AOP 能力:
- 守卫(Guards):处理认证、授权等安全关注点
- 拦截器(Interceptors):处理日志、监控、缓存等运行时关注点
- 装饰器(Decorators):处理元数据、配置等声明式关注点
AOP 的优势包括:
- 关注点分离:将横切关注点与业务逻辑分离
- 代码复用:横切关注点可以跨多个组件复用
- 易于维护:集中管理横切关注点的逻辑
- 灵活配置:可以根据需要在不同层级应用
通过合理使用 AOP,我们可以构建出更加模块化、可维护和可扩展的 NestJS 应用。
在下一篇文章中,我们将探讨拦截器中的 handle():Observable 流的魔法,深入了解为什么能用 RxJS 操作请求/响应流以及 switchMap 实现异步转换。