Skip to content

Spring AOP 中的 @AspectJ 支持:让面向切面编程变得简单优雅 🎯

引言:为什么需要 @AspectJ?

想象一下,你正在开发一个电商系统,需要在每个关键业务方法中添加日志记录、性能监控、安全检查等功能。传统做法是什么?在每个方法里手动添加这些代码!这样做会导致:

  • 🔄 代码重复:相同的横切关注点代码散布在各处
  • 🧩 职责混乱:业务逻辑与系统关注点耦合
  • 🔧 维护困难:修改一个横切功能需要改动多个地方

@AspectJ 就是为了解决这些痛点而生的!它让我们能够以声明式的方式,优雅地将横切关注点从业务逻辑中分离出来。

TIP

@AspectJ 本质上是一种编程范式的转变:从"在哪里做什么"转向"什么时候做什么"。

什么是 @AspectJ?

@AspectJ 是一种使用注解来声明切面的编程风格。它最初由 AspectJ 项目引入,Spring 框架借鉴了这套注解体系,但运行时仍然使用纯 Spring AOP。

核心设计哲学

@AspectJ 的设计哲学可以用三个关键词概括:

  1. 声明式:通过注解声明"在何时何地做什么"
  2. 非侵入式:不修改原有业务代码
  3. 可重用性:一次定义,多处应用

@AspectJ 的核心组成部分

1. 切面(Aspect)

切面是横切关注点的模块化,包含了通知和切点的定义。

2. 切点(Pointcut)

定义了在哪些连接点应用通知的表达式。

3. 通知(Advice)

在特定连接点执行的代码,包括:

  • @Before:前置通知
  • @After:后置通知
  • @AfterReturning:返回后通知
  • @AfterThrowing:异常通知
  • @Around:环绕通知

实战示例:构建一个完整的日志切面

让我们通过一个实际的电商订单服务来演示 @AspectJ 的强大功能:

kotlin
@Service
class OrderService {
    
    private val logger = LoggerFactory.getLogger(OrderService::class.java)
    
    fun createOrder(orderRequest: OrderRequest): OrderResponse {
        // 手动添加日志 - 代码重复!
        logger.info("开始创建订单,参数:$orderRequest") 
        val startTime = System.currentTimeMillis() 
        
        try {
            // 实际业务逻辑
            val order = Order(
                id = generateOrderId(),
                userId = orderRequest.userId,
                items = orderRequest.items,
                totalAmount = calculateTotal(orderRequest.items)
            )
            
            // 保存订单
            orderRepository.save(order)
            
            // 手动添加成功日志 - 代码重复!
            val endTime = System.currentTimeMillis() 
            logger.info("订单创建成功,耗时:${endTime - startTime}ms") 
            
            return OrderResponse.success(order)
        } catch (e: Exception) {
            // 手动添加异常日志 - 代码重复!
            logger.error("订单创建失败:${e.message}", e) 
            throw e
        }
    }
    
    fun cancelOrder(orderId: String): Boolean {
        // 又要重复写一遍日志代码...
        logger.info("开始取消订单,订单ID:$orderId") 
        // ... 更多重复代码
    }
}
kotlin
// 1. 定义切面
@Aspect
@Component
class LoggingAspect {
    
    private val logger = LoggerFactory.getLogger(LoggingAspect::class.java)
    
    // 2. 定义切点:匹配 service 包下所有公共方法
    @Pointcut("execution(* com.example.service..*(..))")
    fun serviceLayer() {} 
    
    // 3. 前置通知:方法执行前记录日志
    @Before("serviceLayer()")
    fun logBefore(joinPoint: JoinPoint) { 
        val methodName = joinPoint.signature.name
        val args = joinPoint.args
        logger.info("🚀 开始执行方法:$methodName,参数:${args.contentToString()}")
    }
    
    // 4. 环绕通知:记录执行时间和结果
    @Around("serviceLayer()")
    fun logAround(proceedingJoinPoint: ProceedingJoinPoint): Any? { 
        val startTime = System.currentTimeMillis()
        val methodName = proceedingJoinPoint.signature.name
        
        return try {
            // 执行目标方法
            val result = proceedingJoinPoint.proceed()
            val endTime = System.currentTimeMillis()
            logger.info("✅ 方法 $methodName 执行成功,耗时:${endTime - startTime}ms")
            result
        } catch (e: Exception) {
            logger.error("❌ 方法 $methodName 执行失败:${e.message}", e)
            throw e
        }
    }
}

// 5. 简洁的业务服务
@Service
class OrderService {
    
    fun createOrder(orderRequest: OrderRequest): OrderResponse { 
        // 纯粹的业务逻辑,无需关心日志记录
        val order = Order(
            id = generateOrderId(),
            userId = orderRequest.userId,
            items = orderRequest.items,
            totalAmount = calculateTotal(orderRequest.items)
        )
        
        orderRepository.save(order)
        return OrderResponse.success(order)
    }
    
    fun cancelOrder(orderId: String): Boolean { 
        // 同样简洁,日志自动处理
        return orderRepository.cancelById(orderId)
    }
}

IMPORTANT

注意对比两种方式:@AspectJ 让业务代码变得纯粹,横切关注点被完美分离!

深入理解切点表达式

切点表达式是 @AspectJ 的核心,它决定了通知在何时何地执行:

kotlin
@Aspect
@Component
class SecurityAspect {
    
    // 1. 基于方法执行的切点
    @Pointcut("execution(* com.example.service.UserService.*(..))") 
    fun userServiceMethods() {}
    
    // 2. 基于注解的切点
    @Pointcut("@annotation(com.example.annotation.RequireAuth)") 
    fun requireAuthMethods() {}
    
    // 3. 基于参数类型的切点
    @Pointcut("execution(* *(.., @Valid (*), ..))") 
    fun validatedMethods() {}
    
    // 4. 组合切点表达式
    @Pointcut("userServiceMethods() && requireAuthMethods()") 
    fun secureUserServiceMethods() {}
    
    @Before("secureUserServiceMethods()")
    fun checkAuthentication(joinPoint: JoinPoint) {
        // 安全检查逻辑
        val currentUser = SecurityContextHolder.getContext().authentication
        if (currentUser == null || !currentUser.isAuthenticated) {
            throw UnauthorizedException("用户未认证")
        }
        logger.info("🔐 用户 ${currentUser.name} 正在访问 ${joinPoint.signature.name}")
    }
}

通知类型详解与最佳实践

1. @Before - 前置通知

在目标方法执行前执行,常用于参数验证、权限检查。

kotlin
@Before("execution(* com.example.service.PaymentService.processPayment(..))")
fun validatePayment(joinPoint: JoinPoint) { 
    val args = joinPoint.args
    val paymentRequest = args[0] as PaymentRequest
    
    // 参数验证
    if (paymentRequest.amount <= 0) {
        throw IllegalArgumentException("支付金额必须大于0")
    }
    
    // 风控检查
    riskControlService.checkPaymentRisk(paymentRequest)
}

2. @Around - 环绕通知

最强大的通知类型,可以完全控制目标方法的执行。

kotlin
@Around("@annotation(Cacheable)")
fun cacheAround(proceedingJoinPoint: ProceedingJoinPoint): Any? { 
    val methodSignature = proceedingJoinPoint.signature as MethodSignature
    val method = methodSignature.method
    val cacheable = method.getAnnotation(Cacheable::class.java)
    
    val cacheKey = generateCacheKey(proceedingJoinPoint)
    
    // 尝试从缓存获取
    val cachedResult = cacheManager.get(cacheKey)
    if (cachedResult != null) {
        logger.info("🎯 缓存命中:$cacheKey")
        return cachedResult
    }
    
    // 执行目标方法
    val result = proceedingJoinPoint.proceed()
    
    // 存入缓存
    cacheManager.put(cacheKey, result, cacheable.expireTime)
    logger.info("💾 结果已缓存:$cacheKey")
    
    return result
}

3. @AfterReturning - 返回后通知

在方法成功返回后执行,可以访问返回值。

kotlin
@AfterReturning(
    pointcut = "execution(* com.example.service.OrderService.createOrder(..))",
    returning = "result"
)
fun afterOrderCreated(joinPoint: JoinPoint, result: OrderResponse) { 
    if (result.success) {
        // 发送订单创建成功的事件
        eventPublisher.publishEvent(OrderCreatedEvent(result.order))
        
        // 发送确认邮件
        emailService.sendOrderConfirmation(result.order)
        
        logger.info("📧 订单 ${result.order.id} 确认邮件已发送")
    }
}

高级特性:引入(Introduction)

引入允许我们为现有的类添加新的方法或字段,这是一个非常强大的特性:

kotlin
// 1. 定义要引入的接口
interface Auditable {
    fun getCreatedTime(): LocalDateTime
    fun getLastModifiedTime(): LocalDateTime
    fun updateLastModifiedTime()
}

// 2. 实现引入的功能
class AuditableImpl : Auditable {
    private var createdTime: LocalDateTime = LocalDateTime.now()
    private var lastModifiedTime: LocalDateTime = LocalDateTime.now()
    
    override fun getCreatedTime(): LocalDateTime = createdTime
    override fun getLastModifiedTime(): LocalDateTime = lastModifiedTime
    override fun updateLastModifiedTime() {
        lastModifiedTime = LocalDateTime.now()
    }
}

// 3. 定义引入切面
@Aspect
@Component
class AuditAspect {
    
    // 为所有实体类引入 Auditable 接口
    @DeclareParents(
        value = "com.example.entity..*", 
        defaultImpl = AuditableImpl::class
    )
    lateinit var auditable: Auditable
    
    // 在实体保存前自动更新时间戳
    @Before("execution(* com.example.repository..save(..)) && args(entity)")
    fun updateTimestamp(entity: Any) {
        if (entity is Auditable) {
            entity.updateLastModifiedTime()
        }
    }
}

// 4. 使用示例
@Service
class UserService {
    
    fun saveUser(user: User) {
        // user 现在自动具有了 Auditable 的功能!
        val auditableUser = user as Auditable 
        logger.info("用户创建时间:${auditableUser.getCreatedTime()}")
        
        userRepository.save(user)
    }
}

配置与启用 @AspectJ 支持

基于注解的配置(推荐)

kotlin
@Configuration
@EnableAspectJAutoProxy
@ComponentScan("com.example")
class AopConfig {
    
    // Spring Boot 项目中通常不需要额外配置
    // @EnableAspectJAutoProxy 会自动处理一切
}

在 Spring Boot 中的自动配置

INFO

Spring Boot 默认启用了 AOP 自动配置,当检测到 spring-boot-starter-aop 依赖时,会自动启用 @AspectJ 支持。

kotlin
// build.gradle.kts
dependencies {
    implementation("org.springframework.boot:spring-boot-starter-aop") 
    // 其他依赖...
}

性能考虑与最佳实践

1. 切点表达式优化

kotlin
// ❌ 低效的切点表达式
@Pointcut("execution(* *(..))")  // 匹配所有方法,性能差

// ✅ 高效的切点表达式
@Pointcut("execution(* com.example.service..*(..))")  // 精确匹配特定包

2. 合理选择通知类型

kotlin
@Aspect
@Component
class PerformanceAspect {
    
    // ✅ 对于简单的前后处理,使用 @Before 和 @After
    @Before("serviceLayer()")
    fun logStart(joinPoint: JoinPoint) {
        logger.info("开始执行:${joinPoint.signature.name}")
    }
    
    // ✅ 对于需要控制执行流程的场景,使用 @Around
    @Around("@annotation(Transactional)")
    fun handleTransaction(proceedingJoinPoint: ProceedingJoinPoint): Any? {
        return transactionTemplate.execute {
            proceedingJoinPoint.proceed()
        }
    }
}

3. 避免过度使用 AOP

WARNING

AOP 虽然强大,但不应该滥用。以下场景不适合使用 AOP:

  • 简单的业务逻辑
  • 只在一两个地方使用的功能
  • 需要复杂参数传递的场景

实际业务场景应用

场景1:接口限流

kotlin
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class RateLimit(
    val value: Int = 100,  // 每分钟允许的请求数
    val timeUnit: TimeUnit = TimeUnit.MINUTES
)

@Aspect
@Component
class RateLimitAspect {
    
    private val rateLimiters = ConcurrentHashMap<String, RateLimiter>()
    
    @Around("@annotation(rateLimit)")
    fun rateLimit(proceedingJoinPoint: ProceedingJoinPoint, rateLimit: RateLimit): Any? { 
        val key = "${proceedingJoinPoint.signature.toShortString()}"
        val rateLimiter = rateLimiters.computeIfAbsent(key) {
            RateLimiter.create(rateLimit.value.toDouble() / 60) // 转换为每秒速率
        }
        
        if (!rateLimiter.tryAcquire()) {
            throw RateLimitExceededException("请求过于频繁,请稍后再试")
        }
        
        return proceedingJoinPoint.proceed()
    }
}

// 使用示例
@RestController
class ApiController {
    
    @RateLimit(value = 10) // 每分钟最多10次请求
    @GetMapping("/sensitive-data")
    fun getSensitiveData(): ResponseEntity<Any> { 
        // 敏感接口,需要限流
        return ResponseEntity.ok(sensitiveDataService.getData())
    }
}

场景2:分布式锁

kotlin
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class DistributedLock(
    val key: String,
    val waitTime: Long = 3000,  // 等待时间(毫秒)
    val leaseTime: Long = 10000 // 锁持有时间(毫秒)
)

@Aspect
@Component
class DistributedLockAspect {
    
    @Autowired
    private lateinit var redissonClient: RedissonClient
    
    @Around("@annotation(distributedLock)")
    fun distributedLock(
        proceedingJoinPoint: ProceedingJoinPoint, 
        distributedLock: DistributedLock
    ): Any? { 
        val lockKey = parseLockKey(distributedLock.key, proceedingJoinPoint)
        val lock = redissonClient.getLock(lockKey)
        
        val acquired = lock.tryLock(
            distributedLock.waitTime, 
            distributedLock.leaseTime, 
            TimeUnit.MILLISECONDS
        )
        
        if (!acquired) {
            throw LockAcquisitionException("获取分布式锁失败:$lockKey")
        }
        
        return try {
            proceedingJoinPoint.proceed()
        } finally {
            if (lock.isHeldByCurrentThread) {
                lock.unlock()
            }
        }
    }
    
    private fun parseLockKey(keyExpression: String, joinPoint: ProceedingJoinPoint): String {
        // 解析 SpEL 表达式,支持动态 key
        return spelExpressionParser.parseExpression(keyExpression)
            .getValue(EvaluationContext(joinPoint)) as String
    }
}

// 使用示例
@Service
class InventoryService {
    
    @DistributedLock(key = "'inventory:' + #productId")
    fun reduceInventory(productId: String, quantity: Int): Boolean { 
        // 减库存操作,需要分布式锁保证并发安全
        val currentInventory = inventoryRepository.findByProductId(productId)
        if (currentInventory.quantity >= quantity) {
            currentInventory.quantity -= quantity
            inventoryRepository.save(currentInventory)
            return true
        }
        return false
    }
}

总结与思考 🎯

@AspectJ 为我们提供了一种优雅的方式来处理横切关注点,它的核心价值在于:

✅ 解决的核心问题

  1. 代码重复:一次定义,处处可用
  2. 关注点分离:业务逻辑与系统关注点解耦
  3. 可维护性:集中管理横切功能

🚀 设计哲学

  • 声明式编程:描述"做什么"而非"怎么做"
  • 非侵入式:不修改原有代码结构
  • 组合优于继承:通过切面组合功能

💡 最佳实践建议

  1. 精确的切点表达式:避免过度匹配影响性能
  2. 合理的通知选择:根据需求选择合适的通知类型
  3. 适度使用:不要为了 AOP 而 AOP

TIP

@AspectJ 不仅仅是一个技术工具,更是一种编程思维的转变。它教会我们如何优雅地处理横切关注点,让代码更加清晰、可维护。

通过掌握 @AspectJ,你将能够构建更加模块化、可维护的 Spring 应用程序。记住,好的架构不是一蹴而就的,而是在实践中不断演进和优化的结果! 🌟