AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。
一、AspectJ概述
图1 :FigureEditor例子的UML图
AspectJ(也就是AOP)的动机是发现那些使用传统的编程方法无法很好处理的问题。考虑一个要在某些应用中实施安全策略的问题。安全性是贯穿于系统所有模块间的问题,每个模块都需要应用安全机制才能保证整个系统的安全性,很明显这里的安全策略的实施问题就是一个横切关注点,使用传统的编程解决此问题非常的困难而且容易产生差错,这就正是AOP发挥作用的时候了。
传统的面向对象编程中,每个单元就是一个类,而类似于安全性这方面的问题,它们通常不能集中在一个类中处理因为它们横跨多个类,这就导致了代码无法重用,可维护性差而且产生了大量代码冗余,这是我们不愿意看到的。
面向方面编程的出现正好给处于黑暗中的我们带来了光明,它针对于这些横切关注点进行处理,就好象面向对象编程处理一般的关注点一样。而作为AOP的具体实现之一的AspectJ,它向Java中加入了连接点(Join Point)这个新概念,其实它也只是现存的一个Java概念的名称而已。它向Java语言中加入少许新结构:切点(pointcut)、通知(Advice)、类型间声明(Inter-type declaration)和方面(Aspect)。切点和通知动态地影响程序流程,类型间声明则是静态的影响程序的类等级结构,而方面则是对所有这些新结构的封装。
一个连接点是程序流中指定的一点。切点收集特定的连接点集合和在这些点中的值。一个通知是当一个连接点到达时执行的代码,这些都是AspectJ的动态部分。其实连接点就好比是程序中的一条一条的语句,而切点就是特定一条语句处设置的一个断点,它收集了断点处程序栈的信息,而通知就是在这个断点前后想要加入的程序代码。AspectJ中也有许多不同种类的类型间声明,这就允许程序员修改程序的静态结构、名称、类的成员以及类之间的关系。AspectJ中的方面是横切关注点的模块单元。它们的行为与Java语言中的类很象,但是方面还封装了切点、通知以及类型间声明。
动态连接点模型
任何面向方面编程的关键元素就是连接点模型。AspectJ提供了许多种类的连接点集合,但是本篇只介绍它们中的一个:方法调用连接点集(method call join points)。一个方法调用连接点捕捉对象的方法调用。每一个运行时方法调用都是一个不同的连接点,许多其他的连接点集合可能在方法调用连接点执行时运,包括方法执行时的所有连接点集合以及在方法中其他方法的调用。我们说这些连接点集合在原来调用的连接点的动态环境中执行。
切点
在AspectJ中,切点捕捉程序流中特定的连接点集合。例如,切点
call(void Point.setX(int))
捕捉每一个签名为void Point.setX(int)的方法调用的连接点,也就是说,调用Point对象的有一个整型参数的void setX方法。切点能与其他切点通过或(||)、与(&&)以及非(!)操作符联合。例如 call(void Point.setX(int)) || call(void Point.setY(int)) 捕捉setX或setY调用的连接点。切点还可以捕捉不同类型的连接点集合,换句话说,它们能横切类型。例如
call(void FigureElement.setXY(int,int)) || call(void Point.setX(int))
|| call(void Point.setY(int) || call(void Line.setP1(Point))
|| call(void Line.setP2(Point));
捕捉上述五个方法调用的任意一个的连接点集合。它在本文的例子中捕捉当FigureElement移动时的所有连接点集合。AspectJ使程序员可以命名一个切点集合,以便通知的使用。例如可以为上面的那些切点命名
pointcut move():
call(void FigureElement.setXY(int,int)) || call(void Point.setX(int))
|| call(void Point.setY(int)) || call(void Line.setP1(Point)) || call(void Line.setP2(Point));
无论什么时候,程序员都可以使用move()代替捕捉这些复杂的切点。
前面所说的切点都是基于显示的方法签名,它们称为基于名字(name-based)横切。AspectJ还提供了另一种横切,称为基于属性(property-based)的横切。它们可以使用通配符描述方法签名,例如 call(void Figure.make*(..)) 捕捉Figure对象中以make开头的参数列表任意的方法调用的连接点。而 call(public & Figure.*(..)) 则捕捉Figure对象中的任何公共方法调用的连接点。但是通配符不是AspectJ支持的唯一属性,AspectJ中还有许多其他的属性可供程序员使用。例如cflow,它根据连接点集合是否在其他连接点集合的动态环境中发生标识连接点集合。例如 cflow(move()) 捕捉被move()捕捉到的连接点集合的动态环境中发生的连接点。
通知
虽然切点用来捕捉连接点集合,但是它们没有做任何事。要真正实现横切行为,我们需要使用通知机制。通知包含了切点和要在每个连连接点处执行的代码段。AspectJ有几种通知。
·前通知(Before Advice) 当到达一个连接点但是在程序进程运行之前执行。例如,前通知在方法实际调用之前运行,刚刚在方法的参数被分析之后。
Before() : move(){ System.out.println(“物体将移动了”);}
·后通知(After Advice) 当特定连接点处的程序进程执行之后运行。例如,一个方法调用的后通知在方法体运行之后,刚好在控制返回调用者之前执行。因为Java程序有两种退出连接点的形式,正常的和抛出异常。相对的就有三种后通知:返回后通知(after returning)、抛出异常后通知(after throwing)和清楚的后通知(after),所谓清楚后通知就是指无论是正常还是异常都执行的后通知,就像Java中的finally语句。
After() returning : move(){ System.out.println(“物体刚刚成功的移动了”);}
·在周围通知(Around Advice) 在连接点到达后,显示的控制程序进程是否执行(暂不讨论)
暴露切点环境
切点不仅仅捕捉连接点,它还能暴露连接点处的部分执行环境。切点中暴露的值可以在通知体中声明以后使用。通知声明有一个参数列表(和方法相同)用来描述它所使用的环境的名称。例如后通知
after(FigureElement fe,int x,int y) returning : somePointcuts { someCodes }
使用了三个暴露的环境,一个名为fe的FigureElement对象,两个整型变量x,y。通知体可以像使用方法的参数那样使用这些变量,例如
after(FigureElement fe,int x,int y) returning : somePointcuts {
System.out.println(fe+”移动到(”+x+”,”+y+”)”);
}
通知的切点发布了通知参数的值,三个原生切点this、target和args被用来发布这些值/所以上述例子的完整代码为
after(FigureElement fe,int x,int y) returning : call(void FigureElement.setXY(int,int)
&& target(fe) && args(x,y) {
System.out.println(fe+”移动到(”+x+”,”+y+”)”);
}
目标对象是FigureElement所以fe是after的第一个参数,调用的方法包含两个整型参数所以x和y为after的第二和第三个参数。所以通知打印出方法setXY调用返回后对象移动到的点x和y。当然还可以使用命名切点完成同样的工作,例如
pointcut setXY(FigureElement fe,int x,int y):call(void FigureElement.setXY(int,int)
&& target(fe) && args(x,y);
after(FigureElement fe,int x,int y) returning : setXY(fe,x,y){
System.out.println(fe+”移动到(”+x+”,”+y+”)”);
}
类型间声明
AspectJ的类型间声明指的是那些跨越类和它们的等级结构的声明。这些可能是横跨多个类的成员声明或者是类之间继承关系的改变。不像通知是动态地操作,类型间声明编译时的静态操作。考虑一下,Java语言中如何向一个一些的类中加入新方法,这需要实现一个特定接口,所有类都必须在各自内部实现接口声明的方法,而使用AspectJ则可以将这些工作利用类型间声明放在一个方面中。这个方面声明方法和字段,然后将它们与需要的类联系。
假设我们想有一个Sreen对象观察Point对象的变化,当Point是一个存在的类。我们可以通过书写一个方面,由这个方面声明Point对象有一个实例字段observers,用来保存所有观察Point对象的Screen对象的引用,从而实现这个功能。
Aspect PointObserving{
Private Collection Point.observers=new ArrayList();
……
}
observers字段是私有字段,只有PointObserving能使用。因此,要在aspect中加入方法管理observers聚集。
Aspect PointObserving{
Private Collection Point.observers=new ArrayList();
Public static void addObserver(Point p,Screen s){
p.observers.add(s);
}
public static void removeObserver(Point p,Screen s){
p.observers.remove(s);
}
……
}
然后我们可以定义一个切点stateChanges决定我们想要观察什么并且提供一个after通知定义当观察到变化时我们想要做什么。
Aspect PointObserving{
Private Collection Point.observers=new ArrayList();
Public static void addObserver(Point p,Screen s){
p.observers.add(s);
}
public static void removeObserver(Point p,Screen s){
p.observers.remove(s);
}
pointcut stateChanges(Point p) : target(p) && call(void Point.set*(int));
after(Point p) : stateChanges(p){
Iterator it=p.observers.iterator();
While(it.hasNext()){
UpdateObserver(p,(Screen)it.next()));
}
}
private static void updateObserver(Point p,Screen s){
s.display(p);
}
}
注意无论是Sreen还是Point的代码都没有被修改,所有的新功能的加入都在方面中实现了,很酷吧!
方面
方面以横切模块单元的形式包装了所有的切点、通知和类型间声明。这非常像Java语言的类。实际上,方面也可以定义自己的方法,字段和初始化方法。像类一样一个方面也可以用abstrace关键字声明为抽象方面,可以被子方面继承。在AspectJ中方面的设计实际上使用了单例模式,缺省情况下,它不能使用new构造,但是可以使用一个方法实例化例如方法aspectOf()可以获得方面的实例。所以在方面的通知中可以使用非静态的成员字段。
例如
aspect Tracing {
OutputStream trace=System.out;
After() : move(){ trace.println(“物体成功移动”); }
二、AspectJ应用范围
如前所述,AspectJ可以用于应用开发的不同阶段。下面讨论不同阶段的AspectJ的具体应用情况。
开发型方面(Development Aspects)
开发方面可以很容易的从真正的产品中删除。而产品方面则被可用于开发过程和生产过程,但是仅仅影响某几个类。
这一部分将通过几个例子说明方面在Java应用的开发阶段是如何使用的。这些方面包括调试、测试和性能检测等工作。方面定义的行为范围包括简单的代码跟踪、测试应用的内在联系等等。使用AspectJ不仅使得模块化这些功能变为可能,同时也使得根据需要打开和关闭这些功能变成可能。
代码跟踪(Tracing)
首先让我们看看如何增加一个程序内部工作的可视性。我们定义一个简单的方面用于代码跟踪并且在每个方法调用时输出一些信息。在前一篇的图形编辑例子中,这样的方面可能仅仅简单的跟踪什么时候画一个点。
aspect SimpleTracing {
pointcut tracedCall():
call(void FigureElement.draw(GraphicsContext));
before(): tracedCall() {
System.out.println("Entering: " + thisJoinPoint);
}
}
代码利用了thisJoinPoint变量。在所有的通知体内,这个变量将与描述当前连接点的对象绑定。所以上述代码在每次一个FigureElement对象接受到draw方法时输出如下信息:
Entering: call(void FigureElement.draw(GraphicsContext))
通常我们在调式程序时,会在特定的地方放置几条输出语句,而当调试结束时还需要找到这些代码段将它们删除,这样做不但使我们的代码很难看而且很费时间。而使用AspectJ我们可以克服以上的两个问题,我们可以通过定义切点捕捉任何想要观察的代码段,利用通知可以在方面内部书写输出语句,而不需要修改源代码,当不在需要跟踪语句的时候还可以很轻松的将方面从应用中删除并重新编译代码即可。
前提条件和后续条件(Pre-and Post-Conditions)
许多的程序员使用按契约编程(Design by Contract)的形式。这种形式的编程需要显式的前提条件测试以保证方法调用是否合适,还需要显式的后续条件测试保证方法是否工作正常。AspectJ使得可以模块化地实现这两种条件测试。例如下面的代码
aspect PointBoundsChecking {
pointcut setX(int x):
(call(void FigureElement.setXY(int, int)) && args(x, *))
|| (call(void Point.setX(int)) && args(x));
pointcut setY(int y):
(call(void FigureElement.setXY(int, int)) && args(*, y))
|| (call(void Point.setY(int)) && args(y));
before(int x): setX(x) {
if ( x < MIN_X || x > MAX_X )
throw new IllegalArgumentException("x is out of bounds.");
}
before(int y): setY(y) {
if ( y < MIN_Y || y > MAX_Y )
throw new IllegalArgumentException("y is out of bounds.");
}
}
它实现了边界检测功能。当FigureElement对象移动时,如果x或y的值超过了定义的边界,程序将会抛出IllegalArgumentException异常。
合同实施(Contract Enforcement)
基于属性的横切机制在定义更加复杂的合同实施上非常有用。一个十分强大的功能是它可以强制特定的方法调用只出现在对应的程序中,而在其他程序中不出现。例如,下面的方面实施了一个限制,使得只有在知名的工厂方法中才能向注册并添加FigureElement对象。实施这个限制的目的是为了确保没有任何一个FigureElement对象被注册多次。
static aspect RegistrationProtection {
pointcut register(): call(void Registry.register(FigureElement));
pointcut canRegister(): withincode(static * FigureElement.make*(..));
before(): register() && !canRegister() {
throw new IllegalAccessException("Illegal call " + thisJoinPoint);
}
}
这个方面使用了withincode初始切点,它表示在FigureElement对象的工厂方法(以make开始的方法)体内出现的所有连接点。在before通知中声明一个异常,该通知用于捕捉任何不在工厂方法代码内部产生的register方法的调用。该通知在特定连接点处抛出一个运行时异常,但是AspectJ能做地更好。使用declare error的形式,我们可以声明一个编译时的错误。
static aspect RegistrationProtection {
pointcut register(): call(void Registry.register(FigureElement));
pointcut canRegister(): withincode(static * FigureElement.make*(..));
declare error: register() && !canRegister(): "Illegal call"
}
当使用这个方面后,如果代码中存在定义的这些非法调用我们将无法通过编译。这种情况只出现在我们只需要静态信息的时候,如果我们需要动态信息,像上面提到的前提条件实施时,就可以利用在通知中抛出带参数的异常来实现。
配置管理(Configuration Management )
AspectJ的配置管理可以使用类似于make-file等技术进行处理。程序员可以简单的包括他们想要的方面进行编译。不想要任何方面出现在产品阶段的开发者也可以通过配置他们的make-file使用传统的Java编译器编译整个应用。
产品型方面(Production Aspects)
这一部分的方面例子将描述方面用于生产阶段的应用。产品方面将向应用中加入功能而不仅仅为程序的内部工作增加可视性。
改变监视(Change Monitoring)
在第一个例子,方面的角色是用于维护一位数据标志,由它说明对象从最后一次显示刷新开始是否移动过。在方面中实现这样的功能是十分直接的,testAndClear方法被显示代码调用以便找到一个图形元素是否在最近移动过。这个方法返回标志的状态并将它设置为假。切点move捕捉所有能够是图形移动的方法调用。After通知截获move切点并设置标志位。
aspect MoveTracking {
private static boolean dirty = false;
public static boolean testAndClear() {
boolean result = dirty;
dirty = false;
return result;
}
pointcut move():
call(void FigureElement.setXY(int, int)) ||
call(void Line.setP1(Point)) ||
call(void Line.setP2(Point)) ||
call(void Point.setX(int)) ||
call(void Point.setY(int));
after() returning: move() {
dirty = true;
}
}
这个简单例子同样说明了在产品代码中使用AspectJ的一些好处。考虑使用普通的Java代码实现这个功能:将有可能需要包含标志位,testAndClear以及setFlag方法的辅助类。这些方法需要每个移动的图形元素包含一个对setFlag方法的调用。这些方法的调用就是这个例子中的横切关注点。
·显示的捕捉了横切关注点的结构
·功能容易拔插
·实现更加稳定
传递上下文(Context Passing)
横切结构的上下文传递在Java程序中是十分复杂的一部分。考虑实现一个功能,它允许客户设置所创建的图形对象的颜色。这个需求需要从客户端传入一个颜色或颜色工厂。而要在大量的方法中加入一个参数,目的仅仅是为传递上下文信息这种不方便的情况是所有的程序员都十分熟悉的。
使用AspectJ,这种上下文的传递可以使用模块化的方式实现。下面代码中的after通知仅当一个图形对象的工厂方法在客户ColorControllingClient的某个方法控制流程中调用时才运行。
aspect ColorControl {
pointcut CCClientCflow(ColorControllingClient client):
cflow(call(* * (..)) && target(client));
pointcut make(): call(FigureElement Figure.make*(..));
after (ColorControllingClient c) returning (FigureElement fe):
make() && CCClientCflow(c) {
fe.setColor(c.colorFor(fe));
}
}
这个方面仅仅影响一小部分的方法,但是注意该功能的非AOP实现可能 需要编辑更多的方法。
提供一致的行为(Providing Consistent Behavior)
接下来的例子说明了基于属性的方面如何在很多操作中提供一致的处理功能。这个方面确保包com.bigboxco的所有公共方法记录由它们抛出的任何错误。PublicMethodCall切点捕捉包中的公共方法调用, after通知在任何一个这种调用抛出错误后运行并且记录下这个错误。
aspect PublicErrorLogging {
Log log = new Log();
pointcut publicMethodCall():
call(public * com.bigboxco.*.*(..));
after() throwing (Error e): publicMethodCall() {
log.write(e);
}
}
在一些情况中,这个方面可以记录一个异常两次。这在com.bigboxco包内部的代码自己调用本包中的公共方法时发生。为解决这个问题,我们可以使用cflow初始切点将这些内部调用排除:
after() throwing (Error e) : publicMethodCall() && !cflow(publicMethodCall()) {
log.write(e);
}
结论
AspectJ是对Java语言的简单而且实际的面向方面的扩展。仅通过加入几个新结构,AspectJ提供了对模块化实现各种横切关注点的有力支持。向以有的Java开发项目中加入AspectJ是一个直接而且渐增的任务。一条路径就是通过从使用开发方面开始再到产品方面当拥有了AspectJ的经验后就使用开发可重用方面。当然可以选取其他的开发路径。例如,一些开发者将从使用产品方面马上得到好处,另外的人员可能马上编写可重用的方面。
AspectJ可以使用基于名字和基于属性这两种横切点。使用基于名字横切点的方面仅影响少数几个类,虽然它们是小范围的,但是比起普通的Java实现来说它们能够减少大量的复杂度。使用基于属性横切点的方面可以有小范围或着大范围。使用AspectJ导致了横切关注点的干净、模块化的实现。当编写AspectJ方面时,横切关注点的结构变得十分明显和易懂。方面也是高度模块化的,使得开发可拔插的横切功能变成现实。
AspectJ提供了比这两部分简短介绍更多的功能。本系列的下一章内容,The AspectJ Language,将介绍 AspectJ语言的更多细节和特征。系列的第三章,Examples将通过一些完整的例子说明如何使用AspectJ。建议大家在仔细阅读了接下来的两章后再决定是否在项目中加入AspectJ。
三、AspectJ的高级特性
(一)、The reflection API
说到高级特性,首先要说的就是AspectJ提供的一套reflection API,主要包括JoinPoint、JoinPoint.StaticPart和Signature三个主要的接口。你可以从aspectj.jar中的javadoc来了解它们的详细情况。那它们能提供什么功能呢?其实从字面上就能大致明白:通过这三个接口能访问到Join Points的信息。譬如,调用thisJoinPoint.getArgs()就可以得到方法的参数列表。
(二)、Aspect precedence
在AspectJ中,pointcut和advice都会包含在一个aspect中。在应用系统中,对同一个join point会有多种advice(logging,caching等),这就会引出一个问题:如果系统中有很多的aspect,而这些aspect很有可能会捕获同样的join points,那这些aspect的执行顺序是如何安排的呢?
AspectJ早已为我们考虑到了这个问题,它提供了一种设置aspect precedence的方法。对三种不同的advice来说:
1、before advice是先执行higher-precedence,后执行lower-precedence;
2、around advice是higher-precedence包含lower-precedence,当higher-precedence around advice没有调用proceed()方法时,lower-precedence不会被执行;
3、after advice与before advice正好相反,先执行执行lower-precedence,然后执行higher-precedence。
那应该如何来声明aspect precedence?非常简单,只要在aspect中使用如下的语法即可:
declare precedence : TypePattern1, TypePattern2, ..;
从左往右,排在前面的是higher-precedence advice,后面的是lower-precedence。
(三)、Aspect association
在Java中,为了节省对象每次构建的耗费,增加效率,很多人会考虑使用Singleton模式,让jvm中只有一个实例存在。AspectJ当然为我们考虑到这个问题,Aspect association实际上就是aspect与advised join point object的一种关联关系,这很类似于OO中association,譬如1:1,1:m等。Aspect association能让我们能更好地控制aspect的状态信息。
在AspectJ中可以把Aspect association大致分为三类:
1、Per virtual machine (default)
一个jvm中只有一个aspect instance,AspectJ默认association。
2、Per object
每一个advised join point object都会产生一个aspect instance,不过同一个object instance只会产生一个aspect instance。
3、Per control-flow association
这种association稍微复杂一些,它主要针对程序调用的控制流,譬如:A方法调用B方法,B方法又调用C方法,这就是control-flow。
在aspect中声明这三种association非常简单,它的主要语法如下:
aspect [( )] {
... aspect body
}
Per virtual machine是aspectj的默认association,不需要你额外的声明,正常使用即可。
Per object主要有两种方式:perthis()和pertarget()。perthis()主要用于execution object,pertarget()主要用于target object,两者非常类似。
Per control-flow中也包含两种方式:percflow()和percflowbelow()。这两者也很类似,只是两者的control-flow不太一样而已。
维护aspect的状态信息还有一种方法,就是使用introduce。可以在aspect中introduce member fields,通过fields来保存状态信息。
四、AspectJ实例
使用方面的Tracing程序
写一个具有跟踪能力的类是很简单的事情:一组方法,一个控制其开或关的布尔变量,一种可选的输出流,可能还有一些格式化输出能力。这些都是Trace类需要的东西。当然,如果程序需要的话,Trace类也可以实现的十分的复杂。开发这样的程序只是一方面,更重要的是如何在合适的时候调用它。在大型系统开发过程中,跟踪程序往往影响效率,而且在正式版本中去除这些功能十分麻烦,需要修改任何包含跟踪代码的源码。出于这些原因,开发人员常常使用脚本程序以便向源码中添加或删除跟踪代码。
AspectJ可以更加方便的实现跟踪功能并克服这些缺点。Tracing可以看作是面向整个系统的关注点,因此,Tracing方面可以完全独立在系统之外并且在不影响系统基本功能的情况下嵌入系统。
应用实例
整个例子只有四个类。应用是关于Shape的。TwoShape类是Shape类等级的基类。
public abstract class TwoDShape {
protected double x, y;
protected TwoDShape(double x, double y) {
this.x = x; this.y = y;
}
public double getX() { return x; }
public double getY() { return y; }
public double distance(TwoDShape s) {
double dx = Math.abs(s.getX() - x);
double dy = Math.abs(s.getY() - y);
return Math.sqrt(dx*dx + dy*dy);
}
public abstract double perimeter();
public abstract double area();
public String toString() {
return (" @ (" + String.valueOf(x) + ", " + String.valueOf(y) + ") ");
}
}
TwoShape类有两个子类,Circle和Square
public class Circle extends TwoDShape {
protected double r;
public Circle(double x, double y, double r) {
super(x, y); this.r = r;
}
public Circle(double x, double y) { this( x, y, 1.0); }
public Circle(double r) { this(0.0, 0.0, r); }
public Circle() { this(0.0, 0.0, 1.0); }
public double perimeter() {
return 2 * Math.PI * r;
}
public double area() {
return Math.PI * r*r;
}
public String toString() {
return ("Circle radius = " + String.valueOf(r) + super.toString());
}
}
public class Square extends TwoDShape {
protected double s; // side
public Square(double x, double y, double s) {
super(x, y); this.s = s;
}
public Square(double x, double y) { this( x, y, 1.0); }
public Square(double s) { this(0.0, 0.0, s); }
public Square() { this(0.0, 0.0, 1.0); }
public double perimeter() {
return 4 * s;
}
public double area() {
return s*s;
}
public String toString() {
return ("Square side = " + String.valueOf(s) + super.toString());
}
}
Tracing版本一
首先我们直接实现一个Trace类并不使用方面。公共接口Trace.java
public class Trace {
public static int TRACELEVEL = 0;
public static void initStream(PrintStream s) {...}
public static void traceEntry(String str) {...}
public static void traceExit(String str) {...}
}
如果我们没有AspectJ,我们需要在所有需要跟踪的方法或构造子中直接调用traceEntry和traceExit方法并且初试化TRACELEVEL和输出流。以上面的例子来说,如果我们要跟踪所有的方法调用(包括构造子)则需要40次的方法调用并且还要时刻注意没有漏掉什么方法,但是使用方面我们可以一致而可靠的完成。TraceMyClasses.java
aspect TraceMyClasses {
pointcut myClass(): within(TwoDShape) || within(Circle) || within(Square);
pointcut myConstructor(): myClass() && execution(new(..));
pointcut myMethod(): myClass() && execution(* *(..));
before (): myConstructor() {
Trace.traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myConstructor() {
Trace.traceExit("" + thisJoinPointStaticPart.getSignature());
}
before (): myMethod() {
Trace.traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myMethod() {
Trace.traceExit("" + thisJoinPointStaticPart.getSignature());
}
}
这个方面在合适的时候调用了跟踪方法。根据此方面,跟踪方法在Shape等级中每个方法或构造子的入口和出口处调用,输出的是各个方法的签名。因为方法签名是静态信息,我们可以利用thisJoinPointStaticPart对象获得。运行这个方面的main方法可以获得以下输出:
--> tracing.TwoDShape(double, double)
<-- tracing.TwoDShape(double, double)
--> tracing.Circle(double, double, double)
<-- tracing.Circle(double, double, double)
--> tracing.TwoDShape(double, double)
<-- tracing.TwoDShape(double, double)
--> tracing.Circle(double, double, double)
<-- tracing.Circle(double, double, double)
--> tracing.Circle(double)
<-- tracing.Circle(double)
--> tracing.TwoDShape(double, double)
<-- tracing.TwoDShape(double, double)
--> tracing.Square(double, double, double)
<-- tracing.Square(double, double, double)
--> tracing.Square(double, double)
<-- tracing.Square(double, double)
--> double tracing.Circle.perimeter()
<-- double tracing.Circle.perimeter()
c1.perimeter() = 12.566370614359172
--> double tracing.Circle.area()
<-- double tracing.Circle.area()
c1.area() = 12.566370614359172
--> double tracing.Square.perimeter()
<-- double tracing.Square.perimeter()
s1.perimeter() = 4.0
--> double tracing.Square.area()
<-- double tracing.Square.area()
s1.area() = 1.0
--> double tracing.TwoDShape.distance(TwoDShape)
--> double tracing.TwoDShape.getX()
<-- double tracing.TwoDShape.getX()
--> double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.distance(TwoDShape)
c2.distance(c1) = 4.242640687119285
--> double tracing.TwoDShape.distance(TwoDShape)
--> double tracing.TwoDShape.getX()
<-- double tracing.TwoDShape.getX()
--> double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.getY()
<-- double tracing.TwoDShape.distance(TwoDShape)
s1.distance(c1) = 2.23606797749979
--> String tracing.Square.toString()
--> String tracing.TwoDShape.toString()
<-- String tracing.TwoDShape.toString()
<-- String tracing.Square.toString()
s1.toString(): Square side = 1.0 @ (1.0, 2.0)
Tracing版本二
版本二实现了可重用的tracing方面,使其不仅仅用于Shape的例子。首先定义如下的抽象方面Trace.java
abstract aspect Trace {
public static int TRACELEVEL = 2;
public static void initStream(PrintStream s) {...}
protected static void traceEntry(String str) {...}
protected static void traceExit(String str) {...}
abstract pointcut myClass();
}
为了使用它,我们需要定义我们自己的子类。
public aspect TraceMyClasses extends Trace {
pointcut myClass(): within(TwoDShape) || within(Circle) || within(Square);
public static void main(String[] args) {
Trace.TRACELEVEL = 2;
Trace.initStream(System.err);
ExampleMain.main(args);
}
}
注意我们仅仅在类中声明了一个切点,它是超类中声明的抽象切点的具体实现。版本二的Trace类的完整实现如下
abstract aspect Trace {
// implementation part
public static int TRACELEVEL = 2;
protected static PrintStream stream = System.err;
protected static int callDepth = 0;
public static void initStream(PrintStream s) {
stream = s;
}
protected static void traceEntry(String str) {
if (TRACELEVEL == 0) return;
if (TRACELEVEL == 2) callDepth++;
printEntering(str);
}
protected static void traceExit(String str) {
if (TRACELEVEL == 0) return;
printExiting(str);
if (TRACELEVEL == 2) callDepth--;
}
private static void printEntering(String str) {
printIndent();
stream.println("--> " + str);
}
private static void printExiting(String str) {
printIndent();
stream.println("<-- " + str);
}
private static void printIndent() {
for (int i = 0; i < callDepth; i++)
stream.print(" ");
}
// protocol part
abstract pointcut myClass();
pointcut myConstructor(): myClass() && execution(new(..));
pointcut myMethod(): myClass() && execution(* *(..));
before(): myConstructor() {
traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myConstructor() {
traceExit("" + thisJoinPointStaticPart.getSignature());
}
before(): myMethod() {
traceEntry("" + thisJoinPointStaticPart.getSignature());
}
after(): myMethod() {
traceExit("" + thisJoinPointStaticPart.getSignature());
}
}
它与版本一的不同包括几个部分。首先在版本一中Trace用单独的类来实现而方面是针对特定应用实现的,而版本二则将Trace所需的方法和切点定义融合在一个抽象方面中。这样做的结果是traceEntry和traceExit方法不需要看作是公共方法,它们将由方面内部的通知调用,客户完全不需要知道它们的存在。这个方面的一个关键点是使用了抽象切点,它其实与抽象方法类似,它并不提供具体实现而是由子方面实现它。
Tracing版本三
在前一版本中,我们将traceEntry和traceExit方法隐藏在方面内部,这样做的好处是我们可以方便的更改接口而不影响余下的代码。
重新考虑不使用AspectJ的程序。假设,一段时间以后,tracing的需求变了,我们需要在输出中加入方法所属对象的信息。至少有两种方法实现,一是保持traceEntry和traceExit方法不变,那么调用者有责任处理显示对象的逻辑,代码可能如下
Trace.traceEntry("Square.distance in " + toString());
另一种方法是增强方法的功能,添加一个参数表示对象,例如
public static void traceEntry(String str, Object obj);
public static void traceExit(String str, Object obj);
然而客户仍然有责任传递正确的对象,调用代码如下
Trace.traceEntry("Square.distance", this);
这两种方法都需要动态改变其余代码,每个对traceEntry和traceExit方法的调用都需要改变。
这里体现了方面实现的另一个好处,在版本二的实现中,我们只需要改变Trace方面内部的一小部分代码,下面是版本三的Trace方面实现
abstract aspect Trace {
public static int TRACELEVEL = 0;
protected static PrintStream stream = null;
protected static int callDepth = 0;
public static void initStream(PrintStream s) {
stream = s;
}
protected static void traceEntry(String str, Object o) {
if (TRACELEVEL == 0) return;
if (TRACELEVEL == 2) callDepth++;
printEntering(str + ": " + o.toString());
}
protected static void traceExit(String str, Object o) {
if (TRACELEVEL == 0) return;
printExiting(str + ": " + o.toString());
if (TRACELEVEL == 2) callDepth--;
}
private static void printEntering(String str) {
printIndent();
stream.println("Entering " + str);
}
private static void printExiting(String str) {
printIndent();
stream.println("Exiting " + str);
}
private static void printIndent() {
for (int i = 0; i < callDepth; i++)
stream.print(" ");
}
abstract pointcut myClass(Object obj);
pointcut myConstructor(Object obj): myClass(obj) && execution(new(..));
pointcut myMethod(Object obj): myClass(obj) &&
execution(* *(..)) && !execution(String toString());
before(Object obj): myConstructor(obj) {
traceEntry("" + thisJoinPointStaticPart.getSignature(), obj);
}
after(Object obj): myConstructor(obj) {
traceExit("" + thisJoinPointStaticPart.getSignature(), obj);
}
before(Object obj): myMethod(obj) {
traceEntry("" + thisJoinPointStaticPart.getSignature(), obj);
}
after(Object obj): myMethod(obj) {
traceExit("" + thisJoinPointStaticPart.getSignature(), obj);
}
}
在此我们必须在methods切点排除toString方法的执行。问题是toString方法在通知内部调用,因此如果我们跟踪它,我们将陷入无限循环中。这一点不明显,所以必须在写通知时格外注意。如果通知回调对象,通常都回存在循环的可能性。
事实上,简单的排除连接点的执行并不够,如果在这之中调用了其他跟踪方法,那么就必须提供以下限制
&& !cflow(execution(String toString()))
排除toString方法的执行以及在这之下的所有连接点。
总之,为了实现需求的改变我们必须在Trace方面中做一些改变,包括切点说明。但是实现的改变只局限于Trace方面内部,而如果没有方面,则需要更改每个应用类的实现。