AspectJinAction
1 介绍AOP
想象你是一个正在设计一幢房屋的建筑师,你首要关注的问题在于对于房屋的的核心特性做出好的决断,例如地基的设计,墙壁的高度,屋顶的倾斜度,房间的位置和大小,等等。你第二要关注的问题是一些在多个核心要素之间共享的特性,例如电线走向和卫浴水管。再想象你在设计一座桥梁,这时虽然首要关注的东西是不一样的,例如桥墩,桁架,梁和拉索,但是第二关注点仍然会包括象电源布线这样全系统范围的特性。
软件的设计也是以类似的方式运作的。一个软件架构师,当需要设计一个新系统时,首先需要处理那些主要的核心功能,对于商业应用来说也就是基本的业务逻辑。例如在一个银行应用中,核心模块被设计用来处理储户们使用的银行事务;在一个零售应用中,核心模块处理的是购买商品和库存管理。在这两个应用中,有一些全系统范围的共同的关注点例如日志记录,用户认证,数据持久,以及其他一些对很多核心业务模块来说共同的要素。
让我们来看另一个软件的例子。如果这个架构师在设计一个机器人的应用程序,核心关注点在于动作管理和路径计算,而对于许多核心模块来说共同的关注点包括日志记录,远程管理和路径优化这样的特性。这些分布在多个模块中的全系统范围特性我们称之为:横切关注点。面向方面编程(AOP)就是处理这些横切关注点。
虽然面向对象编程(OOP)是当前处理核心关注点的使用最广泛的方法,它对于许多横切关注点来说还很不够,尤其是在负载的应用当中。就像你将在这一章中看到的那样,典型的OOP实现所制造的核心关注点和横切关注点之间的耦合是很不理想的,因为如果要增加新的横切特性,或者修改现存的横切功能,都需要对相关的所有核心模块进行修改。
AOP是一套新的方法,通过提供一种新的能够能够横切其他模块的模块化单位:aspect(方面),达到了分隔横切关注点的目的。在AOP中,你在aspect中实现横切关注点,而不是把他们融合到核心模块当中去。一个类似于编译器的东东:aspect weaver(方面编织器),通过一个称为weaving(织入)的过程把核心模块和横切模块合并到一起,从而构造出最终的实际系统。最终,AOP用一种边界清晰的方式把横切关注点模块化,产生出一个更容易设计,实现和维护的系统架构。
在开篇的这一章中,我们看到AOP的基础,它所应对的问题,以及为什么你需要懂得它。
1.1 架构师的两难困境
也许当今软件工程当中最普遍的疑问是:到底多少设计算是太多?优秀的系统架构师会考虑当前的以及潜在的未来需求,如果没有考虑到一个潜在的未来横切性质的需求,也许会导致系统许多部分需要更改甚至可能要重新实现。另一方面,包含一些几率很低的特性也会导致设计过度、难以理解还臃肿的系统。我们需要创建一个设计良好的系统既能满足未来的需求又无需做出质量上的妥协。这样,对未来的不可预见和交货时间的压力将会使人只好仅仅专注于当前的需要。进一步说,既然未来需求总是在不端改变,为什么现在要去考虑它们呢?我把设计不足和设计过度称之为架构师困境。
理解架构师困境是理解为什么需要AOP的关键一步。否则,开发人员总是会问:“难道你不能通过更好地应用我们当前的设计技术来满足需求?”或者“AOP不就是修补那些糟糕和不当的设计么?”这两个问题的答案都是否定的。
想想你最近的几个项目,在事后会有一些很明显的想法让你觉得本应做出不同的设计。问题在于,通过当初你所掌握的信息,你能做出这些决断么?尽管在理论上那些关注点应该能够被预见性地处理,就算只能部分地解决,但是在实际生活中却不能够做到。
举个例子吧,一个架构师应该在项目的初级阶段考虑性能方面的需求么?一般的方式是先创建系统,然后测量性能,再用优化手段来改造系统,这样来提升性能,这种方式导致系统的很多部分通过测量之后可能会被修改,然后,随着时间过去,使用方式的改变导致一些新的瓶颈需要被处理。对于设计可重用程序库的架构师来说,任务甚至还要艰难,因为对于库来说,想象到它所有的使用情形要更困难。当今快速变化的技术使之变得难上加难,因为一些技术上的改变会使得某些设计决策突然变得毫无用处。表1.1列举了这些作用于架构师并成为架构师困境根源的力量。
[Table 1.1]
当软件项目被推出而无力满足未来的业务需求的时候,通常都会把帐算在那些设计决策头上。无论如何,通常被认为是设计努力不足或者设计缺陷的东西,也许只是来自所用的设计方法或者语言实现的限制。使用当前的设计和实现技术,我们在制造均衡满足当前和潜在未来需求的系统方面,有一个限度,当考虑到一个富于特性的产品,其持续增长的交货时间压力和品质要求使得这个限制不能被接受。
所以,架构师困境是在软件过程中取得平衡的一个长期性问题,你总是在向这个平衡努力,尽管你知道你达不到这个平衡。使用AOP,就像你接下来将看到的,你可以做得更好。在这整本书中,你将看到许许多多架构师困境的范例,以及为什么AOP是当前可用的方法中对此处理得最好的一种。
有一点需要被强调清楚:AOP不是对糟糕的或者不足的设计的一种矫正。实际上,在一个设计的很糟糕的核心系统中去实现横切关注点是很鲁莽的行为。你将仍然需要使用像OOP这样传统的方法来创建一个坚实的核心架构。AOP所提供的并不是一个完整的全新设计过程,而是一个额外的方案,让设计师能够处理潜在的未来需求而不用破坏整个核心架构,并且在项目的初始阶段也能够更容易地处理那些横切性关注点,因为它们能够在需要的时候被织入系统中而不会危及原来的设计。
1.2 编程方法的演变
从机器语言到过程性语言再到OOP,软件工程走过了一条很长的路,我们现在和十年前相比在一个高得多的层次上来处理问题。我们不再需要担心机器指令,而是把系统看作一个由互相协作的对象构成的共生体。但无论如何,哪怕当前的方法也在懂得系统目标和实现它们之间有一条巨大的鸿沟。当前的方法使得初始的设计和实现很复杂而其演进也很难以管理。这是我们生活的世界的一个反讽,一方面唯一不变的就是变化,另一方面又要求越来越快的实现周期。
用演化的角度来看编程方法,过程性编程引入了功能抽象,OOP引入了对象抽象,而现在AOP引入了关注点抽象。当前,OOP是绝大多数新软件开发项目所选择的方法,OOP的力量来自于共同行为的建模。但是,就像我们即将要看到的,并且也许是你已经体会到的,它在处理那些分布于许多个通常还是没有关联的模块之间的行为上做得并不好。AOP填补了这一空白。
1.3 管理系统关注点
关注点,是一个为了满足系统整体目标必须被处理的特定需求或考虑。一个软件系统是一组关注点的实现。例如一个银行系统是以下这些关注点的实现:客户与账户管理、利息计算、银行间事务、自动柜员机事务、所有实体的保持、访问不同服务的认证、账单生成、客户关怀等等。除了系统关注点以外,软件系统还需要处理过程性关注点,例如可理解性、可维护性、可跟踪性和易于演进。
正像我们在本章开头看到的例子那样,关注点可以被分为两种类型:核心关注点,捕捉模块的中心功能,以及横切关注点,捕捉横跨多个模块的系统级外围需求。一个典型的企业应用需要处理的横切关注点例如:用户认证、记录日志、资源池、系统管理、性能、存储管理、数据保持、安全保密、多线程安全、事务完整性、错误检验、策略实施,以及更多。所有这些关注点都横切了数个子系统,例如日志记录影响到系统中每一个重要的模块,认证机制影响到所有需要访问控制的模块,存储管理影响到每一个有状态的业务对象。图1.1显示了这些关注点在一个典型应用当中通常如何相互作用。
[Figure 1.1]
这张图显示出系统中的实现模块各自都处理系统级和业务的关注点,表现出用当前的实现技术,系统由许多互相纠缠在一起的关注点组合而成,这样关注点的独立性就不能维持了。
1.3.1 辨认系统关注点
通过辨认出系统中核心性和横切性的关注点,我们可以集中力量处理互相分离的单独的关注点,并减少设计和实现的总体复杂度。为了做到这一点,第一步需要通过划分关注点来分解一组需求。图1.2使用光线通过棱镜的比喻说明了把需求分解为一组关注点的过程。我们让一道需求的光线通过一个关注点辨认的棱镜,然后可以看到各个关注点被分离了。虽然每个需求最初看上去都是一个单独的个体,通过应用关注点辨认的过程,我们可以分离出满足需求所需的独立的核心和横切关注点。
用另一种方式来看一个系统中关注点的分解,想象你把它们投影在一个关注点空间中,这是一个N维的空间,每一个关注点构成一个维度。图1.3显示了一个三个维度的关注点空间,三个维度分别是核心关注点:商业逻辑和两个横切关注点:日志记录和数据保持,这样一个对系统的观察方式的重要之处在于,它显示出了在一个多维空间当中,每个关注点是互相独立的,可以不影响其它部分而独立演化,例如,数据保持的需求从关系型数据库变成对象数据库,这不应该影响到业务逻辑和安全保密的需求。
分离和辨认一个系统中的关注点是开发软件系统当中的一个重要练习,无论所使用的是那种方法。一旦我们完成了这一点,我们就可以独立地来处理各个关注点,使得设计任务更加可管理。而当我们要把关注点实现到模块中去的时候,问题就出现了。理想情况是,我们的实现可以保持关注点之间的这种独立性,但是这并不会经常发生。
1.3.2 一维的解决方案
横切关注点的本质决定了它们要横跨多个模块,当前的实现技术倾向于把它们混合到各个独立的核心模块中去。为了说明这一点,图1.4显示了一个三维的关注点空间,然而实现这些关注点的代码是一个连续的调用流程,在这种意义上它是一维的,维度的这种不匹配导致了从关注点到代码的映射十分笨拙。
由于实现的空间是一维的,它的主要目标通常集中在实现核心关注点上,而横切关注点的实现就被混杂于其中。虽然在设计阶段,我们可以很自然地把单独的需求分离成互相独立的关注点,然而在实现阶段,当前的编程技术不能让我们保持这种分离。
1.3.3 重要的是模块化
有一个被广泛接受的假设,处理复杂性最好的方法就是简化它。在软件设计中,简化复杂系统最好的方法就是辨认出关键点然后把它们模块化。实际上,OOP方法被开发出来就是作为对模块化软件系统关注点的需求的反应。但是现实是,尽管OOP方法擅长模块化核心关注点,但它在模块化横切关注点方面却很欠缺。而开发AOP方法就是为了处理这一欠缺。AOP中模块化横切关注点的方法是,首先把系统中每个部分辨认为一个明确的角色,然后把每个角色用单独的模块实现,最后把每个模块与有限数量的其他模块松散地耦合。
在OOP中,核心模块可以通过接口(interfaces)松散耦合起来,但是对于横切关注点却没有什么简单的方法达到同样的目的。这是因为一个关注点是由两个部分实现的:服务器端部分和客户端部分。(这里的“服务器”和“客户”的用法是在经典OOP中的意义,分别表示提供某些服务的对象和使用某些服务的对象。不要和网络中的服务器和客户机混淆起来。)OOP使用类和接口能够很好地模块化服务器部分,然而,如果关注点属于横切性的,这时由对服务器的调用构成的客户端部分,将会分布于所有的客户端中。
好吧来看例子,OOP中对横切性关注点的典型实现方式是这样:一个用户认证模块使用抽象接口提供服务,接口的使用使得客户端与接口实现之间的耦合比较松散,通过接口使用认证服务的客户端对于接口的确切实现来说基本上是不关心的,对于所使用的实现做任何变更应该不需要对客户端做自身任何修改,同样,把一个认证实现替换成另一个,就仅仅是实例化正确实现的问题,这样,不需要对独立的客户端作什么修改就可以把认证的实现换成另一个。然而这样一种结构仍然需要每个客户端都有内嵌的代码来调用API,所有需要认证的模块中都会需要这些调用,而且是和它们各自的核心逻辑混合在一起。
图1.5显示了一个银行系统如何使用通常的技术来实现日志记录。尽管使用了一个设计得很好的日志模块,通过提供抽象接口隐藏了格式化和流输出日志消息的细节,然而每一个模块,无论是帐务模块,ATM模块还是数据库模块,还是全都需要调用日志API的代码,总体效果上说,在需要日志功能的模块和日志模块自身之间产生了不必要纠缠。每一个耦合在图上用一个灰色的箭头表示。
[Figure 1.5]
这里就是AOP出场的地方了。使用AOP,没有一个核心模块需要包含调用日志功能的代码,它们甚至不知道系统中有日志的存在。图1.6显示的是用AOP实现的与图1.5中相同的日志功能。现在日志逻辑聚集在日志模块和日志aspect(方面)中,客户端不再包含任何日志代码。现在横切性的日志需求被映射为仅仅一个模块——日志方面。有这样的模块化,横切性的日志需求的任何变化只会影响到这个日志方面,而客户端被完全隔离了。现在暂时不要去管AOP是如何做到这一点的,这将会在第1.6节中解释。
[Figure 1.6]
模块化横切性需求很重要,因而有数种技术在实现它。例如,Enterprise JavaBeans(EJB)简化了分布式服务器端应用的创建,它处理了一些横切性的关注点,例如安全保密,系统管理,性能,以及容器管理的持久性(CMP)。让我们来看看持久性这个横切关注点的EJB实现,组件开发人员专注于业务逻辑,而部署开发人员专注于部署的问题,例如把组件的数据影射到数据库里面。组件开发人员一般来说是不用关心存储问题的。EJB框架做到了持久关注点和业务逻辑之间的分离,使用的是一个部署描述文件,这是一个XML文件,指定了组件的域如何映射到数据库的列。类似地,这个框架通过管理部署描述文件中的说明,也分离了用户认证和事务管理等其他的横切关注点。
另一个操纵横切关注点的技术是动态代理(dynamic proxies),它提供了模块化代理这一设计模式的语言级支持。动态代理很复杂,超出了本书的范围,但无论如何,Java中这个自JDK 1.3以来就可用的新特性,提供了模块化横切关注点的一个合理的解决方案,只要关注点够简单就行。