让我好好想想,
AspectJ
中最常用的切入点是什么?哦,也许是
call
(
Method-Signature
)吧。这是个相对简单的方法签名。实际上,方法签名的完整形式如下:
[modifiers]
[returnTypePattern]
[DeclaredTypePattern.]methodName([Parameters])[throws
TypePattern]
,其中方括号中的签名组件是可选的。
modifiers
为修饰符模式,
returnTypePattern
为返回类型模式,
DeclaredTypePattern
为类型声明模式,
methodName
为方法名称,
Parameters
为方法参数,
throws
TypePattern
为
throw
字句。该文仅仅介绍
DeclaredTypePattern
,因为相比之下其它模式比较简单的多。
在介绍类型声明模式之前,介绍一下类型模式。类型模式是匹配一种类型或一系列类型的方法。精确的类型模式是如
java.lang.String
一样的完整的有效类型名。但在使用
AspectJ
类型模式时,经常会用到下列通配符(这些通配符同样适用于
Spring
的
AOP
)。
1
)“
*”
:代表任意字符的零次或多次出现。当嵌入到一串字符的内部时,它匹配任意字符的零次或多次出现,除了包分割符(
.
)。
2
)“
+”
:用作类型模式的后缀,代表此类型和其所有的子类型(那些扩展或实现带后缀类型的类型)。
3
)“
..”
:用于指定所有的子包,它匹配任意以包分割符开头和结束的字符串。
下面给出几个示例:
1
)
*Account
使用
Account
名称结束的类型,如
CheckingAccount
2
)
java.*.Date
类型
Date
在任何直接的
java
子包中,如
java.util.Date
和
java.sql.Date
3
)
java..*
任何在
java
包或者所有子包中的类型,如
java.awt
或者
java.awt.event
4
)
javax..*Model+
所有
javax
包或者子包中以
Model
结尾的类型和其所有子类,如
TableModel,TreeModel
。
现在开始说说类型声明模式。实际上,在方法签名中,类型声明模式不是必需的(就像很多书中所说,应该少用类型声明模式而改用与“
target”
结合的切入点指示符)。但如果指定了类型声明模式,切入点将只匹配对由模式匹配的类型(或者超类型)声明的方法的调用。和其他类型模式一样,类型声明模式支持上述的通配符。同时,它也支持复合类型模式。对于类型声明模式来说,程序员容易犯错的地方在于类型声明模式是基于静态类型而不是运行时类型,这也是本文的主要内容。
在很好地理解类型声明模式之前,先看一下下面的例子:
public
class
A {
public
void
foo(){
System.out.println(
"
A.foo()
"
);
}
}
public
class
B
extends
A{
public
void
foo(){
System.out.println(
"
B.foo()
"
);
}
}
public
class
Main {
public
static
void
main(String[] args) {
A b
=
new
B();
b.foo();
//
(1)
callFoo(b);
//
(2)
}
public
static
void
callFoo(A a){
System.out.println(
"
Call A
"
);
}
public
static
void
callFoo(B b){
System.out.println(
"
Call B
"
);
}
}
它的运行结果是这样的:
B.foo()
Call
A
和你的想法一致吗?对于(
1
)处
b.foo()
的调用应用了面向对象中的覆盖(
override
),它是动态的,是在运行时进行解析。而(
2
)处的
callFoo
(b)
则是重载(
overload
),它是静态的,是在编译时解析的。因此,对于变量
b
,虽然它是
B
的一个实例,但
b
的静态类型(也就是变量声明的类型)是
A
;由于重载方法的选择是静态的,所以
main
中调用的是
callFoo(A
a)
,而不是
callFoo(B
b)
。
终于说到了类型声明模式。类型声明模式是基于静态类型信息进行匹配的,而不是动态(或者运行时。下面根据几个典型的例子说明类型声明模式的特性。
还是上面的两个类
A
和
B
,现在我们定义一个方面如下:
public
aspect TypeAspect {
pointcut callA():
call(
*
A.
*
(..));
before():callA(){
System.out.println(
"
call A
"
);
}
}
main
函数内容如下:
public
static
void
main(String[] args) {
A b1
=
new
B();
b1.foo();
B b2
=
new
B();
b2.foo();
}
运行结果如下:
call
A
B.foo()
call
A
B.foo()
可以看到,尽管切入点
callA()
声明的类型为
A
,但实际上,切入点
callA()
可以捕获
A
中的方法及其子类中继承于
A
的方法或重载
A
的方法,而声明的静态类型既可以是
A
也可以是其子类。
但如果在
B
中增加一个新的方法:
public
void
doAnotherThing(){
System.out.println(
"
B.doAnotherThing
"
);
}
main
函数改为:
public
static
void
main(String[] args) {
B b2
=
new
B();
b2.doAnotherThing();
}
输出结果为:
B.doAnotherThing
,如果想对
A
的子类
B
中扩展的方法进行通知,可采用的方法是将切入点
callA()
改为
pointcut
callA():
call
(*
A+.*(..));
。
让我们再来看另一种情景:如果定义一个切入点如下:
pointcut callB():call(
*
B.
*
(..));
before():callB(){
System.out.println(
"
call B
"
);
}
main
函数内容如下:
public
static
void
main(String[] args) {
A b
=
new
B();
b.foo();
}
运行结果为:
B.foo()
。
b.foo()
没有匹配切入点
callB()
的原因在于,
b
的静态类型是
A
,从静态类型的角度来看,这是对
A
的调用,而不是对
B
的调用。在使用
AspectJ
的类型声明时,很容易在这个地方犯错。
好了,如上便是有关类型声明模式的东西,说得有些凌乱,希望对
AspectJ
初学者有些帮助(我本身也是个初学者)。该文参考了《
Eclipse
AspectJ
》和《
AspectJ
cookbook
》。