0%

官方系统架构设计师教程-ch18-面向方面的编程

面向方面的编程

随着计算机越来越广泛地应用于社会各个行业,应用软件的规模不断扩大,复杂度不断提高。传统的软件开发方法,如过程化程序设计、面向对象程序设计等已渐渐不能适应这种变化。近年来,一种新的程序开发方法——AOP(Aspect Oriented Programming,面向方面编程)引起了国内外的广泛关注,并被《MIT技术评论》杂志评为21世纪10种对经济和人类生活工作方式最具影响力的技术之一。

  • 切面相当于装饰器

方面编程的概念

AOP产生的背景

面向过程编程面临的问题

面向过程编程是一种自顶向下的编程方法,其实质是对软件进行功能性分解。它适用于小型软件系统,例如某一算法的实现。在大型应用系统中,自顶向下逐步求精的方法无论在系统体系结构的确立,系统的进化和维护,以及软件重用性方面都存在其不足之处。

传统面向对象编程面临的问题

传统的面向对象语言由于其良好的封装性、层次化性以及继承性等特性而取得了很大的成功,并且对象模型可以很好地映射到实际领域。但是,在软件的生命周期中,它存在以下不足之处。

(1)设计阶段,由于以类为单位组织建模,因此它不能全面地反映软件系统的需求。

(2)编码阶段,将数据和方法封装到类中的思想增强了数据的安全性和软件的模块化,但是有一些数据和方法是特定于应用的,因此这种编码阶段的封装减少了代码重用的可能性。

(3)维护阶段,由于类中夹杂了各种特定于应用的代码,使得维护人员难以理解代码。此外,完成某个特定需求的代码分散在各个类中,当这些代码需要改变时,很难把它们全部找到,这就给程序的健壮性带来了隐患。

由于上述这些问题的产生,需要一种新的程序设计方法从更高的层次上对软件系统进行抽象,将传统的按功能或按对象划分程序模块的方法转化为按系统特征划分程序模块,这就是AOP的基本思想。

AOP的产生

在1997年的欧洲面向对象编程大会(ECOOP97)上,施乐公司Palo Alto研究中心首席科学家、大不列颠哥伦比亚大学教授Gregor Kiczales等人首次提出了AOP的概念,此后每年的ECOOP上都有AOP相关的专题研讨会,各大公司、大学、研究机构纷纷投入人员进行研究。2001年3月15日,Palo Alto研究中心发布了首种支持AOP的语言AspectJ。

面向方面的原因

为了理解和完成一个复杂的程序,通常要把程序划分为若干较小的子程序。理想的划分准则已成为众多研究的题目——这些研究的目标对开发人员在程序的设计、发展、维护和更新方面有所帮助。

当一个程序按实现过程编写时,应用程序依照实现的行为和步骤模块化。当使用面向对象的方法时,程序的模块化组则基于类中封装的数据。两种情况下,某些操作较难实现模块化。我们称涉及到这些操作的代码是分散的。

代码分散现象

无论是使用面向对象程序设计还是其他方法,代码分散的问题与特定的程序设计语言没有关系,且其影响已经在大量的应用程序中表现出来。事实上,代码分散可能出现在任何编程环境——从J2SE或J2EE下的Java,到.NET下的C#,到其他语言。但对此现象最广泛的研究是用Java实现的。

例如,AspectJ小组分析了Tomcat服务器的容器。他们认识到,如果像URL模式匹配和XML分析这些操作在一个或两个类中完全模块化,其他操作会高度分散在引用程序中,例如日志功能和对使用者通信的管理。

关于代码分散的分析

知道了代码分散的出现,那么是否可以不同地组织类的结构或用其他方法设计程序来消除这个问题呢?

代码分散现象发生的主要原因与服务的可用方式和其使用方式的不同有关。一个类通过它的方法提供一个或多个服务。在同一个类中,聚集可用的服务是相对容易的。然而,一旦这些服务被若干个类所使用,将对这些方法的调用聚集在一起并重新构建这个应用程序会变得困难。因此,一个基本的服务在应用程序中到处被调用就没有什么奇怪的了。

代码分散现象在所有复杂程序中都会表现出来。然而,它的出现实际上依赖于一个具体的问题,代码分散问题很难去除。

应用程序中的代码分散减慢了程序的发展、维护和更新的速度。当若干个操作被分散,情况就会变得更复杂,因为代码包含了许多对多种关系的调用,这些关系逻辑上联系松散但需要结合在一起。

一个模块化的新因素

AOP主要的贡献在于在某一方面提供了一种融合代码的方式——否则这些代码会分散在整个应用程序中。

方面的定义:一个设计来用于捕捉应用程序横切面功能的程序单位。

一个方面通常描述为一个横切程序的结构。实际上,方面这个概念的发明者Gregor Kiczales提到,“AOP是用来捕捉一个横切的结构。”

方面的定义几乎和类一样普通。当对一个问题建模时,人们用类来表示对象(顾客、命令和供应者等)的种类,且每个对象包含适当的数据(属性)和过程(操作)。同样地,方面用于实现一个应用程序中的功能性(安全性、持续性、日志记录等等),而这些功能性要求同样的数据和处理。使用AOP时,一个应用程序包含各个类和方面。方面与类的不同在于它实现了横切程序的功能。在面向过程和面向对象的案例中,横切的功能就是那些遍及应用程序的代码。程序中包括类和方面意味着模块性可以在两个因素上实现:类实现基本的功能性(这个因素叫做结构性),方面实现横切的功能性(这个因素叫做可操作性)。

图18-1说明了方面在应用程序的代码优化上的作用。图18-1(a)表现了一个含有三个类的程序。水平线表示代码行相应的横切的功能性,如日志功能。这种功能横切整个应用程序,因其可影响所有类。图18-1(b)显示使用了方面处理日志功能的同样的程序(带阴影的矩形)。实现这个功能的代码已完全被这个方面所包含,而类则与这些代码分离了。用这种方法设计的程序比没用使用方面的程序容易编写、维护和改编。

图18-1 横切功能中方面的影响找不到图片(Image not found)
横切功能的综合

方面由两个部分组成:切入点和通知代码。

通知代码包括要执行的代码,切入点定义了程序中要执行的代码处的点。

显然,方面所包含的代码(或更准确地说,通知代码)依赖于你所要执行的操作。例如,若你想保证数据的持久性,需要在数据库编写保存数据的代码。虽然可以根据基本原理编写这些代码,但你极少会这样做。通常认为的良好的习惯做法是使用一个专门的API,例如Hibernate,通过这种类型的框架,这个方面的代码只是调用了API。这种工作方式意味着方面并不需要知道服务是怎样执行的,因而方面就与一个特定的执行独立了。

根据这种最优方法,一个方面只允许你整合一个贯穿程序的功能到程序中,这个功能利用一个专门的API执行。在图18-2中,方面PersistenceAspect使用Hibernate整合维持数据持续性的功能到类1和类3中。

图18-2 使用方面实现横切的数据持续性功能的综合找不到图片(Image not found)

严格地说,一个方面并不直接执行一个横切程序的功能,而是使用了一个专门的API去实现。但为了使这方面的知识容易理解,仍可以说一个方面执行了一个横切程序的功能。

非功能的服务和方面

大多数应用程序有两种考虑:商业的和非功能的。商业的考虑,也叫做功能上的需求,符合真实世界需要建模的行为。非功能的考虑,或非功能的需求,是附加的服务,这些服务是应用程序必须执行的——事实上,这是出于技术上的或系统级上的考虑。例如,在一个管理人力资源的应用程序中,添加和删除雇员的功能是出于商业上的考虑,而程序安全性和权限的问题是非功能的。

无论如何,在利用这种差别的时候要仔细,因为一个服务可以在一个程序中是非功能性的,但在另一个程序中却是功能性的。很多情况下,非功能性的服务会被遍及各处的商业层面上的代码调用。因此,非功能性的服务在AOP中会像方面一样实现,而商业的考虑则会向类一样实现。然而某些情况下,商业的考虑也可能横切程序——使其适合像方面一样实现。

依赖性的颠倒

在面向对象或程序化编程中,一旦程序从API使用一个技术服务,设备与服务之间的一种依赖性就建立了,每个程序对API外在的调用会发生一种联系。当API改变了或它的语义发展了,整个程序中对它服务的调用就必须作出改变。这种修改有可能是非常昂贵的——尤其是当API被用在程序中众多不同的地方时。

另外,要使用API还需要理解它的主要原理。要知道应调用什么方法,应按什么顺序调用,应传递哪些参数等。非功能的服务要被包含进每个开发它的新程序中。所以,即使API只开发一次,它可能要包含进许多不同的应用程序中。

通过使用AOP,程序的开发者并不需要担心非功能的服务。方面开发者除了编写提供服务的代码外,还要管理程序中服务的融合。方面开发者的优势在于,专门的方面开发者比一般的程序开发者对服务有着更好的了解,而一般的程序开发者只是API的使用者。特别地,方面开发者能确定通过实现服务使用方式的某些约束,使服务的融合是合适的。

AOP技术

AOP可以说是OOP(Object-Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承和多态性等概念来建立一种对象层次结构,用以模拟公共行为的一个集合。当需要为分散的对象引入公共行为时,OOP则显得无能为力。也就是说,OOP允许定义从上到下的关系,但并不适合定义从左到右的关系。例如日志功能,日志代码往往水平地散布在所有对象层次中,而与它所散布到的对象的核心功能毫无关系。对于其他类型的代码,如安全性、异常处理和透明的持续性也是如此。这种散布在各处的无关的代码被称为横切(cross-cutting)代码,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。

而AOP技术则恰恰相反,它利用一种称为“横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为Aspect,即方面。所谓“方面”,简单地说,就是将那些与业务无关,却被业务模块所共同调用的逻辑或责任封装起来,以减少系统的重复代码,降低模块间的耦合度,并有利于未来的可操作性和可维护性。AOP代表的是一个横向的关系,如果说“对象”是一个空心的圆柱体,其中封装的是对象的属性和行为,那么面向方面编程的方法,就仿佛一把利刃,将这些空心圆柱体剖开,以获得其内部的消息。而剖开的切面,也就是所谓的“方面”了。然后它又以巧夺天工的妙手将这些剖开的切面复原,不留痕迹。

使用“横切”技术,AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,它们经常发生在核心关注点的多处,而各处都基本相似,例如权限认证、日志、事务处理。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。正如Avanade公司的高级方案构架师Adam Magee所说,AOP的核心思想就是“将应用程序中的商业逻辑同对其提供支持的通用服务进行分离。”

实现AOP的技术,主要分为两大类:一是采用动态代理技术,利用截取消息的方式,对该消息进行装饰,以取代原有对象行为的执行;二是采用静态织入的方式,引入特定的语法创建“方面”,从而使得编译器可以在编译期间织入有关“方面”的代码。然而殊途同归,实现AOP的技术特性却是相同的。

(1)join point(连接点):是程序执行中的一个精确执行点,例如类中的一个方法。它是一个抽象的概念,在实现AOP时,并不需要去定义一个join point。

(2)point cut(切入点):本质上是一个捕获连接点的结构。在AOP中,可以定义一个point cut,来捕获相关方法的调用。

(3)advice(通知):是point cut的执行代码,是执行“方面”的具体逻辑。

(4)aspect(方面):point cut和advice结合起来就是aspect,它类似于OOP中定义的一个类,但它代表的更多的是对象间横向的关系。

(5)introduce(引入):为对象引入附加的方法或属性,从而达到修改对象结构的目的。有的AOP工具又将其称为mixing。

上述的技术特性组成了基本的AOP技术,大多数AOP工具均实现了这些技术。它们也可以是研究AOP技术的基本术语。

AOP特性

衡量软件质量高低的要素主要包括可靠性、可扩展性、可重用性、兼容性以及易用性、易维护性等。AOP作为一种程序设计方法学,关注于提高软件的抽象程度和模块性,从而在很大程度上改善了软件的可扩展性、重用性、易理解性和易维护性,并由此提高影响软件质量的其他因素。下面通过对OOP和AOP在提高软件可扩展性、可重用性和易理解性、易维护性等方面的能力比较来阐述AOP特性。

(1)可扩展性:指软件系统在需求更改时程序的易更改能力。OOP主要通过提供继承和重载机制来提高软件的可扩展性,因此它的扩展性体现在类一级。AOP提供系统的扩展机制,通过扩展Aspect(AspectJ支持Aspect的继承机制)或增加Aspect,系统相关的各个部分都随之产生变化。由此带来的另一好处是在软件测试中,通过屏蔽某些Aspect,可以大大简化软件的测试复杂度,提高测试精度。

(2)可重用性:是指某个应用系统中的元素被应用到其他系统的能力。OOP的类机制作为一种抽象数据类型,提供了比过程化更好的重用性。泛化机制也使可重用性得到很大提高。OOP所提供的重用性对非特定于系统的功能模块有很好的支持,如对于堆栈的操作或窗口机制的实现等。但在特定于系统的功能模块中,一个类通常包含很多应用系统相关的数据及对其的操作,此时类的重用性变得十分困难。此外,OOP的重用性也限于类一级,对于不能封装成类的元素,如异常处理等,很难实现有效的重用。AOP中的系统模块包括系统组件和影响这些组件的特性,通过将实现基本功能的组件和特定于应用的系统特性分离,使得组件(包括类或者函数)的重用性得到提高,并使不能封装为类或函数的系统元素(Aspect)的重用成为可能。

(3)易理解性和易维护性:是影响软件质量的内在因素,它对软件开发人员和维护人员产生影响。在OOP中,类机制的引入使其具有比过程化编程更好的模块性,因此也更易于被程序员理解和维护。但是如上所述的代码缠结问题的存在,使OOP技术在易理解性和易维护性方面都难有更大的提高。Kiczales经过统计发现:“如果一个他人写的程序有37处需要改动,对于一个最优秀的软件开发人员,也大概只能找到35个”。而对于AOP,对一个aspect的修改可以通过联结器影响到系统相关的各个部分,从而大大提高了系统的易维护性。另外,对系统特征的模块化封装无疑也能提高程序的易理解性。

AOP程序设计

AOP程序结构

基于AOP的应用程序结构与传统高级语言的应用程序结构基本类似。传统的高级语言系统实现由以下三部分组成。

(1)一种编程语言。

(2)特定于这种语言的编译器。

(3)利用这种语言编写的应用程序。

基于AOP的系统实现也有以上三个主要部分,但由于AOP中有了动态aspect的概念,因此可进一步细化为如下部分。

(1)一种组件语言,一种或多种aspect语言。

(2)一个用来合并两者的aspect编织器(weaver)。

(3)利用组件语言实现的系统组件,利用aspect实现的aspect组件。

AOP的程序设计步骤

AOP应用程序包括以下三个主要的开发步骤。

(1)将系统需求进行功能性分解,区分出普通关注点以及横切关注点,确定哪些功能是组件语言必须实现的,哪些功能可以以aspect的形式动态加入到系统组件中。

(2)单独完成每一个关注点的编码和实现,构造系统组件和系统aspect。这里的系统组件,是实现该系统的基本模块,对OOP语言,这些组件可以是类;对于过程化程序设计语言,这些组件可以是各种函数和API。系统aspect是指用AOP语言实现的将横切关注点封装成的独立的模块单元。

(3)用联结器指定的重组规则,将组件代码和aspect代码进行组合,形成最终系统。为达到此目的,应用程序需要利用或创造一种专门指定规则的语言,用它来组合不同应用程序片断。这种用来指定联结规则的语言可以是一种已有编程语言的扩展,也可以是一种完全不同的全新语言。将以上过程用图18-3的形式来表示,该图中将系统需求看作一束光线,需求光束通过可标识关注点的棱镜将每个关注点区分开,形成单独关注点的实现,最后通过另一个Weaver棱镜将这些关注点整合,形成最终的应用程序。

图18-3 AOP系统开发过程示意图找不到图片(Image not found)

AOP的优势

面向方面的技术具有很多潜在的优势,它为在系统中详细指定并封装横切点提供了方法。随着它们的发展,允许我们更好地进行系统维护。AOP还将使我们对现存系统以一种有组织的方式增加新的特点。表达及结构方面的提高允许我们保持系统运行更长的时间,并且不会带来完全改写的开销就可以增量地对其维护。

AOP还是质量专业人员工具箱的利器。使用AOP语言,可以自动测试应用程序代码而不会对代码带来干扰。这将消除可能的代码错误。

在理解AOSD(方面面向软件设计)全部潜能中我们还处于一个初始阶段。显然,这项技术为保证未来的探索与实验提供了足够多的优点。距离每日使用AOP语言进行应用程序开发还有多远?这取决于我们向谁询问这个问题。

现在已经看到了一些优点,下面来看一些关于AOSD的风险,以及将其引入软件开发主流所需要的东西。

质量及风险

基于从质量的角度所做出的考察以及所完成的对AspectJ的探索,我已经看到了伴随着优点所带来的潜在风险。下面将讨论三个问题,以说明随着AOSD越来越普遍所带来的我们需要面对的关于质量的问题。

(1)如何修正我们的过程来适合AOP。最有效的探测软件缺陷的技术之一是通过代码检查及复审。在复审中,一组程序员评论代码,以决定其是否满足了需求。在面向对象程序设计中,可以对类或一组相关类进行复审,并进行推论。可以查看代码并确定其是否正确处理了意料之外的事件,是否具有逻辑上的缺陷等。在OO系统中,每个类完全封装了特定概念的数据以及行为。

但是,在AOP中,仅仅通过代码查看,我们不再能够进行推论。我们并不知道代码是否被来自某些方面的通知所增长,或是完全被这种通知所取代。为了能够对应用程序代码做推断,我们必须能够查看来自每个类的代码,以及能够影响这个类行为的任何一个方面的代码。但是,也许这些方面还没有被编写出来。如果这样,那么当我们孤立地考虑它的时候,我们实际上能够对这个应用程序类的代码行为理解多少呢?

实际上,考虑AOP代码正确性的方法与我们考虑面向对象的程序设计代码是相反。在OOP中,我们自内向外:我们考虑一个类,对它的上下文环境做假设,然后通过孤立的以及按照它如何与其他类交互的两种方式推断它的正确性。在AOP中,需要从外向内来看,并确定在可能的连接点上每个方面的效果。确定如何才能正确推断AOP,以及开发适宜的技术和工具来帮助我们是一个值得研究的领域。

(2)测试工具以及技术的开发,特别是单元测试。由于代码可以被某一个方面所改变,当一个可以完美运行的单元测试的类被集成入一个AOP系统中时,可能会出现完全不同的运行状况。下面的例子说明了这一点。

例如,堆栈是一种数据结构,被用来以后进先出的模式增加或移除条目。如果向堆栈压入数据2、4、6,然后弹出栈两次,将按照顺序得到6与4。可以很直观地写出对堆栈类的单元测试,并能够很好地保证实现是正确的。但当用AspectJ实现了一个简单的改变——对每个条目做增一操作时,向栈上压入2、4、6,然后从栈顶弹出两个元素。单元测试的代码并没有改变,但是行为改变了。不再是6和4,而是变成了7和5。

这是一个很小的例子,在真实环境中不太可能发生,但是它显示了一个恶意的程序员可以很容易地导致许多损害。即使我们忽略这种恶意的程序员,由于我们所做出的改变存在着许多副作用,许多错误仍然可能发生。要保证一个为非常有效推断所实现的方面不会对现存程序功能带来多余的效果是非常困难的。

(3)测试过程本身。一旦我们有了一组工具及技术,如何修改我们的测试过程以有效地使用它们并且能够支持我们整体的开发目标?虽然这个问题也许并不是一个主要的问题,我仍然相信在我们可以真正地对采用方面所构件的软件进行很好的测试以前需要解决它。

对AOSD采用的其他障碍

质量问题也许是对AOSD方法采用的最大阻碍,但是它们并不是唯一的。AOSD是一种新的范例。正如其他范例一样,当它们刚刚出现时(例如面向对象的软件开发),由于所包含的学习曲线,需要经历一段时间才能被广泛采用。首先,我们需要学习基本技术以及结构,然后是高级技术,再然后是如何更好地应用技术以及什么时候它们才是最适合的。

工具在AOP中具有很重要的地位。除了编译器以及编辑器之外,我们需要能够帮助我们推断系统,确定潜在横切关注点,以及能够帮助我们对所存在的方面进行测试的工具。例如在UML中描述方面,我们的工具必须发展以支持这些方法。

此外,其他类似于AOP的范例也正在出现中。例如,关注点范例的多维分离,已在IBM研究院处于发展中(http://www.alphaworks.ibm.com/tech/hyperj)。任何对新范例的使用都是有风险的,直至对你所使用语言的标准实现被建立起来。例如,AspectJ是一个仍然发展的AOP实现。风险是,也许你开发了合并入横切点的软件,你的实现方案或者将不再被支持,或者要做很大的改动。

向构建软件的更好方法前进

很显然,我们具有一种能够为使AOP对日常应用可行化而开发工具与过程的方法。但是,这里讨论的任何问题不代表不可克服的困难。当为AOP开发出经得起考验的一组工具与流程时,可以找到比今天所做的更好的构建软件的方法。

正如Barry M. Boehm所述的关于敏捷流程,我们必须小心的采用AOP。无论是作为早期应用者或是等待这种技术成为主流,都需要确保软件投资者在今天或是未来能够提供可接受的回报,这是非常好的商业判断力。

当前的AOP技术

当前,各种AOP技术层出不穷,其中相当成熟完善适用于商业开发的AOP技术主要包括AspectJ、AspectWerkz、JBoss AOP和Spring AOP,这些皆适合用于商业开发中的开源项目。AOP是一种概念,不同的技术可以有不同的实现。

在语法方面,AspectWerkz、JBoss AOP和Spring AOP都在没有改变Java语言语法的情况下加入了方面语义,而AspectJ则对Java语言进行了扩展。

在声明方式方面,AspectJ在代码中对方面进行声明。AspectWerkz和JBossAOP支持用元数据对Java代码进行注释,或者在独立的XML文件中对方面进行声明。在SpringAOP中,则完全用XML对方面进行声明,比起JBossAOP和AspectWerkz,SpringAOP提供了更加精细的配置。

在性能方面,AspectJ通过编译时对目标二进制类的增强获得面向方面能力,所以在编译时会带来开销,运行时可获得更快的速度。JBossAOP和SpringAOP基于拦截技术则在运行时有更多的工作要做,对比之下,AspectJ的构建时开销最多,AspectWerkz次之,JBossAOP再次,SpringAOP没有构建时开销。

下面将重点介绍AspectJ和SpringAOP的概念构造与实践。

AspectJ

AspectJ概述

AspectJ既是一个语言规范,又是一个AOP语言实现。语言规范部分定义了多种语言构造以及它们支持面向方面范型的方式;语言实现部分则提供了编译、调试及从代码生成文档的工具。

AspectJ的语言构造是从Java语言中扩展而来的,因此所有合法的Java程序也都是合法的AspectJ程序。AspectJ编译器生成的是符合Java字节码规范的.class文件,这使得所有符合规范的Java虚拟机都可以解释、执行其所生成的代码。通过选择Java为基础语言,AspectJ继承了Java的所有优点并使Java程序员能够比较容易地上手。

AspectJ还提供了许多有用的工具。它有一个方面编织器(以编译器的方式)、一个调试器、文档生成工具以及一个独立的可用来以可视化的方式观察通知是如何切入系统各部分的方面浏览器。另外,AspectJ还提供了与流行IDE的集成,如Sun公司的Forte、Borland公司的JBuilder以及Emacs等,这使得AspectJ成为一个很有用的AOP实现,特别是对Java开发者而言。

AspectJ语言概念和构造

连接点

连接点是AspectJ中的一个重要概念,它是程序执行过程中明确定义的点。连接点可能定义在方法调用、条件检测、循环的开始或是赋值动作处。连接点有一个与之相关联的上下文。例如,一个方法调用连接点的上下文可能会包含一个目标对象及调用参数等。

虽然程序执行过程中所有可以确认的点都可以是连接点,但并不是每个点都是有用的。在AspectJ中,有下列可用的连接点。

(1)方法的调用(call)和执行(execution)。

(2)构造器(constructor)的调用和执行。

(3)对属性(field)的读/写访问。

(4)异常处理的执行。

(5)对象和类的初始化执行。

AspectJ中没有提供在像if条件检查或for循环这样细粒度语言构造上的连接点。

切入点

切入点是用来指明所需连接点的程序构造,可以用它来指明一系列的连接点。同时,它还可以为在连接点上执行的通知提供上下文信息。例如:

图片详情找不到图片(Image not found)

其中,pointcut关键字表明其后是一个命名的切入点的声明。接着,callSayMessagc()是切入点的名字,这与方法声明类似。其后的空括号表明此切入点不需要上下文信息。

再往后,call (public static void HelloWorld.say(…)) 捕获所需的连接点。call表明此切入点捕获对指定方法的调用,而不是方法的执行或是别的什么。public static void HelloWorld.say(…) 是将会产生影响的方法的签名。void是说所捕获的方法必须要有一个void返回类型。HelloWorld.say指明将要捕获的方法的类和名字。这里,我们指定HelloWorld类;say使用通配符,来说明要捕获的方法应以say开始。最后,(…)指明了将要捕获的方法的参数列表。这里使用了“…”,表示任何形式的参数列表都在考虑范围之内。

现在,你已经知道了如何指定切入点来捕获连接点,下面再来看一下其他的切入点类型。

1)方法调用和构造器调用切入点

方法调用和构造器切入点捕获执行中准备好了方法参数后而尚未执行方法本身时的那个点。它们的形式是call(方法或是构造器的签名)。

2)方法执行和构造器执行切入点

方法执行和构造器执行切入点捕获方法的执行,与调用切入点相比,执行切入点体现在方法和构造器本身。其形式为execution(方法或是构造器的签名)。

3)属性访问切入点

属性访问切入点捕获对一个类中属性的读写访问。可以捕获所有对System类中的out属性的访问,如System.out;也可以仅捕获读访问或写访问。举个例子来说,可以捕获对MyClass的属性x的写访问,其形式为MyClass.x=5。读访问切入点的形式为get(FieldSignature);写访问切入点的形式则为set(FieldSignature)。其中FieldSignature可以用与调用或执行切入点里的McthodOrConstructor同样的方式使用通配符。

4)异常处理切入点

异常处理切入点捕获特定类型异常处理的执行,其形式为handler(ExceptionTypePattern)。

5)类初始化切入点

类初始化切入点捕获类初始化部分中静态部分的执行,这里静态部分是指类定义中Static代码块中指定的代码。其形式为staticinitialization(TypePattarn)。

6)基于语法结构的切入点

基于语法结构的切入点捕获一个类或方法中所有语法结构里的连接点。捕获类(包括内部类)中的语法结构连接点的切入点形式为within(TypePattern),捕获类方法或类构造器中的语法结构连接点的切入点形式为withincode(Method-OrConstructor-Signature)。

7)基于控制流的切入点

基于控制流的切入点捕获所有指定范围的控制流(程序的指令流)内的连接点。例如,在某个执行过程里,方法a调用方法b,方法b就在方法a的控制流里。通过使用基于控制流的切入点,可以捕获由于一个方法调用而引发的所有方法调用、属性访问及异常处理等。这种类型的切入点可捕获在其控制流内的其他切入点,如果包括其自身,形式为cflow(pointcut);如果不包括其自身,则形式为cflowbelow(Pointcut)。

8)基于当前对象、目标对象及参数类型的切入点

此类切入点可捕获定义在对象自身、目标对象或参数上的连接点。它是唯一可以在连接点上取得上下文的语言构造,捕获基于当前对象的连接点的切入点形式为this(TypePattern或Objecctldentifier),捕获某个目标对象的连接点的切入点形式为target(TypePattern or ObjectIdentifier),基于参数的切入点形式为args (TypePattern orObjectIdentifier,…)。

9)条件测试切入点

这种切入点基于某种条件测试捕获连接点,其形式为if(BooleanExpression)。

通知

通知指定当到达特定切入点处应执行的代码。AspectJ提供了三种把通知关联到连接点的方式:before、after及around。before通知在连接点的前面运行;after通知在连接点的后面运行。对于after通知而言,还可以指定是在正常返回后运行还是在抛出异常后运行,或者也可以是两种情况下都运行。around通知包在连接点的外面,并有权决定是否运行此连接点,还可以在此处修改连接点上下文环境。

方面

方面是AspectJ的模块单元,其地位就像是Java里的类。它把切入点和通知包在一起。和类相似,方面也可以包含方法和属性、从其他类或方面扩展以及实现接口等。与类不同的是,不能用new来建立一个方面实例。

AspectJ允许在类中声明切入点,但在类中只能声明static的切入点。而且AspectJ不允许类里包含通知,只有方面可以包含通知。

方面可以标记其自身和任何切入点为抽象的(abstract)。抽象的切入点,其概念与抽象类相似,允许把细节实现推迟到派生方面里。一个具体的方面可以从抽象的方面扩展而来,它要提供抽象方面里切入点的具体定义。

AspectJ实践

AspectJ也许是已知最好的,并且应用最广泛的AOP实现。

图18-4描述了一种可以进行系统修正的方法。金融系统具有一个接口以及数个方法以更新雇员金融数据。方法名均以单词update开头(例如,updateFederalTaxlnfo),并且每个金融更新均以雇员对象做实参。雇员个人信息也通过雇员对象,使用图10-5所示的方法做更新。

我们的任务是,每一次当调用任何更新函数,或是更新成功完成后,写入一个日志消息。为了简单起见,我们说我们向标准输出打印了一个日志消息。在实际系统中,我们将写入一个日志文件。下面将通过三个步骤采用AspectJ实现我们的解决方案。

确定在代码中需要插入日志代码的位置。这被称作在AspectJ中定义连接点,编写日志代码,编译新代码并将其编织入系统中。下面详细描述每一个步骤。

定义连接点

一个连接点是在代码中被良好定义的,我们所关注的应用程序横切的点。典型的,对每一个关注点存在着许多连接点。如果仅有一两个,通过很少的努力,就可以手工改写代码。

图18-4 与更新雇员信息相关的类找不到图片(Image not found)

在AspectJ中,通过将连接点分组为切点对其进行定义(AspectJ的语法十分丰富,我们将试图在此对其进行完整的描述)。初始,定义两个切点,分别将雇员类及IEmployeeFinance组件中的连接点分组。下列代码定义了这两个切点。

图片详情找不到图片(Image not found)
图片详情找不到图片(Image not found)

第一个切点称作employeeUpdates,描述了我们调用雇员对象中以字符串update开头,以字符串Info结尾,且无实参的方法的连接点位置,它还通过target指示器明确指定了在雇员类中定义的方法。第二个切点employeeFinanceUpdates,描述了所有以update开头,以Info结尾的,具有一个Employee类型实参的方法的调用点。合起来,这两个切点定义了所有我们关注的连接点。如果要为雇员类或IEmployeeFinance组件增加更多的更新方法,只要保持同样的命名规则,对它们的调用会自动被包含于切点中。这意味着当每次增加更新方法时,不需要特意地去包含日志代码。

编写日志代码

实现日志的代码与Java中其他任何方法都很相似,但是被置于一个称作方面的新风格中。方面是用来对与某一特定关注相关联的代码进行封装的一种机制。对雇主数据变更日志的方面实现如下所示。

图片详情找不到图片(Image not found)

首先,注意到方面的结构与Java中的类结构很相似。典型地,方面被置于它独有的文件中,正如Java的类一样。虽然通常的方法是在方面代码中包含以往定义的切点,但是也可以将它们更紧密地包括在含有切点的代码中。

在切点之后,有一段与常规Java代码中方法相似的代码,这被称作AspectJ中的通知。存在着三种不同类型的通知:before、after和around。它们分别在连接点之前、之后,或是取代连接点而执行。还存在着许多可以使用的变种以定制你自己的通知。在我们的例子中,选择连接点返回中的更新方法之后立即运行日志。还要注意到,我们通过在冒号之后的通知头中立即分别对它们命名,以及通过逻辑“或”的方式组合两个切点。因为每个切点都有一个雇员参数,因此可以很容易地完成这项工作。

随着雇员名字,通知中的两条语句打印出了雇员信息被改变的事实。既然受到影响的雇员对象作为实参被传递给通知,那么这很容易安排。第二条语句指明了通知被执行的确切连接点,并且应用了AspectJ的JoinPoint类。只要通知执行,仅存在一个被thisJoinPoint引用的关联连接点。

编译及测试

现在,已经编写了日志代码,接下来需要编译并将其集成入现存的系统。为了方便起见,已经实现了两个类:Employee和EmployeeFinance。我们还拥有一个具有主函数的简单测试类,如下所示。

图片详情找不到图片(Image not found)

这个代码不需要任何AOP实现就可以很好地运行。为了我们的例子,所有更新方法的函数体仅包含一个打印语句。当运行这个例子时,得到如下输出:

图片详情找不到图片(Image not found)

为了将我们的方面合并入系统,我们向项目中增加了方面的源代码,并且采用AspectJ编译器,ajc进行编译。编译器接受每一个方面,并建立包含通知代码的类文件。然后,在这些类文件中对适当方法的调用被编织入原始应用程序代码。在当前AspectJ的发行版中,这种编织在Java字节码级别发生,因此不存在可以进行查阅以对最终代码进行审查的中间源文件。但是,如果你很好奇,可以对Java字节码进行反编译。

在开发中,我使用Eclipse,而AspectJ插件保证采用正确的实参调用编译器。一旦利用AspectJ编译器对项目进行了编译,得到如下输出:

图片详情找不到图片(Image not found)

现在,我们知道哪个雇员信息被改变,以及改变是在哪里发生的。当然,日志可以更精细,但是基本的方法没有变化。

Spring AOP

Spring AOP概述

在有很多的开放源代码和专有的J2EE Framework时,Spring Framework能够脱颖而出,并且一枝独秀,我们应该相信Spring是独特的。Spring定位的领域是许多其他流行的Framework不具有的,Spring是全面的和模块化的,引入了方面(Aspect)提供一种新的方法来管理你的业务对象。Spring有分层的体系结构,这意味着你能选择使用它的任何部分,它的架构仍然是内在稳定的。

Spring的架构性,能有效地组织你的中间层对象,无论你是否选择使用了EJB。如果你仅仅使用了Struts或其他的包含了J2EE特有APIs的framework,你会发现Spring关注了遗留下来的问题,Spring能消除在许多工程上对Singleton的过多使用。Spring能够消除各类属性文件的定制,在Spring应用中大多数业务对象没有依赖于Spring,创建的应用程序更易于单元测试。

Spring为已建立的企业级应用提供了一个轻量级的解决方案,这个方案包括声明式事务管理,通过RMI或webservices远程访问业务逻辑,mail支持工具以及数据库持久化的多种选择。Spring还提供了一个MVC应用框架、可以透明地把AOP集成到你的软件中的途径和一个优秀的异常处理体系,包括自动从Spring特有的异常体系中映射。

Spring是潜在的一站式解决方案,定位于与典型应用相关的大部分基础结构。同时,Spring也是组件化的,允许使用它的部分组件而不需牵涉其他部分。可以使用Bean容器,在前台展现层使用Struts;还可以只使用Hibernate集成部分或是JDBC抽象层。Spring是无侵入性的,意味着根据实际使用的范围,应用对框架的依赖几乎没有或是绝对最小化的。

Spring包含许多功能和特性,并被很好地组织在图18-5所示的7个模块中。

图18-5 Spring框架图找不到图片(Image not found)

Core包是框架的基础部分,并提供依赖注入特性来管理Bean容器功能。这里的基础概念是BeanFactory,它提供Factory模式来消除对程序性单例的需要,并允许从程序逻辑中分离出依赖关系的配置和描述。

构建于Beans包上的Context包,提供了一种框架式的Bean访问方式,有些像JNDI注册。Context包的特性得自Beans包,并添加了文本消息的发送,通过资源串、事件传播、资源装载的方式和Context的透明创建,如通过Servlet容器。

DAO包提供了JDBC的抽象层,它可消除冗长的JDBC编码和解析数据库厂商特有的错误代码。该包也提供了一种方法实现编程性和声明性事务管理,不仅仅是针对实现特定接口的类,而且对所有的POJO。

ORM包为流行的关系——对象映射APIs提供了集成层,包括JDO、Hibernate和iBatis。通过ORM包,可与所有Spring提供的其他特性相结合来使用这些对象/关系映射,如前边提到的简单声明性事务管理。

Spring的AOP包提供与AOP联盟兼容的面向方面编程实现,允许定义,如方法拦截器和切点,来干净地给从逻辑上说应该被分离的功能实现代码解析。使用源码级的元数据功能,可将各种行为信息合并到你的代码中。

Spring的Web包提供了基本的面向Web的综合特性,如Multipart功能,使用Servlet监听器的Context的初始化和面向Web的application Context。当与WebWork或Struts一起使用Spring时,这个包使Spring可与其他框架结合。

Spring的Web MVC包提供了面向Web应用的Model-View-Controller实现。Spring的MVC实现不仅仅是一种实现,它提供了一种domain model代码和web form的清晰分离,这使用户可使用Spring框架的所有其他特性,如校验。

Spring语言概念和构造

前面提到,AOP提供从另一个角度来考虑程序结构以完善面向对象编程。面向对象将应用程序分解成各个层次的对象,而AOP将程序分解成各个方面或者说关注点。这使得可以模块化诸如事务管理等这些横切多个对象的关注点,称作横切关注点。

Spring的一个关键组件就是AOP框架。Spring IoC容器(BeanFactory和Application-Context)并不依赖于AOP,这意味着如果不需要,可以不使用AOP。AOP完善了Spring IoC,使之成为一个有效的中间件解决方案。

AOP在Spring中的使用

(1)提供声明式企业服务,特别是作为EJB声明式服务的替代品。这些服务中最重要的是声明式事务管理,这个服务建立在Spring的事务管理抽象之上。

(2)允许用户实现自定义的方面,用AOP完善他们的OOP的使用。这样,可以把Spring AOP看作是对Spring的补充,它使得Spring不需要EJB就能提供声明式事务管理;或者使用Spring AOP框架的全部功能来实现自定义的方面。

Spring AOP的功能

Spring AOP用纯Java实现,不需要特别的编译过程,区别于AspectJ的实现。Spring AOP不需要控制类装载器,因此适用于J2EE Web容器或应用服务器。

Spring目前支持拦截方法调用。成员变量拦截器没有实现,虽然加入成员变量拦截器支持并不破坏Spring AOP核心API。Spring提供代表切入点或各种通知类型的类。Spring使用术语advisor来表示代表方面的对象,它包含一个通知和一个指定特定连接点的切入点。各种通知类型有MethodInterceptor,来自AOP联盟的拦截器APD和定义在org.springframework.aop包中的通知接口。所有通知必须实现org.aopalliance.aop.Advice标签接口。取出就可使用的通知有MethodInterceptor、ThrowsAdvice、BeforeAdvice和AfterReturningAdvice。

Spring实现AOP的途径不同于其他大部分AOP框架,它的目标不是提供及其完善的AOP实现(虽然Spring AOP非常强大);而是提供一个和Spring IoC紧密整合的AOP实现,帮助解决企业应用中的常见问题。因此,例如Spring AOP的功能通常是和Spring IoC容器联合使用的。AOP通知是用普通的bean定义语法来定义的(虽然可以使用autoproxying功能)。通知和切入点本身由Spring IoC管理,这是一个重要的其他AOP实现的区别。有些是使用Spring AOP无法容易或高效地实现,例如通知非常细粒度的对象。这种情况AspectJ可能是最合适的选择。但是,我们的经验是Spring针对J2EE应用中大部分能用AOP解决的问题提供了一个优秀的解决方案。

Spring AOP的重要概念

前面已经提到了AOP的重要概念,下面介绍在Spring中的定义和实现。

(1)方面:一个关注点的模块化,这个关注点的实现可能横切另外多个对象。事务管理是J2EE应用中一个很好的横切关注点例子。方面用Spring的Advisor或拦截器实现。

(2)连接点:程序执行过程中明确的点,如方法的调用或特定的异常被抛出。

(3)通知:在特定的连接点,AOP框架执行的动作。各种类型的通知包括around、before和throws通知。许多AOP框架包括Spring都是以拦截器做通知模型,维护一个“围绕”连接点的拦截器链。

(4)切入点:指定一个通知将被引发的一系列连接点的集合。AOP框架必须允许开发者指定切入点。例如,使用正则表达式。

(5)引入:添加方法或字段到被通知的类。Spring允许引入新的接口到任何被通知的对象。例如,可以使用一个引入使任何对象实现IsModified接口,来简化缓存。

(6)目标对象:包含连接点的对象。也被称作被通知或被代理对象。

(7)AOP代理:AOP框架创建的对象,包含通知。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理。

(8)织入:组装方面来创建一个被通知对象。这可以在编译时完成(例如使用AspectJ编译器),也可以在运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。

特别指出,Spring默认使用JDK动态代理实现AOP代理。这使得任何接口或接口的集合能够被代理。Spring也可以是CGLIB代理。这可以代理类,而不是接口。如果业务对象没有实现一个接口,CGLIB被默认使用。但是,作为一针对接口编程而不是类编程的良好实践,业务对象通常实现一个或多个业务接口。

前面提到横切关注点是AOP中的重要因素,使之独立于OO的层次选定目标,横切点到系统的切入点理所当然是构成系统的结构要素。下面看看Spring是如何处理切入点这个重要因素的。

Spring的切入点模型能够使切入点独立于通知类型被重用,同样的切入点有可能接受不同的通知。org.springframework.aop.Pointcut接口是重要的接口,用来指定通知到特定的类和方法目标。完整的接口定义如下:

图片详情找不到图片(Image not found)

将Pointcut接口分成两个部分有利于重用类和方法的匹配部分,并且组合细粒度的操作(如和另一个方法匹配器执行一个“并”的操作)。ClassFilter接口被用来将切入点限制到一个给定的目标类的集合。如果matches永远返回true,所有的目标类都将被匹配。

图片详情找不到图片(Image not found)

MethodMatcher接口通常更加重要。完整的接口如下:

图片详情找不到图片(Image not found)

matches(Method,Class)方法被用来测试这个切入点是否匹配目标类的给定方法。这个测试可以在AOP代理创建的时候执行,避免在所有方法调用时都需要进行测试。如果两个参数的匹配方法对某个方法返回true,并且MethodMatcher的isRuntime()也返回true,那么三个参数的匹配方法将在每次方法调用时被调用。这使得切入点能够在目标通知被执行之前立即查看传递给方法调用的参数。

大部分MethodMatcher都是静态的,意味着isRuntime()方法返回false。这种情况下,三个参数的匹配方法永远不会被调用。如果可能,尽量使切入点是静态的,使当AOP代理被创建时,AOP框架能够缓存切入点的测试结果。当然,目前的技术只实现了方面静态织入,无法动态地在运行状态下组合方面。

Spring AOP应用

Spring AOP是Spring框架的重要组成部分,它实现了AOP联盟约定的接口。Spring AOP是由纯Java开发完成的,它实现了方法级别的连接点,而在J2EE应用中,AOP拦截到方法级的操作已经足够了。由于OOP倡导的是基于setter/getter的方法访问,而非直接访问域,所以Spring仅仅提供方法级的连接点。为了使控制反转(IoC)很方便地使用健壮、灵活的企业服务,需要Spring AOP来实现,因为它在运行时才创建Advice对象。下面讨论使用Spring AOP松散耦合的几种方式。

创建通知

为实现AOP,开发者需要开发AOP通知(Advice)。AOP通知包含了方面(Aspect)的业务逻辑。当创建一个Advice对象时,就编写了实现横切(cross-cutting)功能的代码。Spring的连接点是用方法拦截器实现的,这就意味着编写的Spring AOP通知将在方法调用的不同点织入程序中。由于在调用一个方法时有几个不同的时间点,Spring可以在不同的时间点织入程序。

Spring AOP中,提供了如下4种通知的接口。

(1)MethodBeforeAdvice:用于在目标方法调用前触发。

(2)AfterReturningAdvice:用于在目标方法调用后触发。

(3)ThrowsAdvice:用于在目标方法抛出异常时触发。

(4)MethodInterceptor:用于实现Around通知(Advice),在目标方法执行的前后触发。

如果要实现相应的功能,则需要实现上述接口。例如,实现Before通知(Advice)需要实现方法void before(Method method,Object[] args,Object target);实现After通知(Advice)需要实现方法void afterReturning(Method method,Object[] args,Object target)。

在Spring中定义切入点

在不能明确调用方法时,通知就很不实用。切入点则可以决定特定的类、特定的方法是否匹配特定的标准。如果匹配,则通知将应用到此方法上。Spring切入点允许用很灵活的方式将通知组织进我们的类中。Spring中的切入点框架的核心是Pointcut接口,此接口允许定义织入通知中的类和方法。许多方面就是通过一系列的通知和切入点组合来定义的。

在Spring中,一个advisor就是一个方面的完整的模块化表示。Spring提供了PointcutAdvisor接口把通知和切入点组合成一个对象。Spring中很多内建的切入点都有对应的PointcutAdvisor,因此可以很方便地在一个地方管理切入点和通知。Spring中的切入点分为两类:静态和动态。因为静态切入点的性能要优于动态切入点,所以优先考虑使用静态切入点。Spring为我们提供创建静态切入点很实用的类StaticMethodMatherPointcut,在这个类中,只需要关心setMappedName和setMappedNams方法,可以使用具体的类名,也可以使用通配符。例如,设置mappedName属性为set*,则匹配所有的set方法。Spring还提供了通过正则表达式来创建静态切入点的实用类RegexpMethodPointcut。通过使用Perl样式的正则表达式来定义感兴趣的方法。当切入点需要用运行时参数值来执行通知时,则使用动态切入点。Spring提供了一个内建的动态切入点ControlFlowPointcut,此切入点匹配基于当前线程的调用堆栈。只有在当前线程运行时找到特定的类和特定的方法才返回true,使用动态切入点有很大的性能损耗。大多数的切入点可以静态确定,我们很少有机会创建动态切入点。为了增加切入点的可重用性,Spring提供了切入点上的集合操作——交集和并集。

用ProxyFactoryBean创建AOP代理

ProxyFactoryBean和其他Spring的FactoryBean实现一样,引入一个间接的层次。如果定义一个名字为myfactory的ProxyFactoryBean,引用myfactory的对象所看到的不是ProxyFactoryBean实例本身,而是由实现ProxyFactoryBean的类的getObject()方法所创建的对象。这个方法将创建一个包装了目标对象的AOP代理。使用ProxyFactoryBean或者其他IoC可知的类来创建AOP代理最重要的一个优点是IoC可以管理通知和切入点。这是一个非常强大的功能,能够实现其他AOP框架很难实现的特定的方法。例如,一个通知本身可以引用应用对象(除了目标对象,它在任何AOP框架中都可以引用应用对象),这完全得益于依赖注入所提供的可插入性。通常,不需要ProxyFactoryBean的全部功能,因为我们常常只对一个方面感兴趣。例如,事务管理。当我们仅仅对一个特定的方面感兴趣时,可以使用许多便利的工厂来创建AOP代理,如TransactionProxyFactoryBean。

自动代理

在应用规模比较小,只有很少类需要被通知时,ProxyFactoryBean可以很好地工作。当有许多类需要被通知时,创建每个代理就显得很烦琐。幸运的是,Spring提供了使用自动通过容器来创建代理的功能。这时,只需要配置一个Bean来做烦琐的工作。Spring提供了两个类实现自动代理:BeanNameAutoProxyCreator和DefaultAdvisorAutoProxyCreator。BeanNameAutoProxyCreator为匹配名字的Bean产生代理,它可将一个或者多个方面应用在命名相似的Bean中。自动代理框架将自动产生代理要暴露出的接口。如果目标Bean没有实现任何接口,就会动态产生一个子类。而更强大的自动代理是DefaultAdvisorAutoProxyCreator,只需要在BeanFactory中包含它的配置就可完成代理。这个类的奇妙之处在于它实现了BeanPostProcessor接口。当Bean定义被加载到Spring容器中后,DefaultAdvisorAutoProxyCreator将搜索上下文中的Advisor,最后它将Advisor应用到匹配Advisor切入点的Bean中。这个代理只对Advisor起作用,它需要通过Advisor来得到需要通知的Bean。元数据自动代理(MetaDataAutoProxy)配置依赖于源代码属性而不是外部XML配置文件。这可以非常方便地将源代码和AOP元数据组织在同一个地方。元数据自动代理最常用的地方是用来声明事务,Spring提供了很强的AOP框架来声明事务。