让我好好想想, 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 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 ,现在我们定义一个方面如下:
pointcut callA():
call( * A. * (..));
before():callA(){
System.out.println( " call A " );
}
}
main 函数内容如下:
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 中增加一个新的方法:
System.out.println( " B.doAnotherThing " );
}
main 函数改为:
B b2 = new B();
b2.doAnotherThing();
}
输出结果为: B.doAnotherThing ,如果想对 A 的子类 B 中扩展的方法进行通知,可采用的方法是将切入点 callA() 改为 pointcut callA(): call (* A+.*(..)); 。
让我们再来看另一种情景:如果定义一个切入点如下:
before():callB(){
System.out.println( " call B " );
}
main 函数内容如下:
A b = new B();
b.foo();
}
运行结果为: B.foo() 。 b.foo() 没有匹配切入点 callB() 的原因在于, b 的静态类型是 A ,从静态类型的角度来看,这是对 A 的调用,而不是对 B 的调用。在使用 AspectJ 的类型声明时,很容易在这个地方犯错。
好了,如上便是有关类型声明模式的东西,说得有些凌乱,希望对 AspectJ 初学者有些帮助(我本身也是个初学者)。该文参考了《 Eclipse AspectJ 》和《 AspectJ cookbook 》。