引入 (Introductions)
引入(Introductions,在 AspectJ 中称为类型间声明 / inter-type declarations)使切面能够声明:被通知的对象实现给定的接口,并由切面代表这些对象提供该接口的实现。
你可以通过使用 @DeclareParents 注解来实现“引入”。该注解用于声明匹配的类型拥有一个新的父接口(因此得名)。例如,给定一个接口 UsageTracked 及其实现类 DefaultUsageTracked,以下切面声明所有 Service 接口的实现者也都实现了 UsageTracked 接口(例如:为了通过 JMX 进行统计):
@Aspect
public class UsageTracking {
@DeclareParents(value="com.xyz.service.*+", defaultImpl=DefaultUsageTracked.class)
public static UsageTracked mixin;
@Before("execution(* com.xyz..service.*.*(..)) && this(usageTracked)")
public void recordUsage(UsageTracked usageTracked) {
usageTracked.incrementUseCount();
}
}@Aspect
class UsageTracking {
companion object {
@DeclareParents(value = "com.xyz.service.*+",
defaultImpl = DefaultUsageTracked::class)
lateinit var mixin: UsageTracked
}
@Before("execution(* com.xyz..service.*.*(..)) && this(usageTracked)")
fun recordUsage(usageTracked: UsageTracked) {
usageTracked.incrementUseCount()
}
}要实现的接口由被注解字段的类型决定。@DeclareParents 注解的 value 属性是一个 AspectJ 类型模式(Type pattern)。任何匹配该类型的 Bean 都会实现 UsageTracked 接口。
注意,在上述示例的前置通知中,Service Bean 可以直接作为 UsageTracked 接口的实现来使用。如果以编程方式访问该 Bean,你可以编写如下代码:
UsageTracked usageTracked = context.getBean("myService", UsageTracked.class);val usageTracked = context.getBean<UsageTracked>("myService")补充教学
1. 什么是“引入”?(Mixin 模式)
在传统的 OOP 中,如果一个类想实现某个接口,必须在源文件中显式写上 implements InterfaceName。 “引入” (Introduction) 打破了这一规则:它允许你在不修改原始类代码的情况下,动态地给这个类贴上一个“新标签”(接口),并注入一份“全套实现”。这种模式在其他编程语言中常被称为 Mixin(混入) 或 Trait。
2. 为何需要 defaultImpl?
Spring AOP 需要知道:当它强行给一个 Bean 加上 UsageTracked 接口后,如果有人调用该接口的方法,具体的执行逻辑在哪里?
defaultImpl指定了提供默认实现逻辑的类。- Spring 会在底层创建一个代理,该代理不仅包含原始 Bean 的逻辑,还包含一个
defaultImpl的实例,用于分发新接口的方法调用。
3. 类型模式中的 *+ 代表什么?
在 value="com.xyz.service.*+" 表达式中:
com.xyz.service.*匹配该包下的任何类或接口。- 最后的
+是关键:它代表“该类及其所有子类或子接口”。 - 这意味着你不仅影响了当前的类,还影响了它的整个继承体系。
4. 状态是共享的吗?
这是一个常见的困惑。如果我有 10 个 Service Bean 都被“引入”了 UsageTracked,它们是共享同一个计数器吗?
- 不是的。由于
@DeclareParents是实例级别的增强,Spring AOP 会为每个匹配到的 Bean 实例分配一个独立的defaultImpl实例。 - 因此,每个 Service Bean 都有自己独立的
usageCount计数器,不会互相干扰。
5. 实战场景示例
- 审计/统计:如文档中的例子,为所有业务 Bean 动态增加一个
getCreationTime()或getInvokeCount()接口。 - 状态标记:为一个 Bean 动态引入
ActiveCondition接口,用于标记该对象当前是否可用,而无需改动业务逻辑。 - 多重继承模拟:虽然 Java 不支持多继承,但通过 AOP 引入,你可以让一个 Bean “看起来”同时拥有多种互不相关的能力。