Skip to content

Spring AOP 切面声明详解:从零开始理解 @Aspect 注解 🎯

什么是切面声明?为什么需要它?

想象一下,你正在开发一个电商系统,需要在每个重要方法执行前记录日志、检查权限、统计性能等。如果按传统方式,你需要在每个方法中重复编写这些代码:

kotlin
class OrderService {
    fun createOrder(order: Order): Order {
        // 记录日志 - 重复代码
        logger.info("开始创建订单")
        
        // 权限检查 - 重复代码
        if (!hasPermission()) throw SecurityException()
        
        // 性能统计开始 - 重复代码
        val startTime = System.currentTimeMillis()
        
        try {
            // 真正的业务逻辑
            return orderRepository.save(order)
        } finally {
            // 性能统计结束 - 重复代码
            val endTime = System.currentTimeMillis()
            logger.info("订单创建耗时: ${endTime - startTime}ms")
        }
    }
    
    fun updateOrder(order: Order): Order {
        // 又要重复上面的代码... 😫
    }
}

WARNING

这种方式导致代码重复、难以维护,违反了 DRY(Don't Repeat Yourself)原则。

切面声明就是为了解决这个问题而生的!它允许我们将这些横切关注点(Cross-cutting Concerns)从业务逻辑中分离出来,统一管理。

Spring AOP 中的切面声明机制

核心概念理解

在 Spring AOP 中,**切面(Aspect)**是一个特殊的类,它包含了横切关注点的实现逻辑。通过 @Aspect 注解,我们告诉 Spring:"这个类不是普通的业务类,而是一个切面类"。

切面声明的基本语法

让我们从最简单的切面声明开始:

kotlin
@Service
class OrderService {
    fun createOrder(order: Order): Order {
        println("=== 开始执行 createOrder ===") 
        // 业务逻辑与日志混在一起
        val result = orderRepository.save(order)
        println("=== createOrder 执行完成 ===") 
        return result
    }
    
    fun updateOrder(order: Order): Order {
        println("=== 开始执行 updateOrder ===") 
        // 重复的日志代码
        val result = orderRepository.update(order)
        println("=== updateOrder 执行完成 ===") 
        return result
    }
}
kotlin
// 1. 声明切面类
@Aspect
@Component
class LoggingAspect {
    
    @Before("execution(* com.example.service.*.*(..))")
    fun logBefore(joinPoint: JoinPoint) {
        println("=== 开始执行 ${joinPoint.signature.name} ===")
    }
    
    @After("execution(* com.example.service.*.*(..))")
    fun logAfter(joinPoint: JoinPoint) {
        println("=== ${joinPoint.signature.name} 执行完成 ===")
    }
}

// 2. 业务类保持纯净
@Service
class OrderService {
    fun createOrder(order: Order): Order {
        // 只关注业务逻辑,日志由切面自动处理
        return orderRepository.save(order)
    }
    
    fun updateOrder(order: Order): Order {
        // 同样只关注业务逻辑
        return orderRepository.update(order)
    }
}

TIP

注意 @Aspect@Component 的组合使用。@Aspect 标记这是一个切面,@Component 让 Spring 能够自动检测并管理这个 Bean。

切面的注册方式

Spring 提供了多种方式来注册切面,让我们逐一了解:

方式一:基于 @Component 的自动检测

kotlin
@Aspect
@Component
class SecurityAspect {
    
    @Before("@annotation(com.example.annotation.RequireAuth)")
    fun checkPermission(joinPoint: JoinPoint) {
        // 权限检查逻辑
        val currentUser = getCurrentUser()
        if (currentUser == null) {
            throw SecurityException("用户未登录")
        }
    }
}

方式二:基于 @Configuration 的 Bean 声明

kotlin
@Configuration
@EnableAspectJAutoProxy
class AopConfiguration {
    
    @Bean
    fun performanceAspect(): PerformanceAspect {
        return PerformanceAspect().apply {
            // 可以在这里配置切面的属性
            threshold = 1000L // 性能阈值设置为1秒
        }
    }
}

@Aspect
class PerformanceAspect {
    var threshold: Long = 500L // 默认阈值500ms
    
    @Around("execution(* com.example.service.*.*(..))")
    fun measurePerformance(joinPoint: ProceedingJoinPoint): Any? {
        val startTime = System.currentTimeMillis()
        
        return try {
            joinPoint.proceed()
        } finally {
            val duration = System.currentTimeMillis() - startTime
            if (duration > threshold) { 
                println("⚠️  方法 ${joinPoint.signature.name} 执行时间过长: ${duration}ms")
            }
        }
    }
}

方式三:XML 配置方式

XML 配置示例(点击展开)
xml
<!-- applicationContext.xml -->
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:aop="http://www.springframework.org/schema/aop">
    
    <!-- 启用 AspectJ 自动代理 -->
    <aop:aspectj-autoproxy/>
    
    <!-- 注册切面 Bean -->
    <bean id="loggingAspect" 
          class="com.example.aspect.LoggingAspect">
        <!-- 可以配置切面属性 -->
        <property name="logLevel" value="INFO"/>
    </bean>
    
</beans>

实战案例:构建一个完整的审计切面

让我们构建一个真实的业务场景:用户操作审计系统。

kotlin
// 1. 自定义注解,标记需要审计的方法
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Auditable(
    val operation: String = "",
    val resourceType: String = ""
)

// 2. 审计切面实现
@Aspect
@Component
class AuditAspect {
    
    private val logger = LoggerFactory.getLogger(AuditAspect::class.java)
    
    @Autowired
    private lateinit var auditService: AuditService
    
    @Around("@annotation(auditable)") 
    fun auditOperation(
        joinPoint: ProceedingJoinPoint,
        auditable: Auditable
    ): Any? {
        val startTime = System.currentTimeMillis()
        val currentUser = getCurrentUser()
        
        // 构建审计记录
        val auditRecord = AuditRecord(
            userId = currentUser?.id,
            operation = auditable.operation.ifEmpty { joinPoint.signature.name },
            resourceType = auditable.resourceType,
            timestamp = LocalDateTime.now(),
            ipAddress = getClientIpAddress()
        )
        
        return try {
            // 执行目标方法
            val result = joinPoint.proceed()
            
            // 记录成功操作
            auditRecord.status = "SUCCESS"
            auditRecord.duration = System.currentTimeMillis() - startTime
            
            result
        } catch (ex: Exception) {
            // 记录失败操作
            auditRecord.status = "FAILED"
            auditRecord.errorMessage = ex.message
            auditRecord.duration = System.currentTimeMillis() - startTime
            
            throw ex // 重新抛出异常
        } finally {
            // 异步保存审计记录,避免影响业务性能
            auditService.saveAuditRecordAsync(auditRecord) 
        }
    }
    
    private fun getCurrentUser(): User? {
        // 从 SecurityContext 获取当前用户
        return SecurityContextHolder.getContext()
            .authentication?.principal as? User
    }
    
    private fun getClientIpAddress(): String {
        // 从 RequestContext 获取客户端 IP
        return RequestContextHolder.currentRequestAttributes()
            .let { it as ServletRequestAttributes }
            .request.remoteAddr
    }
}

// 3. 在业务方法上使用审计注解
@Service
class UserService {
    
    @Auditable(operation = "创建用户", resourceType = "USER") 
    fun createUser(userDto: UserCreateDto): User {
        // 纯粹的业务逻辑,审计由切面自动处理
        return userRepository.save(User.from(userDto))
    }
    
    @Auditable(operation = "删除用户", resourceType = "USER") 
    fun deleteUser(userId: Long) {
        userRepository.deleteById(userId)
    }
}

切面的重要特性与注意事项

1. 切面不能被其他切面增强

IMPORTANT

Spring AOP 中,被 @Aspect 注解标记的类会被排除在自动代理之外,这意味着切面本身不能成为其他切面的目标。

kotlin
@Aspect
@Component
class LoggingAspect {
    
    @Before("execution(* com.example.service.*.*(..))")
    fun logBefore() {
        println("记录日志") // 这个方法不会被其他切面拦截
    }
}

@Aspect  
@Component
class PerformanceAspect {
    
    @Around("execution(* com.example.aspect.LoggingAspect.*(..))")
    fun measureLoggingPerformance(joinPoint: ProceedingJoinPoint): Any? {
        // ❌ 这个通知不会生效,因为 LoggingAspect 被排除在代理之外
        return joinPoint.proceed()
    }
}

2. 切面的执行顺序

当多个切面作用于同一个方法时,可以通过 @Order 注解控制执行顺序:

kotlin
@Aspect
@Component
@Order(1) 
class SecurityAspect {
    @Before("execution(* com.example.service.*.*(..))")
    fun checkSecurity() {
        println("1. 安全检查")
    }
}

@Aspect
@Component  
@Order(2) 
class LoggingAspect {
    @Before("execution(* com.example.service.*.*(..))")
    fun logBefore() {
        println("2. 记录日志")
    }
}

@Aspect
@Component
@Order(3) 
class TransactionAspect {
    @Before("execution(* com.example.service.*.*(..))")
    fun beginTransaction() {
        println("3. 开始事务")
    }
}

执行结果:

1. 安全检查
2. 记录日志  
3. 开始事务
[执行业务方法]

最佳实践与性能优化

1. 合理使用切点表达式

kotlin
@Aspect
@Component
class OptimizedAspect {
    
    // ✅ 推荐:精确的切点表达式
    @Before("execution(* com.example.service.*.create*(..))")
    fun logCreateOperations() {
        // 只拦截 service 包下的 create 开头的方法
    }
    
    // ❌ 避免:过于宽泛的切点表达式
    @Before("execution(* *.*(..))")  
    fun logEverything() {
        // 会拦截所有方法,严重影响性能
    }
}

2. 异步处理非关键逻辑

kotlin
@Aspect
@Component
class AsyncAuditAspect {
    
    @Async
    @EventListener
    fun handleAuditEvent(auditEvent: AuditEvent) {
        // 异步处理审计逻辑,不阻塞主业务流程
        auditService.saveAuditRecord(auditEvent.toAuditRecord())
    }
    
    @After("@annotation(Auditable)")
    fun publishAuditEvent(joinPoint: JoinPoint) {
        // 发布事件而不是直接处理,提高响应速度
        applicationEventPublisher.publishEvent(
            AuditEvent.from(joinPoint)
        )
    }
}

总结 🎉

切面声明是 Spring AOP 的核心概念,它让我们能够:

  1. 分离关注点:将横切逻辑从业务代码中分离
  2. 提高复用性:一个切面可以作用于多个目标类
  3. 增强可维护性:修改横切逻辑时只需修改切面代码
  4. 保持代码整洁:业务类专注于业务逻辑

TIP

记住切面声明的关键点:

  • 使用 @Aspect 标记切面类
  • 配合 @Component 实现自动检测
  • 切面本身不能被其他切面增强
  • 合理设计切点表达式以获得最佳性能

通过掌握切面声明,你已经迈出了 Spring AOP 编程的重要一步!在下一节中,我们将深入学习如何声明切点(Pointcut),进一步精确控制切面的作用范围。