这是一本关于AspectJ语言(一种程序设计语言)和面向方面程序设计(Aspect-Oriented Programming,AOP)的书籍,其中AOP对程序构建提出了新的思路。 使用面向对象程序设计语言编写过程式的程序是可能的(许多的情况足以证明这一点),但是这样做不能利用面向对象方法的全部优点。实际面向对象编程时需要改变对程序的思考方式。同样,当您开始改变思维方法时,AOP将会展现其强大的优势,希望在我们的帮助下,您可以比较快地做到这一点。 前言的剩余部分将会解释AOP产生的背景,并给出一个框架来说明AOP能做些什么。而在本书的后续部分,我们会讨论进行面向方面编程的有效途径——AspectJ语言,以及能够处理使用AspectJ所编写程序的工具——AJDT(AspectJ Development Tools)。 读者对象 我们假定本书的读者都熟悉Java程序设计语言,但在AspectJ方面没有或基本没有任何经验。掌握使用Eclipse有助于加快Java开发的学习但这不是学习本书所必需的。 从需求到代码 假定读者正在编写代码,对于程序要做什么有一些想法(我们这样希望),虽不一定在纸上,但至少在头脑中对于程序要支持的主要特征和功能的概念,以及如何将其实现到代码中,有了某种“设计”思路。这样,在设计层思想和源代码实现之间的理想映射中,问题域中唯一的概念、需求和实体都会同具体实现结构有一个清晰的一对一关系。 例如,如果程序需要处理货币数量,将“货币”这个概念同类Money建立一对一的关系会是一个比较好的想法。类Money封装了程序需要对货币进行描述的所有方面。如果程序需要面向顾客,可以建立“顾客”这一概念和类Customer之间的一对一映射。如果顾客有不同的类别,可能就需要建立类Customer层次的映射。在上面这两个例子中,很显然,实现的部分与设计层面上货币和顾客的概念相对应。 这种清晰明了的一对一映射对很多方面都有帮助。首先,程序变得更加易于理解。如果想要知道“程序如何处理X”,只需寻找X模块就能得到答案。其次,程序变得更加易于维护。在程序的生命周期中,设计层次上的概念和需求同要改变的单元有着紧密的联系。例如,如果要实现增加货币数量的功能,只需在类Money中添加一个multiply方法;如果要实现对一种新类型顾客的支持,只需在customer层次上添加一个新类;如果不再需要了解顾客头发的颜色,只需在类Customer中删除hairColor属性。 然而,对于一些设计层次上的需求,当使用面向对象语言时很难获得与实现结构之间清晰的一对一映射。比如有这样一个简单的需求:当顾客对象所展现的状态更新时需要通知观测结果,这显然不能由一个精确封装的模块来实现,但可以使用贯穿整个顾客层次的代码段来实现。观察类Customer的内部,会发现类似于程序清单0-1中的代码。同“观测通知”需求相关联的代码部分用粗体表示。 程序清单0-1 顾客更改通知 public class Customer { private Address address; private String lastName; private String firstName; private CustomerID id; private List listeners; public Customer(...) {...} // details omitted public void addListener(CustomerListener listener) { listeners.add(listener); } public void removeListener(CustomerListener listener) { listeners.remove(listener); } public Address getAddress() { return this.address; } public String getLastName() { return this.lastName; } public String setLastName(String name) { this.lastName = name; notifyListeners(this); } // etc } Customer类中含有添加和删除监听者(listener)的方法,和在每个状态改变操作后对notifyListeners方法的调用。顾客层次中其他的类也要确保在每次执行状态改变操作后均调用了notifyListeners方法。 此例没有采用精确的一对一映射,而是采用了一对多映射。一个设计层次上的需求变成了多段代码。在AOP团队中这种变化被描述为“离散化(scattering)”。实际的程序中有不少一对多映射的例子。设想一个在数据访问层上处理SQL异常的方法。当抛出异常SQLException时有一种方式可以描述下一步需要做些什么,但是其执行却要贯穿于整个代码中的try-catch-finally块。或者假设需要对一个资源集进行并发访问的控制,允许多个读操作和一个写操作。尽管这只代表设计层次上的需求,但可以证明在一整串的锁和对acquire以及release的调用上,这种控制会贯穿于所有的读操作和写操作。 无论何时,只要设计层概念和需求与具体实现结构之间存在一对多的映射关系,就会发现许多问题 : ● 难以理解需求的实现,并且难以进行相关的推导——这需要在源代码的多处进行寻找才能获取完整的描述。 ● 难以将需求的具体实现添加到代码库中——对细节的关注(人类似乎并不擅长)要求在每个需要的位置注意增加逻辑关系。因此,在每一个这样的位置都要正确地完成需求的实现。针对具体实现编写良好的测试程序也变得异常困难。 ● 难以维护实现——要改变计算货币数量的方式,只需访问Money类即可。而要改变数据访问层上处理异常SQLException的方式,就需确保找到所有处理SQLException的位置,然后进行统一且正确地更改。这种流程既费时又容易出错。 ● 当不需要某个需求时,难以删除代码库中相应的实现——这个问题等同于实现维护中存在的问题。 ● 难以将实现任务交给任何一个小组成员—— 让某人去写一个Money类显然很容易,然而,让他去写“观测通知”就要困难不少(因为这涉及到许多其他程序员所编写的类)。 ● 难以在其他系统上重用该实现—— 在其他系统上重用设计和设计所倚靠的基础服务是可能的,但许多具体的实现细节并不是以这些系统能接受的方式来模块化的。 一个程序只精确地完成一个任务的情况是很少见的。当要实现多个设计概念和需求且其中存在一对多的映射关系时,不可避免要采用抛硬币的方式决定。可以使用包含逻辑关系的程序模块(面向对象语言中的类)来处理多个概念和需求,也就是说,在设计和实现之间存在一种多对一的映射关系。程序清单0-1中的Customer类就展示了二对一的映射关系:单个模块(Customer类)同时实现了顾客的核心概念和“观测通知”需求。在AOP团队里这种实现被形象地描述为“杂乱的(tangling)”:不同的实现成分相互交错地整合在一个模块中。程序清单0-2的代码段就是基于这样的逻辑关系,在IBM的一个应用服务器内部实现了部分一对一设计概念—— 实体bean的钝化。 程序清单0-2 实体bean的钝化 try { if (!removed) entityBean.ejbPassivate(); setState(POOLED); } catch (RemoteException ex) { destroy(); throw ex; } finally { removed = false; beanPool.put(this); } 显然,很容易就可以理解其中的逻辑关系和具体的实现步骤。程序清单0-3给出了同样代码段的另一个版本,这段代码更接近于真实实现。其中举例说明了某些杂乱的问题(多对一映射),还有要处理的钝化过程中的核心逻辑关系:将异常交给分析引擎进行诊断的需求;输出统计钝化bean次数信息的需求;以及记录出入钝化方法轨迹的需求。 程序清单0-3 杂乱的实体bean钝化 try { if (!removed) entityBean.ejbPassivate(); setState(POOLED); } catch (RemoteException ex) { AnalysisEngine.processException( ex,"EntityBeanO.ejbPassivate", "2078",this); destroy(); throw ex; } finally { if (!removed && (statisticsCollector != null)) { statisticsCollector.recordPassivation(); } removed = false; beanPool.put(this); if (Logger.isEntryExitEnabled) { Logger.exit(tc, "passivate"); } } 当在设计层概念上,以及需求与实现结构之间出现多对一的映射关系时,将会出现很多问题: ● 代码难以理解—— 观察程序清单0-3,其中实体bean钝化的逻辑关系显然要比程序清单0-2复杂。 ● 实现难以维护—— 要维护源模块中任何一个概念或需求具体化后的实现,需涉及到所有的实现。这就意味着如果开发者要维护诸如Customer类,不仅要了解顾客需求,还需知道 “观测通知”这个需求。因为设计层的概念和需求倾向于整个单元的更改,而每一个又有其自身的变化方式,所以可以得出结论,含多对一映射关系的模块要比含一对一映射关系的模块需要更频繁地维护。任何想说“我只是想改变注释而已”的人都将会明白其中的原因。 ● 模块难以测试—— 例如,想要对Account类进行测试,但由于在其具体实现中和安全检测有杂乱的逻辑关系,安全管理系统就必不可少了。因此,测试实体bean钝化的逻辑关系需要分析引擎、跟踪器和集合统计模块。 ● 难以重用任何一个设计层的概念或需求的具体实现,因为它很大程度上依赖于当前与之相互关联的系统—— 例如,在一个使用不同安全解决方案(或根本不存在)的系统上重用Account类是很难的。 正如不会仅实现单个概念或需求一样,任何规格的程序也不会仅包含一个模块(如果真这么做了,在接触并学习AOP之前会遇到更大的问题)。让我们止步不前的并不是设计与需求同具体实现之间一对多或多对一的映射关系,而是多对多的映射关系。为了达到目标,我们从简单的一对一映射开始,却被复杂交错的杂乱阻断。这并不是读者的错 。面向对象的方法不提供这种工具,能清晰地将所有的概念和需求映射到模块实现结构中。而这个问题,正是AOP能够解决的。AOP尽可能的把问题接近一对一映射,也就是说,AOP具有模块性。 AOP如何工作 AOP提供了一种新型模块,称为aspect,在获得设计概念和需求的一对多实现并将其转化为一对一的过程中发挥了重要的作用。使用单个aspect,通过移除顾客层次内处理“观测通知”的代码并置入其自己的模块中,就可以实现“观测通知”的需求。还可以使用一个稳定的方法来处理整个数据访问层的SQLException异常。使用另一个aspect,可以实现多读操作单写操作对资源进行同步的逻辑关系。另外,可以对程序清单0-3中的错误分析、统计和跟踪实现模块化,从而使得实体bean钝化的模块变回到程序清单0-2的样子。另外,使用aspect实现银行应用的安全需求,不会像Account类的实现一样那么复杂。 前面所列出的可以用aspect进行模块化的特征,在AOP团队内部称为横切关系(crosscutting concern)。一个aspect模块会对程序执行过程中的多个时刻产生影响(如每次处理异常SQLException的时候)。第4章将对横切关系进行深入完整的定义。 丰富程序员的词汇 在面向对象的分析和设计中,我们学会了分析需求状态和查找名词及动词。其中名词变成了候选的类,而动词变成了这些类中候选的方法。AOP丰富了我们的词汇,它将模块化形容词和副词的实现变成了可能,如安全的业务事务,持久的记录,线程安全的类,账单式的事件。形容词和副词之所以存在,是因为它们定义了独立于名词和动词的概念。换句话说,形容词和副词描绘了可以应用到许多不同实体的概念,因而是横切关系的一种形式。当出现形容词或副词时,总能找到一个候选的aspect。我们不是建议把aspect起名为“安全的(Secured)”,但是对于安全的资源这样的需求就意味着“安全(Security)”aspect的出现。同样,需要获得缓存的结果时,意味着要使用“缓存(Caching)”aspect。正如所有的设计一样,这并不是简单的黑与白映射关系,每个案例都有它自身的优点,其他形容词和副词的实现技术就有子类和接口。AOP并不取代OOP,只是作了适当的补充。 小结 AOP讨论了如何提高程序的模块化,使设计概念和需求同具体实现结构之间的映射关系接近理想的一对一关系。为了达到这个目的,AOP提供了一种新型的模块,称为aspect,可以用来对宽泛的横切关系实现模块化。AOP代替不了OOP,但是,它建立在OOP之上并作了适当的补充。 本书的结构 本书分为3个部分。第Ⅰ部分将介绍AspectJ和AJDT,为使用AspectJ进行面向方面的程序设计提供一些初步的知识。第Ⅱ部分在第Ⅰ部分的基础上,将详细讲述AspectJ语言的语法和语义,并对从第Ⅰ部分过渡到第Ⅱ部分的过程中出现的难以理解的概念进行阐述。第Ⅲ部分将讨论如何将AspectJ引入项目和组织的策略,指导性地阐述了如何正确地使用AspectJ,并列举了许多例子。附录中有AspectJ的快速参考,一系列继续学习AspectJ和了解AOP的有用资源,还有关于使用Eclipse、AJDT和AspectJ进行开发的信息。