2.1 AOP入门知识
本节主要介绍AOP的入门知识。
2.1.1 AOP简介
AOP(Aspect Oriented Programming),即面向方面编程,是施乐公司帕洛阿尔托研究中心(Xerox PARC)在上世纪90年代发明的一种编程方式。AOP是从OOP中抽象出来的“方面”的概念,目的是为了打破对象的封装性。它以“方面”的方式对原有的模块进行重组,抽取那些与业务无关却为整个系统所通用的功能,并将其最终封装在一起。
在Java的世界里,AOP的应用已经走向成熟。从AOP体现的能力上来说,AspectJ、Spring已经渐趋成熟,在JBoss 4.0中也引入了AOP框架,它在权限管理(Authentication)、错误处理(Error Handling)、事务处理(Transactions)、持久化(Persistence)等方面都获得了很好的应用。
AOP在企业应用中正逐渐体现其自身的价值。但正如其名,它的作用更多地是关注于系统的某一方面。AOP还缺乏革命的驱动力,并不足以颠覆OOP世界。我们不可能像当初面向对象编程取代面向过程编程那样,预见AOP之于OOP具有强大至可以颠覆程序员思想的力量。而事实上,AOP从诞生以来,就从未贴上“革命”的标签。相反,它更多地是起到了一种推波助澜的作用,弥补了OOP的缺失,进而在OO程序设计中,扩展了一种更宽广的模式。
2.1.2 AOP是设计模式的延续
AOP实际上是GoF设计模式的延续,设计模式孜孜不倦追求的是调用者和被调用者之间的解耦,AOP可以说也是这个目标的一种实现,而且AOP的目的就是实现“设计模式”想实现却一直未曾实现的功能。
GoF的“设计模式”给了我们设计的典范与准则,通过最大程度地利用面向对象的特性,诸如利用继承与多态、对责任进行分离、对依赖进行倒置、面向抽象、面向接口,最终设计出灵活、可扩展、可重用的类库与组件,乃至于整个系统的架构。在设计的过程中,通过各种模式体现了对象的行为、暴露的接口、对象间关系,以及对象分别在不同层次中表现出来的形态。然而鉴于对象封装的特殊性,“设计模式”的触角始终在接口与抽象上大做文章,而对于对象内部则无能为力。
举例来说,我们需要为系统提供记录日志的能力。虽然我们可以通过装饰模式(Decorate Pattern)提供各种日志的组合,但不可避免的是,记录日志的大量代码导致了重复代码,同时也导致了强依赖性,这并不利于模块间的解耦。如果我们通过AOP,将这些日志的功能看做是一个“方面”,然后将系统中需要日志能力的模块置于该“方面”的侦听之中,则抽象出来的“方面”就好像是一个容器,只要执行了某种业务,这个容器就会忠实地记录这些模块间传递的消息。至于这些模块到底实现了何种业务,却并非“方面”所关注的。
面向方面编程的价值主要体现在事务处理、日志管理、权限控制等与业务无关却为业务模块所共同调用的逻辑或责任上,而这些所谓的“方面”,恰恰是在企业应用时所必须的。因此,与其说AOP是一种编程的技术,毋宁说AOP是一种企业的“设计模式”。它弥补了OOP之拙,却不曾也不可能超越OOP而单独存在。
2.1.3 第一个AOP程序HelloWorld
下面举一个AOP的HelloWorld的例子,让我们来直观地认识一下AOP。
首先编写一个普通的Java类Hello,然后使用sayHello()函数来输出“Hello”,并编写main()入口函数调用该函数。
public class Hello { public static void sayHello() { System.out.print("Hello "); } public static void main(String[] args) { sayHello(); } }
此时我们运行该程序,将会输出“Hello”这样一个字符串。
下面我们就为该类定义一个方面,如下所示。
public aspect World { pointcut greeting() : execution(* Hello.sayHello(..)); before() : greeting() { System.out.print("Hi, "); } after() returning() : greeting() { System.out.println("World!"); } }
在该方面中,我们首先定义了一个切入点函数greeting(),用以匹配Hello这个类的sayHello()函数。并定义两个通知before()和after(),它们分别在调用Hello.sayHello()前和后执行。然后我们运行该程序,将会输出“Hi, Hello World!”这样一个字符串。
以上是一个简单的例子,可以使用AspectJ实现。它通过额外定义类的方面代码来为原始类添加通用代码,其中的切入点可以通过匹配符语法匹配到所有的类,这样就实现了一个方面为整个应用服务的需求。如果将以上的方面代码换成日志输出,那么就实现了一个完善的日志输出管理器,而通常的做法则是将日志代码编写在每一个类中。通过这种抽取,代码的清晰程度就显而易见了。
2.1.4 使用AOP的优点
AOP可以帮助我们解决代码混乱和代码分散所带来的问题,它还有一些别的好处,如下所述。
● 模块化横切关注点:AOP用最小的耦合处理每个关注点,使得即使是横切关注点也是模块化的。这样的实现产生的系统,其代码的冗余小。模块化的实现还使得系统容易理解和维护;
● 系统容易扩展:由于方面模块根本不知道横切关注点,所以很容易通过建立新的方面加入新的功能。另外,当你往系统中加入新的模块时,已有的方面会自动横切进来,使系统易于扩展;
● 设计决定的迟绑定:使用AOP,设计师可以推迟为将来的需求作决定,因为它可以把这种需求作为独立的方面,从而可以很容易的实现;
● 更好的代码重用性:由于AOP把每个方面都实现为独立的模块,而模块之间又是松散耦合的,所以可以重用方面的实现代码。举例来说,你可以用另外一个独立的日志写入器方面(替换当前的)把日志写入数据库,以满足不同的日志写入要求。
总的来说,松散耦合的实现意味着更好的代码重用性,AOP在使系统实现松散耦合这一点上比OOP做得更好。
2.1.5 使用AOP需要考虑的问题
面向方面的开发过程还必须解决很多的具体问题,诸如:
1.横切关注点的粒度选择问题
粒度越细,绑定得越死。对于具体的AOP实现,单纯从代码的角度来看,可能粒度划分得越细越简单越好。但这样一来,方面的代码与核心代码就会绑定得很死,不好维护。而且方面的代码不优雅,看起来好像单纯把一块代码摘出来放在另一个地方,刻意隐藏的痕迹很重,很难看出AOP的好处。
例如,通过为HelloWorld.java、HelloWorld2.java分别定义各自的AOP实现来进行绑定,这样划分的粒度细化到了每一个具体的类,但是却让AOP的实现与类绑定死了,不能够体现重用性。
为此,我们可以将粒度划分得比较粗。例如可以为HelloWorld*.java定义AOP实现,这样就不至于绑定得太死。而如果粒度过粗,又不能够满足所有的特殊需求,比如记录日志并不都是在函数开头和结尾记录,还需要在其他的时机记录。
所以在应用AOP时进行粒度的选择是很重要的问题,通常需要有丰富的经验积累才能够划分得恰到好处。
2.多个横切关注点的交互问题
这是一个很现实的问题,如果在开发大型项目时遇见有多个横切关注点,应该如何与这些关注点打交道呢?在我们的实际项目中,就遇到了这样一个难题。日志关注点在很多情况下是与其他横切关注点纠缠在一起的,其中的难题是横切关注点的剥离先后顺序问题。在一个版本延续性的开放项目中,由于需要重构,这个问题更为突出。在某些情况下,在横切关注点之间如果存在双向的相互依赖,就必须要修改逻辑,屏蔽这种可能性,否则,以目前的AOP的实现方式很难处理,AOP的代码会很难看,主逻辑的代码重构也会纷繁琐碎,而且极难维护。
3.测试难度的加大
在一个大型的软件开发项目中,软件的生命周期应该都处于可管理的状态。如果AOP大规模介入,很多问题都将变得很敏感,尤其是QA。按照AOP的想法,关注点是互相隔离开的,因此,实质上AOP造成的软件模块间的耦合性变得松散了。软件开发过程的松耦合必然带来测试的复杂性。如果系统中的关注点控制在小范围里,那么这种负担可以在小范围的开发单位内部消化掉,但一旦关注点涉及到全局,这种变动就会蔓延,带来的测试和QA的额外工作会迅速增长,并有可能会冲击到现有的测试与QA流程。
虽然使用AOP带来了一些麻烦,但是AOP的优势还是远远大于其不足的,它的应用还是可以触及到任何一个项目中的,并且为我们的代码混乱带来更清晰的结构,让我们的设计和开发人员能够从中解脱出来。