跟我学AspectJ(一)
编者的话
关于AspectJ的开发资料好象目前还只有英文版的,而且还不是很多,这对于有兴趣学习AOP而英语不是很好的开发人员来是一件很苦闷的事情,所以我决定总结翻译一些有关AOP的Java实现AspectJ的使用和语法的文章,所以才有了跟我学AspectJ这一系列作品的出现,本系列文章是基于Xerox公司的AspectJ产品所包含的编程指南为基础,并加入了我个人的一些理解和例子但是由于有关我还要准备IELTs考试,所以有可能每一篇的发表不会那么的按时,另外由于本人的英语水平也不是太好,所以如果读者发现一些翻译技术上的问题,还请各位看官多多包涵,谢谢。
引语
许多软件开发人员被面向方面编程技术(AOP)深深的吸引,但是苦于不知到如何使用这项技术。他们知道横切关注点(Crosscutting concerns)的概念,而且也确实遇到了这方面的问题。但是问题是如何将AOP引入开发的过程中呢?通常会出现下面的问题:
·能在现有代码上使用方面(aspect)吗?
·使用了方面后能获得怎样的好处呢?
·怎样在程序中找到方面?
·AOP的学习曲线是怎样的呢?
·使用这项新技术的风险是什么?
本文将重点讨论在AspectJ环境下这些问题的答案。AspectJ是对于Java语言的面向方面的扩展。如何获得AspectJ并配置请参见我在CSDN中的文章AspectJ安装和配置指南 。
本文将针对AspectJ介绍一些基本概念并以代码片段的形式说明这些概念(声明一点,这些概念的中文命名十分奇怪,不具代表性,所以建议使用英文名表示)。
当然选取新技术存在很大的风险,因为通常新东西发展特别快,指不定那天完全变了。出于这个考虑许多的公司都对新技术十分保守。还好,方面可以用在开发的不同阶段,而且可以在毫不改变原有代码的情况下实行,所以如果觉得它不怎么的,可以很轻松的去除它的影响。开发阶段的方面可用于程序的调式,单元测试以及性能监测;而在产品阶段的方面完全可以实现通常在Java应用中的横切功能;当然还可以设计开发可重用的方面,很不错哦!
那些有所顾虑的人们完全可以只使用开发阶段的方面,它对一减轻原来很烦琐的重复操作,例如调式时的代码执行情况的追踪。
AspectJ简介
这一部分我将简单介绍AspectJ中的一些基本的概念,如果熟悉它们的读者可以跳过不看(说明:2002月的程序员杂志有国专题介绍,我推荐大家看,十分不错哦,我对于AOP和AspectJ兴趣就始于那次的专题)。
本文使用例子的形式说明概念,这样有助于大家的理解,本文使用简单的图形编辑系统的例子(采用了AspectJ附带文档中的例子),其UML图如下
图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(“物体成功移动”); }
}
总结
本文是本系列的第一篇文章,目的是让读者能够对AOP的Java实现AspectJ有一个基本的认识,并且了解AspectJ中的几个重要的概念连接点、切点、通知还有类型间声明,下一篇将讨论面向方面编程在开发阶段以及产品阶段的应用。
更多信息
参考资料
1.The AspectJTM Programming Guide http://www.eclipse.org/aspectj/
声明
本文由starchu1981保留版权,如果需要转贴请写明作者和出处。