AOP及其实现机制的讨论
AOP及其实现机制的讨论
摘 要:多数软件系统都包括几个跨越多个模块的关注点(concern)。用面向对象技术实现这些关注点会使系统难以实现,难以理解,并且不利于软件的演进。新的面向方面的编程方式(aspect-oriented programming,AOP)利用模块化来分离软件中横切多模块的关注点。首先介绍了AOP的核心思想以及AOP语言的剖析、实现、好处,然后从侧面代码和功能代码的编织问题讨论了两种AOP的实现机制的特点和比较,最后展望了AOP的发展方向。
关键字:AOP;侧面代码;功能代码;编织;关注点
1 引言
面向对象即使很好的解决了软件系统中角色划分的问题。借助于面向对象的分析、设计和实现,开发者可以将问题域的“名词转换成软件系统中的对象,从而很自然地完成问题到软件地转换。但是,问题领域的某些需求却偏偏不是用这样地“名词”描述地。人们认识到,传统的程序经常表现出来一些不能自然地适合单个程序模块或者几个紧密相关地程序模块的行为,例如日志记录、上下文敏感地错误、性能优化以及设计模式等等。我们将这种行为称为“横切关注点(crosscutting concern)”,因为它跨越了给定编程模型中的典型职责界限。如果使用过于横切关注点的代码,您就会知道缺乏模块所带来的问题。因为横切行为的实现是分散的,开发人员发现这种行为难以作逻辑思维、实现和更改。
因此,面向方面的编程(Aspect-Oriented Programming,AOP)应用而生。1997年Gregor、Kiczales等人首次提出AOP的概念。AOP是目前被提议改善关注点分解的技术,它提供了模块化横切关注点的能力,支持功能代码和关注点即侧面代码的分离及自动合并,使程序更容易理解、设计、实现和维护,提供了更高的重用性和生产力,并获得了更好的可跟踪性和灵活性。
AOP为开发者提供了一种描述横切关注点的机制,并能够自动将横切关注点织入到面向对象的软件系统中,从而实现了一种描述横切关注点的模块化。通过划分Aspect代码,横切关注点变得容易处理。开发者可以在编译时更改、插入或除去系统的Aspect,甚至重用系统的Aspect。更重要的是,AOP可能对软件开发的过程造成根本性的影响。我们可以想象这样一种情况:OOP只能用于表示对象之间的泛化-特化(generalization-specialization)关系(通过继承来表现),而对象之间的横向关联则完全用AOP来表现。这样,很多给对象之间横向关联增加灵活性的设计模式(例如Decorator,Role Object等)将不再必要。
AOP的核心思想
面向对象程序设计主要目标是抽象、模块性和代码重用,但是由于目前它只提供了分离关注点的一维,即类,所以不能有效地表示系统地所有关注点,可能造成代码缠结和代码分散。AOP保留面向对象地目标并且努力避免代码缠结和代码分散。目前地AOP方法把构件和侧面看做两个分离地实体。构件用于表示系统地功能模块,侧面用于表示横切系统地非功能特性比如性能优化、同步化、模式如(observer)等,一个侧面就是一个关注点。侧面在编译时或运行时被自动编织到系统的功能行为中来产生整个系统(如图1所示)。一般的方法都是使用一个构件语言(component language)来编写功能,一个或多个侧面语言(aspect language)来编写侧面即关注点,一个侧面组织器(aspect weaver)来合并语言。随着不断深入的研究,也出现了一些方案不单独设计侧面语言,而是让构件和侧面使用同一种语言,利用特定的机制比如反射或运行代码植入等实现构件代码和侧面代码的交互。
图1
2.1 AOP语言剖析
就像其他编程范型的实现一样,AOP的实现由两个部分组成:语言规范和实现。语言规范描述了语言的基础单元和语法;语言实现则按照语言规范来验证代码的正确性,并把代码转换成目标机器可执行的形式。从抽象的角度来看,一种AOP语言要说明两个方面,一是关注点的实现:把每个需求映射为代码,然后,编译器把它翻译成可执行的代码。由于关注点的实现以指定过程的形式出现,你可以使用传统语言如C、C++、Java等。二是织入规则规范作怎样把独立实现的关注点组合起来形成最终系统呢?为了这个目的,需要建立一种语言来指定组合不同的实现单元,以形成最终系统的规则。这种指定织入规则的语言可以是实现语言的扩张,也可以是一种完全不同的语言。
2.2 AOP语言的实现
AOP的编译器执行两个步骤:一是组装关注点;而是把组装结果转成可执行代码。AOP实现可以用多种方式实现织入,包括源代码到源码的转换。它预处理每个方面的源码,产生织入过的源码,然后把织入过的源码交给基础语言的编译器,产生最终可执行代码。比如,使用这种方式,一个基于Java的AOP实现可以先把不同的方面转化成Java源代码,然后让Java编译器把它转化成字节码。也可以直接在字节码级别执行织入;毕竟,字节码本身也是一种源码。此外,底层的执行系统——Java虚拟机——也可以被设计为AOP的。基于Java的AOP实现如果使用这种方式的话,虚拟机可以先装入织入规则,然后对后来装入的类都应用这种规则。也就是说,它可以执行just-in-time的方面织入。
2.3 AOP的好处
AOP可帮助我们解决上面提到的代码混乱和代码分散所带来的问题,它还有一些别的好处:
模块化横切关注点:AOP用最小的耦合处理每个关注点,使得即使是横切关注点也是模块化的。这样的实现产生的系统,其代码的冗余小。模块化的实现还使得系统容易理解和维护。
系统容易扩张:由于方面模块根本不知道横切关注点,所有很容易通过建立新的方面加入新的功能。另外,当你往系统中加入新的模块时,已有的方面自动横切进来,使得系统易于扩展。
设计决定的迟绑定:使用AOP,设计师可以推迟为将来的需求作决定,因为他可以把这种需求作为独立的方面很容易实现。
更好的代码重用性:AOP把每个方面实现为独立的模块,模块之间是松散耦合的。举例来说,你可以用另外一个独立的日志写入器方面来替换当前的,用于把日志写入数据库,以满足不同的日志写入要求。松散耦合的实现通常意味着更好的代码重用性,AOP在使系统实现松散耦合这一点上比OOP做的更好。
两种典型的AOP实现机制的讨论
利用AOP开发的软件系统由构件和侧面组成。构件实现了应用称许的主要功能比如计算保险费。侧面捕获技术上的关注点比如持久性、失败处理、通信或同步化。为了有效开发整个软件系统,AOP必须提供构件代码和侧面代码的自动合并机制。通常构件代码和侧面代码的合并叫做编织(weave)。
这一部分针对侧面和构件的编织中涉及的这些问题的处理讨论两个典型的AOP实现机制,分析它们的特点、优势和缺陷,并进行相互比较。
AspectJ
AspectJ是当前最流行的AOP语言,它是对通用目的的语言Java的扩展,使用几个新的结构提供了模块化横切关注点的结合机制。它增加了类似于类的结构侧面(aspect),可用这个结构来定义横切给定应用程序的代码。Aspect中可以声明切割点(pointcut)、参考点(advice)和引入(introduction)。切割点是连接点(joinpoint)以及连接点的值的集合,连接点即程序执行中定义良好的点。参考是在切割点上的定义切割点的额外行为的类似方法的结构。引入可定义类的新成员,主要用于静态横切,这样就可利用这些新增加的结构定义应用程序的非功能特性,即侧面,使用普通的Java语言定义功能行为,即构件。
举个简单的例子来说明AspectJ是如何处理侧面和构件的编织的。这个例子由两个文件组成,一个是普通的非aspect的java代码文件helloworld.java,一个是aspect代码trace.java。
helloWorld.java
package helloword;
class HelloWorld {
public static void hello (String[] args ) {
new HelloWorld().printMessage();
}
void printMessage () {
System.out.println( “Hello world!”);
}
}
trace.java
package helloworld;
aspect Trace {
pointcut printout();
execution(void printMessage() );
before(): printout() {
System.out.println( “*** Entering printMessage ***”); }
after: printout() {
System.out.outprintln(“***printMessage***”); }
这个例子完成的功能就是跟踪方法printMessage,在执行它之前之后分别打印进入方法和推出方法的信息。首先AspectJ的编译器根据java及AspectJ扩展的语法解析这个两个文件,为每个文件建立一棵静态语法树;然后进入Plan阶段,为侧面trace中的两个参考before和after生成绑定对应切割点的两个planner;接着收集连接点(包括执行方法printMessage()这个连接点joinpoint1及其它连接点);然后为这个连接点根据与其匹配的planner生成有序的plan(这时joinpoint1有两个plan),plan中有连接点和对应的参考的映射;最后Weaver实现每个连接点的plan,就是在原来的连接点的代码体的适当位置嵌入侧面代码中相应于这个连接点的参考的代码,然后用这个嵌入后产生的代码代替语法树上原来的连接点的代码体,这样就实现了侧面代码和非侧面代码的编织,在joinpoint1位置的代码体依次包含参考before中代码、printMessage()代码和参考after中代码。最后要做的就是把编织后的中间代码交给普通的Java编译器,这时候的中间代码已经不存在扩展的那几种结构了,只是普通的Java代码。
AspectJ是对通用目的语言Java的面向侧面的扩展:它实现的编织层次属于编译时,它的编织工作由AspectJ编译器完成,实际上是对代码做了预处理,然后把产生的侧面和构件编织后的中间代码交给java编译器。AspectJ是编织器实现连接点的plans时通过代码转换把侧面代码编织到构件代码中的。AspectJ的编织器时静态的。所谓静态编织器就是利用在连接点插入特定侧面的语句的方法修改类的源码,换句话说静态编织也就是侧面代码被内联到类中,结构就会产生高度优化的编织代码,它的执行速度可以和没有使用侧面的代码相比,因此静态编织不会因为AOP引入了额外的抽象层而给程序性能带来负面影响。AspectC++同AspectJ的实现思想类似,因为侧面和构件是分离的,而且可以横切多组功能构件甚至整个系统的某些特性,所以它们克服传统面向对象程序设计的代码缠结,使得程序更容易理解、设计、实现、和维护,而且提高了重用性。
3.2 DynInst
DynInst是一个建立在动态代码植入技术上的编译后程序操作工具,提供了用于程序植入的C++类库。使用这个类库可以在运行时植入和修改应用程序。DynInst API提供两种主要的抽象:点(point)和代码片断(snippet)。程序中可以进行植入位置的叫点,在一点植入的一小段可执行代码的表示叫代码片断。可以使用这个接口把代码片断定义为抽象语法树提供给类库,并且说明代码片断被植入的点。
在把DynInst用于动态编织侧面和构件的AOP时,只要把侧面看成代码片断,在运行时把它们插入不同的点。目前DynInst API提供 植入点有函数进入、函数退出和函数调用的子程序、函数中的循环、函数中代码块。现在仍然以前面的例子来说明DynInst如何动态编织侧面和构件。首先DynInst的API接口找到要被修改的程序进程,然后为它创建一个线程对象,接着查找植入点(在这个例子中是所有执行printMessage函数的进入点和退出点),并且创建Trace侧面两个打印语句的代码片断,最后利用那个线程对象接口在植入点完成植入。图2显示了把Trace侧面的两个代码片断在运行时动态编织到应用程序中的过程。
图2
首先两个代码片断被翻译成变化进程的内存中的机器语言代码,然后就被拷贝到应用程序的地址空间中的数组中。DynInst使用叫做trampoline的短代码段实现源码的执行到代码片断执行的转移。上图显示了trampoline的结构以及它和植入点的关系。产生代码片断的机器代码后可从植入点转移到BaseTrampoline的开始处,以此来代替植入点的一条或多条指令。然后BaseTrampoline代码转移到第一个MiniTrampoline,第1个MiniTrampoline保存适当的机器状态,然后执行第一个代码片断,在执行完成代码片段后使代码恢复机器状态并转移回BaseTrampoline,然后BaseTrampoline执行被从源代码代替的指令即执行类HelloWorld方法printMessage对应的指令。接着转移到第2个MiniTrampoline,第2个MiniTrampoline同第1个MiniTrampoline类似,在第2个MiniTrampoline转移回BaseTrampoline后BaseTrampoline又转移回源码继续执行。
传统的代码植入在植入代码后要重新编译连接运行。后来又出现了一些编译后植入工具比如EEL、ATOM和Etch,这些工具允许代码在程序执行前插入到二进制代码中。但是经常有些侧面直到运行时才知道,所有传统代码植入或编译后植入用于AOP时存在跟AspectJ同样的缺陷,要么尽可能把所有可能的侧面编织进去,这样就会浪费很多资源并有可能扭曲要测量的现象:要么只植入最少的绝对需要的侧面,这样如果在运行时发现还有要编织的侧面就必须在停止执行后重新编织和执行程序,如果程序很大的话就会浪费大量时间。DynInst可以在运行时任意必要时刻编织任意侧面,不需要重新编译连接运行。
结语
AOP 是一种崭新的编程技术, Grady Booch 的“Through the Looking Glass”(Software Development ,2001 年7月) 讨论了软件工程的未来并预言了多面软件的出现,这种软件同时用多种方法迅速写成。他认为AOP将最终改变整个软件开发的方式,并且更完美地实现“用例驱动”的开发思想。但是,现在的AOP还处于相当不完善的阶段:它只能应用于很少的几种语言环境下,并且必须掌握源代码才能进行织入。
AOP的研究已经成为了热点,目前很多关于.NET平台的AOP系统的研究工作正在进行。实现机制不同的AOP适用于不同要求的应用,因为好多系统存在到运行时才知道的侧面,或者有些侧面需要在运行时修改或删除,所有将来的AOP应该同时支持编译时静态编织和运行时动态编织,这样AOP应该同时支持编译时静态编织和运行时动态编织,这样AOP就能根据要编织的侧面在两种模式间切换,合并两种技术的优点。对依赖于运行时环境的侧面采取运动时动态编织模式,而且使得调试更容易。对于固定的侧面,因为性能原因应该在编译时静态编织。当然对于AOP具体的静态和动态编织机制还要根据不同的应用环境进行选取。同时AOP也要完全分离侧面和构件,不在侧面代码中而是单独声明侧面和构件的关联,提供改善侧面的重用性的机制。
参考文献
G Kiczales ,et al. An Overviewof AspectJ [C] . ECOOP 2001-Object-Oriented Programming , 2001 , Springer Verlag ,LNCS , vol.2072. 3272353.]
Bollert Gregor,Hilsdale. On weaving aspects [C]. ECOOP’99,1999.
Kiczales Gregor, Hilsdale Erik,Hugunin Jim,et al. An overvies of aspect [C]. |ECCOOP,2001.
张广红、陈平, 关于AOP实现机制和应用的研究,计算机工程与设计,2003年第24卷第8期
吕国科、李平立,AOP程序设计方法及其C++语言支持的研究,计算机应用研究,2003年
骆斌、陈武华、张东摩、陈世福,意向驱动的AOP语言AOPLID,南京大学学报(自然科学版) 2000年05期
何克清、何非、应时,角色Use Case: UML的一个更加完全的分析方法,计算机研究与发展 2001年09期
Ramnivas Laddad,通过AspectJ更好地了解AOP,程序员 2002年合订本下
Ramnivas Laddad,利用AOP分离软件关注点,程序员2002年合订本下
Ramnivas Laddad,使用AspectJ描述现实问题里的横切关注点,程序员2002年合订本下
AOP及其实现机制的讨论