Appearance
Spring AOP Introductions:让对象"变身"的魔法 ✨
什么是 Introductions?
想象一下,你有一个已经完成的服务类,突然需要给它添加统计功能。传统做法是修改原有代码,但这违反了开闭原则。Spring AOP 的 Introductions(引入)功能就像是给对象施了魔法,让它在不修改原有代码的情况下,"变身"拥有新的能力!
NOTE
Introductions 在 AspectJ 中被称为 inter-type declarations(类型间声明),它允许切面声明被通知的对象实现给定的接口,并代表这些对象提供该接口的实现。
核心原理:对象的"多重身份"
设计哲学
Introductions 的核心思想是:让一个对象在运行时拥有多重身份。就像一个人可以同时是程序员、父亲、篮球爱好者一样,一个对象也可以同时实现多个接口。
实战案例:为服务添加使用统计功能
场景描述
假设我们有一个用户服务,现在需要添加使用统计功能来监控方法调用次数,但又不想修改原有的服务代码。
kotlin
// 原始的用户服务接口
interface UserService {
fun findUserById(id: Long): User
fun createUser(user: User): User
fun updateUser(user: User): User
}
kotlin
@Service
class UserServiceImpl : UserService {
override fun findUserById(id: Long): User {
// 查找用户的业务逻辑
return User(id, "张三", "[email protected]")
}
override fun createUser(user: User): User {
// 创建用户的业务逻辑
println("创建用户: ${user.name}")
return user
}
override fun updateUser(user: User): User {
// 更新用户的业务逻辑
println("更新用户: ${user.name}")
return user
}
}
步骤1:定义统计接口和实现
kotlin
// 使用统计接口
interface UsageTracked {
fun incrementUseCount()
fun getUseCount(): Int
fun resetUseCount()
}
// 默认的统计实现
class DefaultUsageTracked : UsageTracked {
private var useCount = 0
override fun incrementUseCount() {
useCount++
println("📊 方法调用次数: $useCount")
}
override fun getUseCount(): Int = useCount
override fun resetUseCount() {
useCount = 0
println("🔄 统计计数已重置")
}
}
步骤2:创建 Introduction 切面
kotlin
@Aspect
@Component
class UsageTrackingAspect {
companion object {
// 使用 @DeclareParents 声明引入
@DeclareParents(
value = "com.example.service.*+", // 匹配所有service包下的类
defaultImpl = DefaultUsageTracked::class
)
lateinit var mixin: UsageTracked
}
// 在方法执行前记录使用情况
@Before("execution(* com.example.service.*.*(..)) && this(usageTracked)")
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
}
IMPORTANT
@DeclareParents
注解的关键参数:
value
: AspectJ 类型模式,指定哪些类需要引入新接口defaultImpl
: 提供接口的默认实现类
步骤3:使用引入的功能
kotlin
@RestController
class UserController(
private val applicationContext: ApplicationContext
) {
@GetMapping("/users/{id}")
fun getUser(@PathVariable id: Long): User {
// 正常使用原始服务
val userService = applicationContext.getBean("userServiceImpl", UserService::class.java)
val user = userService.findUserById(id)
// 神奇的地方:同一个对象现在也实现了 UsageTracked 接口!
val usageTracked = applicationContext.getBean("userServiceImpl", UsageTracked::class.java)
println("🎯 当前服务调用次数: ${usageTracked.getUseCount()}")
return user
}
@PostMapping("/users/stats/reset")
fun resetStats(): String {
// 获取具有统计功能的服务实例
val usageTracked = applicationContext.getBean("userServiceImpl", UsageTracked::class.java)
usageTracked.resetUseCount()
return "统计已重置"
}
}
深入理解:Introduction 的工作机制
代理模式的魔法
类型匹配模式详解
AspectJ 类型模式语法
com.example.service.*
: 匹配该包下的所有类com.example.service.*+
: 匹配该包下的所有类及其子类com.example.service.UserService+
: 匹配 UserService 接口的所有实现类
高级应用场景
场景1:为多个服务添加缓存功能
kotlin
// 缓存接口
interface Cacheable {
fun clearCache()
fun getCacheStats(): Map<String, Any>
}
// 缓存实现
class DefaultCacheable : Cacheable {
private val cache = mutableMapOf<String, Any>()
private var hitCount = 0
private var missCount = 0
override fun clearCache() {
cache.clear()
println("🗑️ 缓存已清空")
}
override fun getCacheStats(): Map<String, Any> {
return mapOf(
"cacheSize" to cache.size,
"hitCount" to hitCount,
"missCount" to missCount,
"hitRate" to if (hitCount + missCount > 0) hitCount.toDouble() / (hitCount + missCount) else 0.0
)
}
// 内部缓存操作方法
fun get(key: String): Any? = cache[key]?.also { hitCount++ } ?: run { missCount++; null }
fun put(key: String, value: Any) { cache[key] = value }
}
kotlin
@Aspect
@Component
class CacheableAspect {
companion object {
@DeclareParents(
value = "com.example.service.*Service+",
defaultImpl = DefaultCacheable::class
)
lateinit var cacheableMixin: Cacheable
}
// 可以添加缓存相关的切面逻辑
@Around("execution(* com.example.service.*Service.find*(..)) && this(cacheable)")
fun cacheableFind(joinPoint: ProceedingJoinPoint, cacheable: Cacheable): Any? {
val cacheableImpl = cacheable as DefaultCacheable
val cacheKey = generateCacheKey(joinPoint)
// 尝试从缓存获取
cacheableImpl.get(cacheKey)?.let { return it }
// 缓存未命中,执行原方法
val result = joinPoint.proceed()
cacheableImpl.put(cacheKey, result!!)
return result
}
private fun generateCacheKey(joinPoint: ProceedingJoinPoint): String {
return "${joinPoint.signature.name}:${joinPoint.args.contentToString()}"
}
}
场景2:审计日志功能
审计日志完整实现示例
kotlin
// 审计接口
interface Auditable {
fun getAuditLog(): List<AuditEntry>
fun clearAuditLog()
}
// 审计条目
data class AuditEntry(
val timestamp: LocalDateTime,
val operation: String,
val parameters: String,
val result: String,
val executionTime: Long
)
// 审计实现
class DefaultAuditable : Auditable {
private val auditLog = mutableListOf<AuditEntry>()
override fun getAuditLog(): List<AuditEntry> = auditLog.toList()
override fun clearAuditLog() {
auditLog.clear()
}
fun addAuditEntry(entry: AuditEntry) {
auditLog.add(entry)
// 保持最近100条记录
if (auditLog.size > 100) {
auditLog.removeAt(0)
}
}
}
// 审计切面
@Aspect
@Component
class AuditableAspect {
companion object {
@DeclareParents(
value = "com.example.service.*Service+",
defaultImpl = DefaultAuditable::class
)
lateinit var auditableMixin: Auditable
}
@Around("execution(* com.example.service.*Service.*(..)) && this(auditable)")
fun auditOperation(joinPoint: ProceedingJoinPoint, auditable: Auditable): Any? {
val startTime = System.currentTimeMillis()
val operation = joinPoint.signature.name
val parameters = joinPoint.args.contentToString()
return try {
val result = joinPoint.proceed()
val executionTime = System.currentTimeMillis() - startTime
val auditEntry = AuditEntry(
timestamp = LocalDateTime.now(),
operation = operation,
parameters = parameters,
result = result.toString(),
executionTime = executionTime
)
(auditable as DefaultAuditable).addAuditEntry(auditEntry)
result
} catch (e: Exception) {
val executionTime = System.currentTimeMillis() - startTime
val auditEntry = AuditEntry(
timestamp = LocalDateTime.now(),
operation = operation,
parameters = parameters,
result = "ERROR: ${e.message}",
executionTime = executionTime
)
(auditable as DefaultAuditable).addAuditEntry(auditEntry)
throw e
}
}
}
最佳实践与注意事项
✅ 最佳实践
接口设计要简洁
kotlin// 好的设计:职责单一 interface Trackable { fun track(event: String) fun getTrackingData(): Map<String, Any> } // 避免:职责过多 interface OverloadedInterface { fun track(event: String) fun cache(key: String, value: Any) fun log(message: String) fun validate(data: Any): Boolean }
合理使用类型模式
kotlin// 精确匹配 @DeclareParents(value = "com.example.service.UserService+") // 包级匹配 @DeclareParents(value = "com.example.service.*+") // 避免过于宽泛的匹配 @DeclareParents(value = "*")
⚠️ 注意事项
WARNING
类型转换安全
在使用引入的接口时,确保进行安全的类型转换:
kotlin
val service = applicationContext.getBean("userService", UserService::class.java)
if (service is UsageTracked) {
println("调用次数: ${service.getUseCount()}")
}
CAUTION
性能考虑
Introduction 会创建代理对象,在高频调用场景下需要考虑性能影响。建议在开发环境进行性能测试。
总结
Spring AOP 的 Introductions 功能为我们提供了一种优雅的方式来扩展对象的功能,而无需修改原有代码。它的核心价值在于:
- 🎯 遵循开闭原则:对扩展开放,对修改封闭
- 🔧 横切关注点分离:将统计、缓存、审计等功能从业务逻辑中分离
- 🚀 运行时增强:动态为对象添加新的能力
- 📦 代码复用:一个 Introduction 可以应用到多个类
通过 @DeclareParents
注解,我们可以让任何对象在运行时"变身",拥有新的接口实现,这就是 AOP 编程的魅力所在! ✨