Appearance
Spring AOP 中的 @AspectJ 支持:让面向切面编程变得简单优雅 🎯
引言:为什么需要 @AspectJ?
想象一下,你正在开发一个电商系统,需要在每个关键业务方法中添加日志记录、性能监控、安全检查等功能。传统做法是什么?在每个方法里手动添加这些代码!这样做会导致:
- 🔄 代码重复:相同的横切关注点代码散布在各处
- 🧩 职责混乱:业务逻辑与系统关注点耦合
- 🔧 维护困难:修改一个横切功能需要改动多个地方
@AspectJ 就是为了解决这些痛点而生的!它让我们能够以声明式的方式,优雅地将横切关注点从业务逻辑中分离出来。
TIP
@AspectJ 本质上是一种编程范式的转变:从"在哪里做什么"转向"什么时候做什么"。
什么是 @AspectJ?
@AspectJ 是一种使用注解来声明切面的编程风格。它最初由 AspectJ 项目引入,Spring 框架借鉴了这套注解体系,但运行时仍然使用纯 Spring AOP。
核心设计哲学
@AspectJ 的设计哲学可以用三个关键词概括:
- 声明式:通过注解声明"在何时何地做什么"
- 非侵入式:不修改原有业务代码
- 可重用性:一次定义,多处应用
@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 为我们提供了一种优雅的方式来处理横切关注点,它的核心价值在于:
✅ 解决的核心问题
- 代码重复:一次定义,处处可用
- 关注点分离:业务逻辑与系统关注点解耦
- 可维护性:集中管理横切功能
🚀 设计哲学
- 声明式编程:描述"做什么"而非"怎么做"
- 非侵入式:不修改原有代码结构
- 组合优于继承:通过切面组合功能
💡 最佳实践建议
- 精确的切点表达式:避免过度匹配影响性能
- 合理的通知选择:根据需求选择合适的通知类型
- 适度使用:不要为了 AOP 而 AOP
TIP
@AspectJ 不仅仅是一个技术工具,更是一种编程思维的转变。它教会我们如何优雅地处理横切关注点,让代码更加清晰、可维护。
通过掌握 @AspectJ,你将能够构建更加模块化、可维护的 Spring 应用程序。记住,好的架构不是一蹴而就的,而是在实践中不断演进和优化的结果! 🌟