主题
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
切面注解
| 注解 | 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|---|
@Aspect | order | Int | 0 | 切面执行顺序(越小越先) |
通知注解
| 注解 | 参数 | 类型 | 说明 |
|---|---|---|---|
@Before | value | vararg KClass<Annotation> | 方法执行前触发 |
@After | value | vararg KClass<Annotation> | 方法执行后触发(无论是否异常) |
@AfterReturning | value | vararg KClass<Annotation> | 方法正常返回后触发 |
returning | String | 绑定返回值的参数名 | |
@AfterThrowing | value | vararg KClass<Annotation> | 方法抛异常后触发 |
throwing | String | 绑定异常的参数名 | |
@Around | value | vararg KClass<Annotation> | 环绕通知,完全控制方法执行 |
JoinPoint
| 属性/方法 | 类型 | 说明 |
|---|---|---|
target | Any | 原始对象 |
proxy | Any | 代理对象 |
method | Method | 当前方法 |
args | Array<Any?> | 方法参数 |
signature | String | 方法签名 |
targetClass | Class<*> | 目标类 |
getAnnotation(cls) | T? | 获取方法上的注解 |
hasAnnotation(cls) | Boolean | 判断方法是否有指定注解 |
ProceedingJoinPoint
继承 JoinPoint,仅在 @Around 通知中可用。
| 方法 | 参数 | 返回值 | 说明 |
|---|---|---|---|
proceed() | — | Any? | 执行原方法 |
proceed(args) | Array<Any?> | Any? | 使用新参数执行原方法 |
缓存注解
| 注解 | 参数 | 类型 | 说明 |
|---|---|---|---|
@Cacheable | value | String | 缓存名称 |
key | String | SpEL 表达式,生成缓存 key | |
unless | String | SpEL 条件,为 true 时不缓存 | |
condition | String | SpEL 条件,为 true 时才缓存 | |
@CacheEvict | value | String | 缓存名称 |
key | String | SpEL 表达式 | |
@CachePut | value | String | 缓存名称 |
key | String | SpEL 表达式 |
通知类型详解
@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'、true、null、42 | 字符串/布尔/空/数字 |
| 拼接 | + | 字符串拼接 |
| 比较 | == != > < >= <= | 比较运算 |
| 逻辑 | && || ! | 逻辑运算 |
不支持(混淆不安全):
#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()才能执行原方法,忘记调用会导致原方法被跳过。