Skip to content

AOP(面向切面编程)

基于注解的切面编程框架,将日志、缓存、计时、安全等通用逻辑从业务代码中分离。使用更为现代的 ByteBuddy 而非 CGLib 创建动态代理,支持 final class,性能更优

前置知识

需要了解 IoC 容器,AOP 只对 IoC 管理的 Bean 生效。

Quick Start

kotlin
// 1. 定义标记注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Logged

// 2. 定义切面
@Aspect(order = 1)
class LogAspect {
    @Before(Logged::class)
    fun before(jp: JoinPoint) {
        println("进入方法: ${jp.signature}")
    }
}

// 3. 在业务方法上使用
@Service
class ShopService {
    @Logged
    fun buy(playerId: String, itemId: String) { /* ... */ }
}

API Reference

切面注解

注解参数类型默认值说明
@AspectorderInt0切面执行顺序(越小越先)

通知注解

注解参数类型说明
@Beforevaluevararg KClass<Annotation>方法执行前触发
@Aftervaluevararg KClass<Annotation>方法执行后触发(无论是否异常)
@AfterReturningvaluevararg KClass<Annotation>方法正常返回后触发
returningString绑定返回值的参数名
@AfterThrowingvaluevararg KClass<Annotation>方法抛异常后触发
throwingString绑定异常的参数名
@Aroundvaluevararg KClass<Annotation>环绕通知,完全控制方法执行

JoinPoint

属性/方法类型说明
targetAny原始对象
proxyAny代理对象
methodMethod当前方法
argsArray<Any?>方法参数
signatureString方法签名
targetClassClass<*>目标类
getAnnotation(cls)T?获取方法上的注解
hasAnnotation(cls)Boolean判断方法是否有指定注解

ProceedingJoinPoint

继承 JoinPoint,仅在 @Around 通知中可用。

方法参数返回值说明
proceed()Any?执行原方法
proceed(args)Array<Any?>Any?使用新参数执行原方法

缓存注解

注解参数类型说明
@CacheablevalueString缓存名称
keyStringSpEL 表达式,生成缓存 key
unlessStringSpEL 条件,为 true 时不缓存
conditionStringSpEL 条件,为 true 时才缓存
@CacheEvictvalueString缓存名称
keyStringSpEL 表达式
@CachePutvalueString缓存名称
keyStringSpEL 表达式

通知类型详解

@Before - 前置通知

kotlin
@Aspect
class AuthAspect {
    @Before(RequireAdmin::class)
    fun checkAdmin(joinPoint: JoinPoint) {
        if (!currentUser.isAdmin) {
            throw PermissionDeniedException()
        }
    }
}

@After - 后置通知

无论方法是否抛出异常都会执行:

kotlin
@Aspect
class ResourceAspect {
    @After(AutoClose::class)
    fun cleanup(joinPoint: JoinPoint) {
        // 清理资源
    }
}

@AfterReturning - 返回后通知

kotlin
@Aspect
class AuditAspect {
    @AfterReturning(Audited::class, returning = "result")
    fun audit(joinPoint: JoinPoint, result: Any?) {
        println("方法返回: $result")
    }
}

@AfterThrowing - 异常后通知

kotlin
@Aspect
class ErrorAspect {
    @AfterThrowing(Monitored::class, throwing = "ex")
    fun handleError(joinPoint: JoinPoint, ex: Throwable) {
        logger.error("方法异常: ${ex.message}")
    }
}

@Around - 环绕通知

完全控制方法执行:

kotlin
@Aspect
class PerformanceAspect {
    @Around(Timed::class)
    fun measureTime(pjp: ProceedingJoinPoint): Any? {
        val start = System.currentTimeMillis()
        try {
            return pjp.proceed()  // 执行原方法
        } finally {
            val duration = System.currentTimeMillis() - start
            println("耗时: ${duration}ms")
        }
    }
}

缓存 AOP

Behemiron 内置了缓存切面,支持 @Cacheable@CacheEvict@CachePut

kotlin
@Cacheable("playerData", key = "#p0")
fun getPlayerData(uuid: UUID): PlayerData? { ... }

SpEL(混淆安全语法)

仅支持混淆安全的表达式,避免二开时因为混淆导致失效:

语法示例说明
位置参数#p0#p1#a0#a1按位置引用方法参数
返回值#result仅用于 unless / condition
字面量'text'truenull42字符串/布尔/空/数字
拼接+字符串拼接
比较== != > < >= <=比较运算
逻辑&& || !逻辑运算

不支持(混淆不安全):

  • #paramName(参数名引用)
  • #p0.xxx / #a0.xxx(属性访问)
kotlin
@Cacheable("players", key = "#p0 + ':' + #p1")
@Cacheable("players", unless = "#result == null")

完整示例

kotlin
// 定义标记注解
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Logged

@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class Timed

// 日志切面
@Aspect(order = 1)
class LoggingAspect {
    @Before(Logged::class)
    fun logBefore(joinPoint: JoinPoint) {
        println("调用方法: ${joinPoint.method.name}")
        println("参数: ${joinPoint.args.contentToString()}")
    }

    @After(Logged::class)
    fun logAfter(joinPoint: JoinPoint) {
        println("方法执行完成: ${joinPoint.method.name}")
    }
}

// 计时切面
@Aspect(order = 2)
class TimingAspect {
    @Around(Timed::class)
    fun measureTime(pjp: ProceedingJoinPoint): Any? {
        val start = System.currentTimeMillis()
        try {
            return pjp.proceed()
        } finally {
            println("耗时: ${System.currentTimeMillis() - start}ms")
        }
    }
}

// 业务类(同时使用多个切面注解)
@Service
class UserService {
    @Logged
    @Timed
    fun createUser(name: String): User {
        return User(name)
    }

    @Cacheable("users", key = "#p0")
    fun getUser(id: String): User? {
        return queryFromDb(id)
    }
}

注意事项

  • 只有 IoC 创建的 Bean 才会被代理。手动 new 的对象不会触发 AOP。
  • 业务类内部自调用(this.xxx())不会触发 AOP,因为绕过了代理。
  • Kotlin object 代理取决于运行时实现(可通过 AopAPI.supportsKotlinObjectProxy() 判断)。
  • @Around 通知中必须调用 pjp.proceed() 才能执行原方法,忘记调用会导致原方法被跳过。