Skip to content

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 能力:

  1. 守卫(Guards):处理认证、授权等安全关注点
  2. 拦截器(Interceptors):处理日志、监控、缓存等运行时关注点
  3. 装饰器(Decorators):处理元数据、配置等声明式关注点

AOP 的优势包括:

  1. 关注点分离:将横切关注点与业务逻辑分离
  2. 代码复用:横切关注点可以跨多个组件复用
  3. 易于维护:集中管理横切关注点的逻辑
  4. 灵活配置:可以根据需要在不同层级应用

通过合理使用 AOP,我们可以构建出更加模块化、可维护和可扩展的 NestJS 应用。

在下一篇文章中,我们将探讨拦截器中的 handle():Observable 流的魔法,深入了解为什么能用 RxJS 操作请求/响应流以及 switchMap 实现异步转换。