跟我学AspectJ(二)
本文继续前篇的内容,将介绍AspectJ的应用范围以及AspectJ的部分基本语言。
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。
更多信息
如果需要转贴请写名作者和出处。