面向切面编程中切面实例化机制的设计与实现
1. 关联切面的特性
1.1 可变性
关联切面具有可变性,通过将关联切入点与 AspectJ 的抽象机制(如抽象切面和切入点重写)相结合,能描述对称和非对称关系。以 Bit 集成为例,可以定义一个包含抽象切入点的切面以及具有具体切入点的子切面,来指定对称或非对称关系。
1.2 简单性
关联切面隐藏了关系的实现细节,使得代码更加简单。例如,图 4 中的 Equality 比图 3 中的更简单,因为图 3 中显式地展示了关系的实现细节(如
Bit.relations
字段和
after
通知中迭代器的使用)。此外,关联切入点与自由变量的组合避免了通知声明的重复。
2. 切面实例的创建与关联
2.1 切面声明语法
关联切面使用
perobjects
修饰符进行声明,语法如下:
aspect A perobjects(T , ...) { mdecl ... }
其中,
A
是切面的名称,
T
是要关联的对象类型,
mdecl
是成员声明,包括构造函数、方法、变量、通知等。
2.2 实例化与关联操作
关联切面可以通过执行
new A(...)
表达式进行实例化,新创建的切面实例不会关联到任何对象。
perobjects(T1, T2, ..., Tn)
修饰符会自动在切面
A
中定义
associate
方法,该方法接受
n
个类型为
T1, ..., Tn
的对象,并将切面实例关联到这些对象。同时,还会定义
void A.delete()
方法,用于撤销关联。
与 AspectJ 中的 per - object 切面不同,关联切面的创建和关联是显式的。当在某些连接点需要为对象创建关联切面时,可以通过定义通知来使这些操作变得非侵入性。
3. 切面实例的通知分发
3.1 分发语义
从语义上讲,将通知分发给切面实例是通过尝试在所有切面实例的上下文中执行相同的通知来实现的,只有满足切入点的实例才会实际运行通知体。为了选择关联的切面实例,提供了关联切入点原语。
3.2 示例说明
以图 5 为例,当执行
b2.set()
时,会创建一个调用连接点。对于第二个通知声明,每个切面实例都会测试切入点。由于只有当切面实例以
b2
作为第二个参数关联时,切入点才会满足,因此只有
a1
会运行通知。
3.3 环绕通知的执行步骤
对于环绕通知,执行以下四个步骤:
1. 从所有切面实例中随机选择一个切面实例。
2. 所选切面实例测试并执行通知声明。
3. 当切面实例执行
proceed
形式,或者切面实例不匹配通知声明时,选择下一个切面实例并从第二步开始重复。如果切面实例不执行
proceed
形式,则继续执行而不运行连接点。
4. 当在第一步或第三步的最后部分没有更多的切面实例时,继续执行连接点。
3.4 关联切入点
关联切入点
associated(v1, ..., vn)
用于确定切面实例如何与对象关联,其中
vi
可以是:
- 由另一个切入点绑定的变量(例如,通过
target(vi)
)
- 作为通配符的星号(
*
),或自由变量
关联切入点的参数中至少要有一个绑定变量。对于关联到
<o1, ..., on>
的切面实例,如果对于任何
1 ≤ i ≤ n
,
vi
是星号、自由变量或绑定到
oi
的变量,则
associated(v1, ..., vn)
切入点的计算结果为
true
。星号和自由变量允许多个切面实例匹配同一个连接点。
3.5 绑定到关联对象
关联切入点可以将变量绑定到关联对象。例如,以下修改后的通知声明:
after(Bit l, Bit r) : call(void Bit.set())
&& target(l) && associated(l,r) {
propagateSet(r);
}
与原始通知行为相同,只是在执行通知体时,将
r
绑定到第二个参数位置的关联对象。
对于对称关联切面,绑定特性可以给出更简短的定义。例如,以下单个通知声明可以替代图 4 中的前两个通知声明:
after(Bit b,Bit o): call(void Bit.set()) && target(b)
&& (associated(b,o) || associated(o,b)) {
propagateSet(o);
}
通过析取运算符组合关联切入点可以识别与目标对象关联的切面实例,而绑定特性将
o
绑定到非目标的关联对象。
4. 静态通知
关联切面可以声明静态通知,其语义与单例切面中的通知声明类似。当通知声明带有
static
修饰符时,切入点匹配和执行只进行一次,无论现有切面实例的数量如何。显然,静态通知声明不能使用关联切入点,其执行上下文是切面类,通知体只能访问静态(或类)变量。
静态通知声明通常用于引导。例如,以下代码中的通知在
callSomeMethod()
发生时创建一个
Equality
实例:
aspect Equality perobjects(Bit, Bit) {
static after(Bit l, Bit r) : callSomeMethod() && args(l,r) {
new Equality(l,r); // 创建一个切面实例
}
...
}
5. 查找切面实例的惯用法
5.1 避免重复实例化
有时需要检查是否有与特定对象元组关联的切面实例,或者对与特定对象关联的所有切面实例执行某些操作。这些操作可以通过带有关联切入点的通知声明来实现。
例如,为了防止为同一对对象创建多个
Equality
切面实例,可以使用以下通知:
aspect Equality perobjects(Bit,Bit) {
...
Equality around(Bit l, Bit r) :
call(Equality.new(Bit,Bit)) && args(l,r)
&& (associated(l,r) || associated(r,l)) {
return this;
}
}
当程序执行
new Equality(b, b')
时,如果已经有与
<b, b'>
或
<b', b>
关联的切面实例,则上述通知会返回该实例,而不是创建新实例。
5.2 枚举关联实例
枚举与特定对象关联的所有切面实例可以通过一个空的静态方法和通知声明来实现。例如,执行
Equality.showAll(b)
可以显示所有与
b
关联的切面实例:
aspect Equality perobjects(Bit,Bit) {
...
static void showAll(Bit b) { }
// 空主体
after(Bit b) :
call(void Equality.showAll(Bit)) && args(Bit b)
&& (associated(b,*) || associated(*,b)) {
System.out.println(this);
// this 绑定到关联实例
}
}
6. 实现
6.1 编译概述
关联切面的机制是通过修改 AspectJ 编译器(
ajc
)版本 1.2.0 来实现的。该编译器接受类和切面声明作为输入,并生成 Java 字节码作为编译代码。下面将分别介绍常规 AspectJ 程序的编译和关联切面的编译。
6.2 常规 AspectJ 程序的编译
AspectJ 编译器将切面声明转换为类,将通知体转换为类的方法。通知通过在通知切入点静态匹配的位置插入方法调用的方式来执行。切入点中的动态条件(如
cflow
和
if
)会转换为插入到转换后通知开头的条件语句。
例如,以下(非关联)切面定义用于按目标对象统计方法调用次数:
aspect Counter pertarget(callSet()) {
pointcut callSet() : call(void Bit.set());
int count = 0;
after() returning() : callSet() {
count++;
}
}
编译
Counter
切面和
Bit
类会生成以下代码:
class Bit {
// 转换后的代码
Counter _aspect;
// 关联的切面实例
boolean value;
// 原始实例变量
public synchronized void _bind() {
if (_aspect == null) _aspect = new Counter();
}
// set, clear 和 get 方法的定义
// ...
}
class Counter {
// 转换后的代码
int count = 0;
// 实例变量
public final void _abody0() {
// after 通知的主体
count++;
}
}
当执行
b.set()
时,会转换为以下语句:
b._bind();
// 如果尚未创建,则创建并关联
b.set();
b._aspect._abody0();
// 通知分发
6.3 关联切面的编译概述
关联切面的编译与其他切面类似,但在关联和通知分发方面有所不同。以
Bit
类和
Equality
切面为例,转换后的
Bit
类包含两个字段:
_aspects1
用于保存从
Bit
到
Equality
的映射,
_aspects2
用于保存
Equality
列表。
class Bit {
// 转换后的代码
Map<Bit, Equality> _aspects1 = new HashMap();
List<Equality> _aspects2 = new ArrayList();
...
}
这两个集合分别用于处理
associated(b, *)
和
associated(*, b)
切入点。当一个切面实例
a
关联到
<b1, b2>
时,
b1._aspects1.get(b2) = a
且
b2._aspects2.contains(a) = true
。
6.4 通知分发的代码转换
通知分发会转换为对映射中所有键值对的循环或对列表的循环。例如,
b.set()
会转换为以下代码来分发两个通知声明:
b.set();
// 原始调用
for(Bit v: b._aspects1.keys()) {
// 第一个 after 通知
Equality a = b._aspects1.get(v);
a._abody0(b);
}
for(Equality a: b._aspects2) {
// 第二个 after 通知
a._abody1(b);
}
当关联切入点的所有参数都被绑定时,通知分发会转换为简单的映射查找。例如,以下通知中关联切入点的参数都由
args
绑定:
after(Bit l, Bit r) : call(Equality.new(Bit,Bit))
&& args(l,r) && associated(l,r) {
System.out.println("duplicated!");
}
new Equality(b1, b2)
表达式会转换为以下语句:
Equality a = b1._aspects1.get(b2);
// 第三个 after 通知
if (a != null) a._abody2(b1,b2);
6.5 编译过程
关联切面的编译过程较为复杂,因为允许切面与任意数量的对象关联,并且关联切入点的参数位置可以使用通配符。编译过程包括以下步骤:
1. 对于每个使用
perobjects
修饰符声明的切面,枚举一组作为通知执行分发键的参数组合。
2. 计算一组参数索引序列,每个序列代表一种记录特定关联切入点关联信息的数据结构类型。
3. 根据参数索引序列,在关联类型中安装用于记录关联信息的字段,并生成用于注册关联的方法。
4. 对于每个与关联切入点匹配的连接点阴影,插入用于分发通知的代码片段。
6.6 关联管理与通知分发规则
编译器根据以下规则生成
associate
方法和通知分发方法:
-
生成
associate
方法的规则
:
Uij =
Map<Tσi(j+1), Uij+1> j < |σi|
A j = |σi| = n
List<A> j = |σi| < n
void associate(T1 v1,T2 v2,. . .,Tn vn) {
install1
install2
...
}
installi =
Ui1mi1 = vσi(1)._aspectsi;
Ui2mi2 = getOrCreatei2(mi1,vσi(2));
Ui3mi3 = getOrCreatei3(mi2,vσi(3));
...
Ui|σi|−1mi|σi|−1 = getOrCreatei|σi|−1(mi|σi|−2,vσi(|σi|−1));
addaspect;
addaspect =
mi|σi|−1.put(vσi(n), this) |σi| = n
Ui|σi| mi|σi| = getOrCreatei|σi|(mi|σi|−1,vσi(|σi|));
mi|σi|.add(this) |σi| < n
vσi(1)._aspectsi = this |σi| = 1
- 生成通知分发方法的规则 :
static void dispatch( Tσi(1) v1, Tσi(2) v2, ..., Tσi(l) vl) {
if(!⟨parameterless dynamic conditions⟩) return;
Ui1 mi1 = v1._aspectsi; if(mi1 == null) return;
Ui2 mi2 = mi1.get(v2); if(mi2 == null) return;
...
Uil−1 mil−1 = mil−2.get(vl−1); if(mil−1 == null) return;
Uil mil = mil−1.get(vl); if(mil == null) return;
for (Uil+1 mil+1 : mil.values()) {
for (Uil+2 mil+2 : mil+1.values()) {
...
for (Ui|σi| mi|σi| : mi|σi|−1.values()) {
invokebody
}
...
}
}
}
invokebody =
for(A a : mi|σi|) a._abody(v1, ..., vl); |σi| < n
mi|σi|._abody(v1, ..., vl); |σi| = n
6.7 映射共享
编译器通过在不同关联切入点之间共享映射来最小化记录关联信息的映射类型数量,避免在关联切入点使用不同参数进行通知分发时使用冗余数据结构。通常,如果一个通知声明的参数序列出现在另一个通知声明的参数序列的开头,则可以重用该映射对象。
为了在切入点之间共享映射,编译器通过应用以下算法计算一组覆盖切面声明中所有参数组合
P
的序列
S
:
S ← {}
while P ≠ {}
τmin ← min|τ|{τ|τ ∈ P}
P ← P \ {τmin}
σmax ← max|σ|{σ|σ ∈ S ∪ {⟨⟩}, τmin contains σ}
S ← S \ {σmax} ∪ {append(σmax, τmin)}
end
综上所述,关联切面的设计与实现提供了一种灵活且高效的方式来处理对象之间的关系和通知分发。通过合理使用关联切入点、静态通知等特性,并结合编译器的优化策略,可以编写出简洁、可维护的代码。
7. 关联切面特性总结
7.1 特性对比
| 特性 | 描述 | 示例 |
|---|---|---|
| 可变性 | 通过结合关联切入点与 AspectJ 抽象机制,能描述对称和非对称关系 | 定义含抽象切入点的切面及具体切入点的子切面指定关系 |
| 简单性 | 隐藏关系实现细节,避免通知声明重复 | 图 4 的 Equality 比图 3 简单 |
| 关联切入点绑定 | 可将变量绑定到关联对象,简化对称关联切面定义 |
after(Bit b,Bit o): call(void Bit.set()) && target(b) && (associated(b,o) || associated(o,b)) { propagateSet(o); }
|
| 静态通知 | 提供类似单例切面的语义,用于引导 |
static after(Bit l, Bit r) : callSomeMethod() && args(l,r) { new Equality(l,r); }
|
7.2 特性使用场景
- 可变性 :适用于需要动态调整对象关系的场景,如不同业务规则下对象间的对称或非对称关联。
- 简单性 :在处理复杂对象关系时,能减少代码冗余,提高代码可读性和可维护性。
- 关联切入点绑定 :对于对称关联的对象,可简化通知声明,减少代码量。
- 静态通知 :在程序启动阶段创建切面实例,实现初始化操作。
8. 编译过程详细分析
8.1 编译流程
graph LR
A[枚举参数组合] --> B[计算参数索引序列]
B --> C[安装记录关联字段并生成注册方法]
C --> D[插入通知分发代码片段]
8.2 具体步骤解释
-
枚举参数组合
:对于使用
perobjects修饰符声明的切面,确定作为通知执行分发键的参数组合。例如,对于associated(v1, v2, *),参数组合为{1, 2}。 -
计算参数索引序列
:根据参数组合,确定记录关联信息的数据结构类型。如对于某个关联切入点,可能使用
⟨2, 3⟩或⟨1, 2, 3⟩这样的序列。 -
安装记录关联字段并生成注册方法
:根据参数索引序列,在关联类型中添加用于记录关联信息的字段,并生成
associate方法。例如,对于aspect A perobjects(T1, T2, T3),可能会在T2中添加Map<T3,List<A>>类型的字段,在T1中添加Map<T2, Map<T3,A>>类型的字段,并生成相应的associate方法。 -
插入通知分发代码片段
:对于每个与关联切入点匹配的连接点阴影,插入通知分发代码。如对于
b.set(),会根据关联情况生成相应的循环或查找代码来执行通知。
8.3 关联管理与通知分发规则示例
假设我们有如下切面定义:
aspect A perobjects(T1, T2, T3) {
before(T2 v2, T3 v3): call(* *.*(..)) && args(v2, v3) && associated(*, v2, v3) { ... }
before(T1 v1, T2 v2, T3 v3): call(* *.*(..)) && args(v1, v2, v3) && associated(v1, v2, v3) { ... }
}
编译器使用序列
σ1 = ⟨2, 3⟩
和
σ2 = ⟨1, 2, 3⟩
时,会生成如下代码:
void associate(T1 v1, T2 v2, T3 v3) {
Map<T3, List<A>> m1_1 = v2._aspects1;
List<A> m1_2 = getOrCreate1_2(m1_1, v3);
m1_2.add(this);
Map<T2, Map<T3, A>> m2_1 = v1._aspects2;
Map<T3, A> m2_2 = getOrCreate2_2(m2_1, v2);
m2_2.put(v3, this);
}
对于通知分发,假设切面定义为:
aspect A perobjects(T1, T2, T3, T4) {
before(T1 v1, T2 v2): call(void m1(T1, T2)) && args(v1,v2) && associated(v1,v2,*, *) {
System.err.println("m1 with " + v1 + "," + v2);
}
}
会生成如下分发方法:
static void _dispatch1(T1 v1, T2 v2) {
Map<T2, Map<T3, List<A>>> m1 = v1._aspects1; if(m1==null) return;
Map<T3, List<A>> m2 = m1.get(v2);
if(m2==null) return;
for (List<A> m3: m2.values()) {
for (A a : m3)
a._abody(v1, v2);
}
}
9. 映射共享机制分析
9.1 映射共享的意义
映射共享可以避免使用冗余的数据结构,减少内存开销。例如,当不同关联切入点的参数序列有包含关系时,共享映射可以提高效率。
9.2 映射共享算法步骤
S ← {}
while P ≠ {}
τmin ← min|τ|{τ|τ ∈ P}
P ← P \ {τmin}
σmax ← max|σ|{σ|σ ∈ S ∪ {⟨⟩}, τmin contains σ}
S ← S \ {σmax} ∪ {append(σmax, τmin)}
end
9.3 算法解释
-
初始化集合
S为空。 -
从参数组合集合
P中选择长度最小的参数组合τmin。 -
从
P中移除τmin。 -
在
S中找到被τmin包含且长度最大的序列σmax。 -
从
S中移除σmax,并将σmax与τmin拼接后的序列加入S。 -
重复上述步骤,直到
P为空。
9.4 映射共享示例
以
aspect A perobjects(T1, T2, T3)
为例,两个切入点的参数序列分别为
σ1 = ⟨2, 3⟩
和
σ2 = ⟨2, 3, 1⟩
,此时可以共享部分映射,减少数据结构的使用。
10. 关联切面的应用建议
10.1 设计阶段
- 明确对象间的关系,确定是对称还是非对称关系,以便合理使用关联切入点和切面的可变性。
- 考虑使用静态通知进行初始化操作,确保程序启动时切面实例的正确创建。
10.2 编码阶段
- 合理使用关联切入点绑定特性,简化对称关联切面的通知声明。
- 遵循编译器的优化策略,如利用映射共享机制,减少内存开销。
10.3 调试阶段
- 检查关联字段和通知分发代码,确保对象间的关联和通知执行符合预期。
- 对于复杂的关联关系,可以通过打印日志等方式跟踪切面实例的创建、关联和通知执行过程。
超级会员免费看
168万+

被折叠的 条评论
为什么被折叠?



