Post

一起来看aop

软件开发一直在寻求更加高效、更易维护甚至更易扩展的方式。为了提高开发效率,我们对开发使用的语言进行抽象,走过了从汇编时代到现在各种高级语言繁盛之时期;为了便于维护和扩展,我们就对某些相同的功能进行归类并使之模块化,冲出了最初的“原始部落”,走过了从过程化编程(Procedural Programming)到面向对象编程(OOP,Object-Oriented Programming)的“短暂而漫长”的历程。但不管走过的路有多长,多么坎坷,我们一直没有放弃寻找更加完美、更加高效的软件开发方法。过去如此,现在亦然。

当OOP/00SD(Object-Oriented Software Development)被提出来,以取代”过去的基于过程化编程的开发方法的时候,或许那个时代的人都会以为,而向对象编程和面向对象的软件开发OOP/OOSD)就是我们一直所追求的那颗能够搞定一切的“银弹”。但不得不承认的是,即使面向对象的软件开发模式,依然不能很好地解决软件开发中的所有问题。

软件开发的目的,最终是为了解决各种需求,包括业务需求和系统需求。使用面向对象方法,我们可以对业务需求等普通关注点进行很好的抽象和封装,并且使之模块化。但对于系统需求一类的关注点来说,情况却有所不同。

以我曾经参与开发的CREDIT项目为例,它是一个有关贷款业务的管理系统。从业务角度说,该系统提供了顾客贷款申请、顾客信息管理、贷款信息管理、贷款发放回收等功能:这些都属于普通的业务需求。通过面向对象方法,可以很容易地按照功能划分模块并完成开发。图7-1给出了这些功能模块之间清晰的关系。

对于业务需求而言,需求与其具体实现之间的关系基本上是一对一的。我们可以在系统中某一个确定的点找到针对这种需求的实现,无论从开发还是维护的角度,都比较方便。

不过,事情还没完呢!开发中为了调试,或在进入生产环境后为了对系统进行监控,我们需要为这些业务需求的实现对象添加日志记录功能;或者,业务方法的执行需要一定的权限限制。那么方法执行前肯定需要有相应的安全检查功能。而这些则属于系统需求的范畴。虽然需求都很明确(加入日志记录、加入安全检查),但是要将这些需求以面向对象的方式实现并集成到整个的系统中去,可就不是一个需求对应一个实现那么简单了。系统中的每个业务对象都需要加入日志记录,加入相应的安全检查,那么,这些需求的实现代码就会遍及所有业务对象。整个场景如图7-2所示。

图7-2中日志记录和安全检查的需求和实现的对应关系还仅仅是1:2。但随着系统中业务对象的增加,这个对应关系就会变成1:3、1∶4……1:100甚至更多。你可以想象一下,随着这个数目的增多,你的系统开发和维护的难度会向一个什么方向发展。

对于系统中普通的业务关注点,OOP可以很好地对其进行分解并使之模块化,但却无法更好地避免类似于系统需求的实现在系统中各处散落这样的问题。所以,我们要寻求一种更好的方法,它可以在OOP的基础上更上一层楼,提出一套全新的方法论来避免以上问题,也可以提供某种方法对基于00P的开发模式做一个补足,帮助OOP以更好的方式解决以上问题。但迄今为止,我们还找不到比OOP更加有效的软件开发模式。不过,好消息是,我们找到了后者,那就是AOP。

AOP全称为Aspect-OrientedProgramming,中文通常翻译为面向方面编程。使用AOP,我们可以对类似于Logging和Security等系统需求进行模块化的组织,简化系统需求与实现之间的对比关系,进而使得整个系统的实现更具模块化。

任何一个软件系统就跟CREDIT系统一样,日志记录、安全检查、事务管理等系统需求就像一把把刀“恶狠狠”地横切到我们组织良好的各个业务功能模块之上(见图7-3)。以AOP的行话来说,这些系统需求是系统中的横切关注点(cross-cuttingconcern)。使用传统方法,我们无法更好地以模块化的方式,对这些横切关注点进行组织和实现。所以AOP引入了Aspect的概念,用来以模块化的形式对系统中的横切关注点进行封装。Aspect之对于AOP,就相当于Class之对于OOP。我们说过AOP仅是对0OP方法的一种补足,当我们把以Cass形式模块化的业务需求和以Aspect形式模块化的系统需求拼装到一起的时候,整个系统就算完成了。

7.1 AOP 的尴尬

如果把我们的软件系统看作是可以划分为不同形状的积木,对于业务需求类型的积木块和系统需求类型的积木块来说,它们的形状和材质可以是相近甚至是相同的,但摆放的空间位置却完全处于不同的维度。

当对整个系统进行分析之后,我们可以将不同的需求实现为Aspect类型的积木块或者Class类型的积木块。这样,我们就有了如图7-4所示的一盒积木。

不过,积木块永远是积木块,需要我们动手搭建才能构建出美丽的模型。当我们把Class类型的积木块在一个空间面上摆放,而将Aspect类型的积木块在另一个空间面上摆放的时候,我们就有了一座美丽的城堡(见图7-5),而且最主要的,它是立体的哦!

也就是说,OOP的空间结合AOP的空间就可以构建一个完美的系统。不过,由于当前技术所限,虽然我们可以构造出AOP使用的各个积木块,但却无法构建属于AOP的独有空间,这就像俄罗斯方块一样。现在我们使用OOP进行软件开发,就好像在玩俄罗斯方块,当各个系统业务模块划分完成之后(俄罗斯方块就那几个特定的砖头形状),剩下的工作,就是在游戏规定的空间维度里,想方设法地把每一层都填满,否则,你就得等着你的系统崩溃,彻底地GameOver。还好,我们已经精熟于那几个砖头的方向调整和恰当的放置位置,就好像我们精熟于面向对象编程的方方面面或者设计模式(Design Pattern)一样。但是,即使如此,也并没有解决根本的问题,我们还是时不时地遗漏某个位置的空间。而实际上,如果允许,最简单的方法就是从我们所处的维度,直接使用合适的砖头,把那些遗漏的空间填补上就算大功告成。在那个有限的游戏空间内,我们也不用太过于费尽脑汁地考虑如何使用原来的规则消去遗漏的空间块。

俄罗斯方块那个游戏空间就是OOP现在的空间,而AOP应该在另一个空间内,才可以提供最大的便利。但是,正如我们所见,AOP现在没有主权,所以,现时代的AOP实现都要“寄生”于OOP的主权领土中,系统的维度也依然保持曾经OOP持有的“维度世界纪录”

7.2 AOP 走向现实

AOP是一种理念,要实现这种理念通常需要一种现实的方式。与O0P需要相应的语言支持一样,AOP也需要某种语言以帮助实现相应的概念实体,我们统称这些实现AOP的语言为AOL,即Aspect-Oriented Language,不要跟American On Line混淆哦。

AOL可以与系统实现语言相同,比如,如果系统实现语言为Java,那么,相应的AOL也可以为Java。但AOL并非一定要与系统实现语言相同,它也可以是其他语言,比如AspectJ是扩展自Java的一种AOL,显然与系统实现语言属于不同的两种语言。

除了主流的OOP语言,软件开发界现在已经有一些专门针对AOP的语言扩展。除了上面提到的对Java语言扩展后产生的AspectJ,还有: - AspectC; - AspectC++; - Aspect.Net; - AspectL(Lisp); - AspectPHP。 - ……

囿于现实中AOP技术实现上的尴尬,AOL实现的AOP各个概念实体,最终都需要某种方式集成到系统实现语言所实现的OOP实体组件中。所以,系统实现语言通常称为系统中使用的AOL的“寄生语言”,而将AO组件集成到OOP组件的过程,在AOP中称之为织入(Weave)过程。

将AOP的Aspect织入到OOP系统的实现方式可谓千差万别。但不管如何实现,织入过程是处于AOP和OOP的开发过程之外的,而且对于整个系统的实现是透明的,开发者只需要关注相应的业务需求实现,或者系统需求的实现即可。当所有业务需求和系统需求以模块化的形式开发完成之后,通过织入过程就可以将整个的软件系统集成并付诸使用。

Java界的AOP框架或者说产品,可谓AOP土地上的一朵奇葩,在Xerox公司的PARC(ParoAitoResearchCenter)提出AOP的一套理论之后,Java业界各种AOP框架就如雨后春笋般涌现,其走过的路亦不可谓不精彩,所以,让我们来回顾一下这段精彩历史何如?

7.2.1 静态 AOP 时代

静态AOP,即第一代AOP,以最初的Aspect为杰出代表,其特点是,相应的横切关注点以Aspect形式实现之后,会通过特定的编译器,将实现后的Aspect编译并织入到系统的静态类中。比如,Aspect]会使用ajc编译器将各个Aspect以Java字节码的形式编译到系统的各个功能模块中,以达到融合Aspect和Class的日的。而像EJB所提供的声明性事务等AOP关注点的实现,也应该归入第一代AOP行列。只不过,所采用的实现机制不同,但特点是一样的(后面将提到Java平台上实现AOP的各种机制)。

静态AOP的优点是,Aspect直接以Java字节码的形式编译到Java类中,Java虚拟机可以像通常一样加载Java类运行(因为编译完成的Aspect是完全符合Java类的规范的),不会对整个系统的运行造成任何的性能损失。

缺点嘛,就是灵活性不够。如果横切关注点需要改变织入到系统的位置,就需要重新修改Aspect定义文件,然后使用编译器重新编译Aspect并重新织入到系统中。

7.2.2 动态 AOP 时代

动态AOP,又称为第二代AOP,该时代的AOP框架或产品,大都通过Java语言提供的各种动态特性来实现Aspect织入到当前系统的过程,如JBosSAOP、Spring AOP以及Nanning等AOP框架,都属于此列。在AspectJ融合了AspectWerkz框架之后,也引入了动态织入的行为,从而成为现在Java界唯一一个同时支持静态AOP和动态AOP特性的AOP实现产品。

第二代AOP的AOL大都采用Java语言实现,AOP的各种概念实体全部都是普通的Java类,所以很容易开发和集成。Aspect跟Class一样最终以Class身份作为系统的一等公民存在,与静态AOP最大的不同就是,AOP的织入过程在系统运行开始之后进行,而不是预先编译到系统类中,而且织入信息大都采用外部XML文件格式保存,可以在调整织入点以及织入逻辑单元的同时,不必变更系统其他模块甚至在系统运行的时候,也可以动态更改织入逻辑。

但动态AOP在引入灵活性以及易用性的同时,也会不可避免地引入相应的性能问题。因为动态AOP的实现产品大都在类加载或者系统运行期间,采用对系统字节码进行操作的方式来完成Aspect到系统的织入,难免会造成一定的运行时性能损失。但随着IVM版本的提升,对反射以及字节码操作技术的更好支持,这样的性能损失在逐渐减少,大多数情况下,这种性能损失是可以容忍的。

7.3 Java 平台上的 AOP 实现机制

在Java平台上可以使用多种方式实现AOP。下面提到的儿种方式是最经常使用的,而且也通过了相应AOP产品的验证,它们可都是帮助我们的AOP在Java平台走向现实的基石。

7.3.1 动态代理

JDK1.3之后,引入了动态代理(DynamicProxy)机制,可以在运行期间,为相应的接口(Interface)动态生成对应的代理对象。所以,我们可以将横切关注点逻辑封装到动态代理的InvocationHandler中,然后在系统运行期间,根据横切关注点需要织入的模块位置,将横切逻辑织入到相应的代理类中。以动态代理类为载体的横切逻辑,现在当然就可以与系统其他实现模块一起工作了。

这种方式实现的唯一缺点或者说优点就是,所有需要织入横切关注点逻辑的模块类都得实现相应的接口,因为动态代理机制只针对接口有效。当然,之前也说了,毕党动态代理是在运行期间使用反射,相对于编译后的静态类的执行,性能上可能稍逊一些。

Spring AOP默认情况下采用这种机制实现AOP机能。Nanning也是,只支持动态代理机制。

7.3.2 动态字节码增强

我们知道,Java虚拟机加载的class文件都是符合一定规范的,所以,只要交给Java拟机运行的文件符合Java cass规范,程序的运行就没有问题。通常的class文件都是从Java源代码文件使用Javac编译器编译而成的,但只要符合Javaclass规范,我们也可以使用ASM或者CGLIB等Java工具库,在程序运行期间,动态构建字节码的class文件。

在这样的前提下,我们可以为需要织入横切逻辑的模块类在运行期间,通过动态字节码增强技术,为这些系统模块类生成相应的子类,而将横切逻辑加到这些子类中,让应用程序在执行期间使用的是这些动态生成的子类,从而达到将横切逻辑织入系统的目的。

使用动态字节码增强技术,即使模块类没有实现相应的接口,我们依然可以对其进行扩展,而不用像动态代理那样受限于接口。不过,这种实现机制依然存在不足,如果需要扩展的类以及类中的实例方法等声明为fina1的话,则无法对其进行子类化的扩展。

SpringAOP在无法采用动态代理机制进行AOP功能扩展的时候,会使用CGLIB库的动态字节码增强支持来实现AOP的功能扩展。

7.3.3 Java 代码生成

实际上,如果我们从早期的J2EE开发走过来的话,或者具体点儿,如果我们接触过早期的EJB开发的话,就已经接触了这种类型的AOP实现。

  • 是否记得现在依然让人念念不忘的容器内声明性事务支持?
  • 是否还记得CMP类型的实体Bean只需要声明接口,而不用给出相应的接口实现类?
  • 是否还记得大多数应用服务器提供商都会提供特定的EJB部署工具,以帮助我们进行EIB的部署?

事务属于跨越整个系统的一种横切关注点,所以,EJB容器提供的声明性事务支持,属于一种AOP功能模块实现。但早期EJB容器在实现这一功能的时候,大多会采用Java代码生成技术,这就是我们不需要提供CMP的接口实现类的原因,也是EJB容器提供商大多提供部署接口或者专有部署工具的原因。

EJB容器根据部署描述符文件提供的织入信息,会为相应的功能模块类根据描述符所提供的信息生成对应的Java代码,然后通过部署工具或者部署接口编译Java代码生成相应的Java类。之后,部署到EJB容器的功能模块类就可以正常工作了。

这种方式比较古老,也就早期的EJB容器使用最多,现在已经退休了。

7.3.4 自定义类加载器

所有的Java程序的class都要通过相应的类加载器(Cassloader)加载到Java虚拟机之后才可以运行。默认的类加载器会读取class字节码文件,然后按照class字节码规范,解析并加载这些class文件到虚拟机运行。如果我们能够在这个class文件加载到虚拟机运行期间,将横切逻辑织入到class文件的话,是不是就完成了AOP和OOP的融合呢?

我们可以通过自定义类加载器的方式完成横切逻辑到系统的织入,自定义类加载器通过读取外部文件规定的织入规则和必要信息,在加载class文件期间就可以将横切逻辑添加到系统模块类的现有逻辑中,然后将改动后的class交给Java虚拟机运行。偷梁换得漂亮,不是吗?

通过类加载器,我们基本可以对大部分类以及相应的实例进行织入,功能与之前的几种方式相比当然强大很多。不过这种方式最大的问题就是类加载器本身的使用。某些应用服务器会控制整个的类加载体系,所以,在这样的场景下使用可能会造成一定的问题。

JBOSS AOP和已经并入Aspect1项目的AspectWerkz框架都是采用自定义类加载器的方式实现。

7.3.5 AOL 扩展

AOL扩展是最强大、也最难掌握的一种方式,我们之前提到的AspectJ就属于这种方式。在这种方式中,AOP的各种概念在AOL中大都有一一对应的实体。我们可以使用扩展过的AOL,实现任何AOP概念实体甚至OOP概念实体,比如Aspect以及Class。所有的AOP概念在AOL中得到了最完美的表达。

采用扩展的AOL,在AOP概念的表述上颇具实力,使得AOP涉及的所有横切关注点逻辑在进行织入之前,可以自由自在地存活在自己的“国度”中。而且,具有强类型检查,基本可以对横切关注点要切入的系统运行时点有更全面的控制。而像“编译到静态类可以提升系统运行性能”,“Java虚拟机可以像加载平常类那样,加载已经织入相应逻辑的AO组件所在的类文件并运行”等特点,我们之前已经提过了。

不过,该方式强大的代价就是,你需要重新学习一门扩展了旧有的语言的AOL或者全新的AOL。建议你在看完SpringAOP框架之后,再做出你的决定,因为我们的观点一贯是KI.S.S.(Keep It Simple Stupid)。

7.4 AOP 国家的公民

不管我们是打算实现自己的AOP框架,还是使用现有的AOP框架,在此之前,我们都需要先了解AOP涉及的相关概念。进入AOP国度而不知道这个国度中各种事物是什么的话,可会让你寸步难行的哦!

注意 因为AOP的一些术语在各个框架中可能存在差异,没有一个统一的规定,所以下面大部分概念都是以Aspect】中的概念为主线,穿插其他AOP框架中特有概念的说明,毕竟人家AspectJ出身正统嘛。<br>另外,AOP的一些术语本来就不统一,所以,各个概念会以英文原文阐述,而不会给出相应的中文的翻译。实际上,只要你知道这个单词代表的AOP语意就可以了,就好像你知道“Spring代表一个Java框架,它可以帮助你做什么事情,而没有必要非要跟人说“这个春天框架怎么怎么的”一样。不过,如果你实在想知道中文怎么翻译,可以参考已出版的几本中文的Spring书其中会提及这些名词的中文名称。不过也是一家一个说法,个人感觉乱上加乱而已。

7.4.1 Joinpoint

在系统运行之前,AOP的功能模块都需要织入到OOP的功能模块中。所以,要进行这种织入过程,我们需要知道在系统的哪些执行点上进行织入操作,这些将要在其之上进行织入操作的系统执行点就称之为Joinpoint。

为了便于理解,我们参看图7-6中的比较一般的程序流程图。

在图7-6中,左边为一个方法的简单逻辑,实线表示的是该方法执行的顺序,虚线表示的是更加内部的调用顺序。

我们可以在HelloBean初始化的执行点进行横切逻辑的织入,可以在he1loMethod方法被调用的执行点上进行横切逻辑的织入,可以在helloMethod方法内部执行的开始时点上进行织入,也可以在message字段被设置或者取得的执行点上进行横切逻辑的织入。基本上,只要允许,程序执行过程中的任何时点都可以作为横切逻辑的织入点,而所有这些执行时点都是Joinpoint。

以下是一些较为常见的Joinpoint类型。

方法调用(MethodCal)。当某个方法被调用的时候所处的程序执行点,图7-6中的后面三个“圆圈”所标记的时点都属于这种类型。

方法调用执行(Method Call execution)。称之为方法执行或许更简洁,该Joinpoint类型代表的是某个方法内部执行开始时点,应该与方法调用类型的Joinpoint进行区分。我们看一下以上方法调用和执行的Sequence图或许更好理解(见图7-7)。

方法调用(method call)是在调用对象上的执行点,而方法执行(methodexecution)则是在被调用到的方法逻辑执行的时点。对于同一对象,方法调用要先于方法执行。

构造方法调用(ConstructorCal)。程序执行过程中对某个对象调用其构造方法进行初始化的时点,也就是图7-6中如下代码所在的执行点。

1
HelloBean helloBean = new HelloBean("Hello!");

构造方法执行(ConsructorCall Execution)。构造方法执行和构造方法调用之间的关系类似于方法执行和方法调用之间的关系,指的是某个对象构造方法内部执行的开始时点。

字段设置(Field Set)。对象的某个属性通过setter方法被设置或者直接被设置的时点。该Joinpoint的本质是对象的属性被设置,而通过setter方法设置还是直接设置触发的时点是相同的。

字段获取(Field Get)。相对于字段设置型的Joinpoint,字段获取型的Joinpoint,对应的是某个对象相应属性被访问的时点。可以通过getter方法访问,当然也可以直接访问。

异常处理执行(Exception Handler Execution)。该类型的Joinpoint对应程序执行过程中,在某些类型异常抛出后,对应的异常处理逻辑执行的时点。

类初始化(Classinitialization)。类初始化型的Joinpoint,指的是类中某些静态类型或者静态块的初始化时点。比如,代码清单7-1中的log4j初始化位置即属于该类型Joinpoint对应位置的一种。

基本上,程序执行过程中你认为必要的执行时点都可以作为Joinpoint。不过,对于一些位置,具体的AOP实现产品在捕捉的时候可能存在一定的困难,或者说能够实现,但付出太多却可能收效甚微。比如,程序中某个循环开始的时点也可以作为一种Joinpoint,但较难捕捉,所以,现在的AOP产品大都不支持该类型的Joinpoint。

7.4.2 Pointcut

Pointcut概念代表的是Joinpoint的表述方式。将横切逻辑织入当前系统的过程中,需要参照Pointcut规定的Joinpoint信息,才可以知道应该往系统的哪些Joinpoint上织入横切逻辑。

以图7-6中的helloBean.helloMethod()所在位置的Joinpoint为例,该方法在程序中两个地方被调用,而我们系统在这两个地方都要织入相应的横切逻辑,那么,我们就可以通过以下Pointcut表述来指定这两个Joinpoint:系统中HelloBean类的hel1oMethod()方法被调用的所有Joinpoint。

我们使用自然语言声明了一个Pointcut,该Pointcut指定了系统中符合条件的一组Joinpoint。不过,在实际系统中我们不可能使用自然语言形式的Pointcut。

  1. Pointcut的表述方式

    既然我们现在还没有使用自然语言编程的系统,那么就需要寻找自然语言之外的方式来表述相应的Pointcut定义。当前的AOP产品所使用的Pointcut表达形式通常可以简单划分为以下几种。

    直接指定Joinpoint所在方法名称。这种形式的Pointcut表述方式比较简单,而且功能单一,通常只限于支持方法级别Joinpoint的AOP框架,或者只是方法调用类型的Joinpoint,或者只是方法执行类型的Joinpoint。而且,即使是只针对方法级别的Joinpoint,因为系统中需要织入横切逻辑的方法可能很多,一个一个地指定则过于不便,所以这种方式通常只限于Joinpoint较少且较为简单的情况。

    正则表达式。这是比较普遍的Pointcut表达方式,可以充分利用正则表达式的强大功能,来归纳表述需要符合某种条件的多组Joinpoint。几乎现在大部分的Java平台的AOP产品都支持这种形式的Pointcut表达形式,包括Jboss AOP、Spring AOP以及AspectWerkz等。我们将在稍后看到Spring的AOP框架是如何使用正则表达式来指定Pointcut的。

    使用特定的Pointcut表述语言。这是一种最为强大的表达Pointcut的方式,灵活性也很好,但具体实现起来可能过于复杂,需要设计该表述语言的语法,实现相应的解释器等许多工作。AspectJ使用这种方式来指定Pointcut,它提供了一种类似于正则表达式的针对Pointcut的表述语言,在表达Pointcut方面支持比较完善。而且,Spring从发布2.0版本之后,借助于Aspecu的Pointcut表述语言解释器,现在也支持使用AspectJ的Pointcut表述语言来指定Pointcut。

2.Pointcut运算

1
2
3
4
5
通常,Pointcut与Pointcut之间还可以进行逻辑运算。这样,我们就可以从简单的Pointcut开始,然后通过逻辑运算,得到最终需要的可能较为复杂的Pointcut。打个比方,假设某一学校中的每个学生都算作单独的Joinpoint,那么我们可以声明以下Pointcut(表7-1)。

在这些Pointcut之上,我们就可以执行逻辑运算,以得到我们想要的更为确切的Pointcut表述(以Java语言中的逻辑运算符为例)。表7-2给出了几个Pointcut逻辑运算的示例。

具体使用的逻辑运算语法,会因AOP产品实现的不同而不同。比如在Spring的配置文件中使用and、or等单词作为逻辑运算符,而在AspectJ中,则可以使用&&以及。不要忘了,Aspect]可是扩展自Java的哦,所以,运算符能相同就相同了。

7.4.3 Advice

Advice是单一横切关注点逻辑的载体,它代表将会织入到Joinpoint的横切逻辑。如果将Aspect比作00P中的Class,那么Advice就相当于Class中的Method。按照Advice在Joinpoint位置执行时机的差异或者完成功能的不同,Advice可以分成多种具体形式。

1.Before Advice

1
2
3
Before Advice是在Joinpoint指定位置之前执行的Advice类型。通常,它不会中断程序执行流程,但如果必要,可以通过在Before Advice中抛出异常的方式来中断当前程序流程。如果当前Before Advice将被织入到方法执行类型的Joinpoint,那么这个Before Advice就会先于方法执行而执行。

通常,可以使用Before Advice做一些系统的初始化工作,比如设置系统初始值,获取必要系统资源等。当然,并非就限于这些情况。如果要用Before Advice来封装安全检查的逻辑,也不是不可以的,但通常情况下,我们会使用另一种形式的Advice。
  1. After Advice

    顾名思义,Afer Advice就是在相应连接点之后执行的Advice类型,但该类型的Advice还可以细分为以下三种。

    • After returning Advice。只有当前Joinpoint处执行流程正常完成后,Afterretumming Advice才会执行。比如方法执行正常返回而没有抛出异常。
    • After throwing Advice。又称Throws Advice,只有在当前Joinpoint执行过程中抛出异常的情况下,才会执行。比如某个方法执行类型的Joinpoint抛出某异常而没有正常返回。
    • After Advice。或许叫After (Finally)Advice更为确切,该类型Advice不管Joinpoint处执行流程是正常终了还是抛出异常都会执行,就好像Java中的finally块一样。如果以方法执行类型的Joinpoint为例,我们的各种Advice的执行时机可以基本如图7-8所示。
  2. Around Advice

    AOP Alliance属下的AOP实现大都采用拦截器(Interceptor)的叫法,但完成的功能是一样的Around Advice对附加其上的Joinpoint进行“包裹”,可以在Joinpoint之前和之后都指定相应的逻辑甚至于中断或者忽略Joinpoint处原来程序流程的执行。

    Around Advice的行为可以因为你而发生改变。呵呵,就好像这“居心叵测”的“叵”字,中间的那一“口”就是Joinpoint,上下一横就好像是要执行的逻辑。当我们只是希望在Joinpoint之前和之后执行横切逻辑,而忽略原来Joinpoint处的逻辑执行的时候,就是居心“叵”测了。而正常情况下,AroundAdvice应该像一个“巨”字,我们执行完Joinpoint之前的逻辑之后,会接着走Joinpoint,然后才是Joinpoint之后的逻辑。

    既然Around Advice可以在Joinpoint之前和之后都能执行相应的逻辑,那么,它自然可以完成BeforeAdvice和After Advice的功能。不过,通常情况下,还是应该根据场景选用更为具体的Advice类型。

    Around Advice应用场景非常广泛,我想大家对于J2EE中的Servlet规范提供的Filter功能应该很熟悉吧。实际上,它就是Around Advice的一种体现。使用它,我们就可以完成“资源初始化”、“安全检查”之类横切系统的关注点了。

4.introduction

1
2
3
4
5
在AspectJ中称Inter-Type Declaration,在JBoSS AOP中称Mix-in,都指的是这同一种类型的Advice.与之前的几种Advice类型不同,Introduction不是根据横切逻辑在Joinpoint处的执行时机来区分的,而是根据它可以完成的功能而区别于其他Advice类型。

Introduction可以为原有的对象添加新的特性或者行为,这就好像你是一个普通公民,当让你穿军装,带军帽,添加了军人类型的Introduction之后,你就拥有军人的特性或者行为。

Introduction类型的Advice因实现技术的不同,在具体软件环境中可能存在性能差异。AspectJ采用静态织入的形式,那么对象在使用的时候,Introduction逻辑已经是编译织入完成的。所以理论上来说,Aspect]提供的Introduction类型的Advice,在现有Java平台上的AOP实现中是性能最好的;而像JBoSSAOP或者SpringAOP等采用动态织入的AOP实现,Introduction的性能则要稍逊一筹。在具体的使用中,需要根据具体场景以权衡各方案之利弊。

7.4.4 Aspect

Aspect是对系统中的横切关注点逻辑进行模块化封装的AOP概念实体。通常情况下,Aspect可以包含多个Pointcut以及相关Advice定义。比如,以Aspect形式定义的Aspect如代码清单7-2所示。

Spring AOP最初没有“完全”确切的实体对应真正的Aspect的概念。在2.0发布后,因为集成了Aspect],所以可以通过使用@Aspectx的注解并结合普通的POI0来声明Aspect。

7.4.5 织入和织入器

毛主席有诗句云,“一桥飞架南北,天堑变通途”,织入(Weaving)过程就是“飞架”AOP和00P的那座桥,只有经过织入过程之后,以Aspect模块化的横切关注点才会集成到OOP的现存系统中。而完成织入过程的那个“人”就称之为织入器(Weaver)啦!

AspectJ有专门的编译器来完成织入操作,即ajc,所以ajc就是Aspect完成织入的织入器;JBossAOP采用自定义的类加载器来完成最终织入,那么这个自定义的类加载器就是它的织入器;SpringAOP使用一组类来完成最终的织入操作,ProxyFactory类则是SpringAOP中最通用的织入器。总之Jva平台各AOP实现的织入器形式不一而足,唯一相同的就是它们的职责,即完成横切关注点逻辑到系统的最终织入。

7.4.6 目标对象

符合Pointcut所指定的条件,将在织入过程中被织入横切逻辑的对象,称为目标对象(Target0biect)。当把所有这些概念组织到一个场景之后,我们脑海中应该有这么一幅图(见图7-9)其他AOP概念,我们将在稍后针对Spring AOP的讲解中附带提及,这里就不做更多描述了。

7.5 小结

AOP是近年流行起来的一种软件开发模式。本章伊始,我们对AOP产生的背景做了简短的介绍。之后对Java平台上各种AOP框架及产品的实现原理和方式进行了简单的剖析,以期你能够对Java平台上AOP的现状有一个总体的认识。

因为AOP涉及许多新的概念,所以在继续讲解本部分主体内容前,我们首先要对AOP王国中的各位公民有一个大致的了解。在此基础上,我们再开始真正的Spring AOP之旅。

This post is licensed under CC BY 4.0 by the author.