Appearance
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 的核心概念,它让我们能够:
- 分离关注点:将横切逻辑从业务代码中分离
- 提高复用性:一个切面可以作用于多个目标类
- 增强可维护性:修改横切逻辑时只需修改切面代码
- 保持代码整洁:业务类专注于业务逻辑
TIP
记住切面声明的关键点:
- 使用
@Aspect
标记切面类 - 配合
@Component
实现自动检测 - 切面本身不能被其他切面增强
- 合理设计切点表达式以获得最佳性能
通过掌握切面声明,你已经迈出了 Spring AOP 编程的重要一步!在下一节中,我们将深入学习如何声明切点(Pointcut),进一步精确控制切面的作用范围。