AOP概述,实现机制,案例,扩展

目录

AOP简介

AOP的尴尬

AOP走向现实

静态AOP时代

动态AOP时代

Java平台上的AOP实现机制

动态代理

动态字节码增强

Java代码生成

自定义类加载器

扫描二维码关注公众号,回复: 10855813 查看本文章

AOL扩展

AOP国家的公民

Joinpoint

Pointcut

Advice

Aspect

织入和织入器

目标对象

Spring AOP概述

Spring AOP的实现机制

设计模式之代理模式

动态代理

动态字节码生成

AOP应用案例

异常处理

Java异常处理

Fault Barrier

安全检查

 缓存

AOP扩展

有关公开当前调用的代理对象的探讨

问题的现象

原因的分析

解决方案


AOP简介

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

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

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

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

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

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

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

任何一个软件系统就跟CREDIT系统一样,日志记录、安全检查、事务管理等系统需求就像一把 把刀“恶狠狠“地横切到我们组织良好的各个业务功能模块之上。以AOP的行话来说,这 些系统需求是系统中的横切关注点(cross-cutting concern)。使用传统方法,我们无法更好地以模块 化的方式,对这些横切关注点进行组织和实现。

所以AOP引入了Aspect的概念,用来以模块化的形式对系统中的横切关注点进行封装。Aspect之对于AOP, 就相当于Class之对于OOP。我们说过AOP仅是 对OOP方法的一种补足,当我们把以Class形式模块化的业务需求和以Aspect形式模块化的系统需求拼装到一起的时候,整个系统就算完成了。

AOP的尴尬

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

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

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

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

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

AOP走向现实

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

AOL可以与系统实现语言相同,比如,如果系统实现语言为Java, 那么,相应的AOL也可以为Java。

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

除了主流的OOP语言,软件开发界现在已经有一些专门针对AOP的语言扩展。除了上面提到的对 Java语言扩展后产生的AspectJ. 还有:

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

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

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

静态AOP时代

静态AOP, 即第一代AOP, 以最初的AspectJ为杰出代表,其特点是,相应的横切关注点以Aspect 形式实现之后,会通过特定的编译器,将实现后的Aspect编译并织入到系统的静态类中。

比如,AspectJ 会使用ajc编译器将各个Aspect以Java字节码的形式编译到系统的各个功能模块中,以达到融合Aspect 和Class的目的。而像EJB所提供的声明性事务等AOP关注点的实现,也应该归入第一代AOP行列。只 不过,所采用的实现机制不同,但特点是一样的(后面将提到Java平台上实现AOP的各种机制)。

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

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

动态AOP时代

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

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

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

Java平台上的AOP实现机制

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

动态代理

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

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

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

动态字节码增强

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

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

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

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

Java代码生成

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

是否记得现在依然让人念念不忘的容器内声明性事务支持?

是否还记得CMP类型的实体Bean只需要声明接口,而不用给出相应的接口实现类?

是否还记得大多数应用服务器提供商都会提供特定的EJB部署工具,以帮助我们进行EJB的部 署?

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

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

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

自定义类加载器

所有的Java程序的class都要通过相应的类加载器(Classloader)加载到Java虚拟机之后才可以运行。

默认的类加载器会读取class字节码文件,然后按照class字节码规范,解析并加载这些class文件到虚拟 机运行。如果我们能够在这个class文件加载到虚拟机运行期间,将横切逻辑织入到class文件的话,是 不是就完成了AOP和OOP的融合呢?

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

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

JBoss AOP和已经并入AspectJ项目的Aspectype扣框架都是采用自定义类加载器的方式实现。

AOL扩展

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

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

不过,该方式强大的代价就是,你需要重新学习一门扩展了旧有的语言的AOL或者全新的AOL。

建议你在看完SpringAOP框架之后,再做出你的决定,因为我们的观点一贯是K.I.S.S. (Keep It Simple Stupid)。

AOP国家的公民

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

注意 因为AOP的一些术语在各个框架中可能存在差异,没有一个统一的规定,所以下面大部 分概念都是以AspectJ中的概念为主线,穿插其他AOP框架中特有概念的说明,毕竞人家AspectJ 出身正统嘛.

另外,AOP的一些术语本来就不统一,所以,各个概念会以英文原文阐述,而不会给出相应的 中文的翻译.实际上,只要你知道这个单词代表的AOPl吾意就可以了,就好像你知道"Spring" 代表一个Java框架,它可以帮助你做什么事情,而没有必要非要跟人说“这个春天框架怎么怎 么的"一样。不过,如果你实在想知道中文怎么翻译,可以参考已出版的几本中文的Spring书, 其中会提及这些名词的中文名称.不过也是一家一个说法,个人感觉乱上加乱而已。

Joinpoint

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

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

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

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

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

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

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

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

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

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

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

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

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

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

类初始化(Class initialization)。类初始化型的Joinpoint. 指的是类中某些静态类型或者静态块的 初始化时点。比如,代码消单中的log4j初始化位览即属于该类型Join point对应位置的一种。

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

Pointcut

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

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

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

1. Pointcut的表述方式

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

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

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

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

2. Pointcut运算

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

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

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

Advice

Advice是单一横切关注点逻辑的载体,它代表将会织入到Join point的横切逻辑。如果将Aspect比 作OOP中的Class, 那么Advice就相当于Class中的Method。

按照Advice  Joinpoint位置执行时机的差异或者完成功能的不同,Advice可以分成多种具体形式。

1, Before Advice

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

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

2. After Advice

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

Afte returning Advice。只有当前Join point处执行流程正常完成后,After returning Advice才会 执行。比如方法执行正常返回而没有抛出异常。

After throwing Advice。又称Throws Advice, 只有在当前Joinpoint执行过程中抛出异常的情况 下,才会执行。比如某个方法执行类型的Joinpoint抛出某异常而没有正常返回。

After Advice。或许叫After (Finally) Advice更为确切,该类型Advice不管Joinpoint处执行流程 是正常终了还是抛出异常都会执行,就好像Java中的finally块一样。

如果以方法执行类型的Join point为例,我们的各种Advice的执行时机可以基本如图

3.Around Advice

AOP Alliance属下的AOP实现大都采用拦截器(Interceptor)的叫法,但完成的功能是一样的。

Around Advice对附加其上的Joinpoint进行“包裹",可以在Joinpoint,前和之后都指定相应的逻辑, 甚至于中断或者忽略Joinpoint处原来程序流程的执行。

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

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

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

4. Introduction

在AspectJ中称Inter-Type Declaration, 在JBossAOP中称Mix-in, 都指的是这同一种类型的Advice。

与之前的几种Advice类型不同,Introduction不是根据横切逻辑在Joinpoint处的执行时机来区分的,而 是根据它可以完成的功能而区别于其他Advice类型。

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

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

Aspect

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

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

织入和织入器

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

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

目标对象

符合Pointcut所指定的条件,将在织入过程中被织入横切逻辑的对象,称为目标对象(Target Object)。当把所有这些概念组织到一个场景之后,我们脑海中应该有这么一幅图(见图)。

其他AOP概念,我们将在稍后针对Spring AOP的讲解中附带提及,这里就不做更多描述了。

Spring AOP概述

任何AOP的实现产品都是在构筑实业,没有这些AOP产品的支持,一切AOP的理论和概念都是空 中楼阁。Spring AOP就是这个群体中的一员。

Spring AOP是Spring核心框架的重要组成部分,通常认为它与Spring的IoC容器以及Spring框架对 其他JavaEE服务的组成共同组成了Spring框架的“质量三角”,足见其地位之重要

Spring AOP采用Java作为AOP的实现语言(AOL) ,较之像AspectJ那种语言扩展型的AOP实现, SpringAOP可以更快捷地融入开发过程,学习曲线相对要平滑得多。而且,Spring AOP的设计哲学也 是简单而强大。它不打算将所有的AOP需求全部襄括在内,而是要以有限的20%的AOP支持,来满足 80%的AOP需求。如果觉得Spring AOP无法满足你所需要的那80%之外的需求,那么求助于AspectJ好 了,SpringAOP对AspectJ也提供了很好的集成。

Spring AOP的AOL语言为Java。所以,在Java语言的基础之上,Spring AOP对AOP的概念进行了 适当的抽象和实现,使得每个AOP的概念都可以落到实处。而这些概念的抽象和实现,都是以我们所 熟悉的Java语言的结构呈现在我们面前的。

Spring AOP从最初的框架发布以来,一直保持稳定的AOP模型。即使2.0引入了AspectJ的支持,但 总的设计和实现依然延续最初的设想,也就是使用动态代理机制,构建基于Java语言的简单而强大的 AOP框架。

在详细介绍SpringAOP王国的“各位公民" (AOP概念实体)之前,我们有必要先看一下这个“王 国”是如何运作的。现在就开始吧!

Spring AOP的实现机制

SpringAOP属于第二代AOP, 采用动态代理机制和字节码生成技术实现。

与最初的AspectJ采用编 译器将横切逻辑织入目标对象不同,动态代理机制和字节码生成都是在运行期间为目标对象生成一个 代理对象,而将横切逻辑织入到这个代理对象中,系统最终使用的是织入了横切逻辑的代理对象,而 不是真正的目标对象。

为了理解这种差别以及最终可以达到的效果,我们有必要先从动态代理机制的根源一一代理模式 (Proxy Pattern)开始说起…...

设计模式之代理模式

说到代理,我们大家应该并不陌生,现在伴随房地产的春风而繁荣的房地产中介就是一种代理。

我们偶尔使用的网络代理,也是一种代理,还有许多许多,我就不一一列举了。代理处于访问者与被 访问者之间,可以隔离这两者之间的直接交互,访问者与代理打交道就好像在跟被访问者在打交道一 样,因为代理通常几乎会全权拥有被代理者的职能,代理能够处理的访问请求就不必要劳烦被访问者 来处理了。从这个角度来说,代理可以减少被访问者的负担。另外,即使代理最终要将访问请求转发 给真正的被访问者,它也可以在转发访问请求之前或者之后加入特定的逻辑,比如安全访问限制,或 者,像房产中介那样抽取一定的中介费等。

在软件系统中,代理机制的实现有现成的设计模式支持,就叫代理模式。在代理模式中,通常涉 及4种角色,如图所示。

ISubject。该接口是对被访问者或者被访问资源的抽象。在严格的设计模式中,这样的抽象 接口是必须的,但往宽了说,某些场崇下不使用类似的统一抽象接口也是可以的。

SubjectImpl。被访问者或者被访问资源的具体实现类。如果你要访问某位明星,那么 SubjectImpl就是你想访问的明星;如果你要买房子,那么Subjectlmpl就是房主......

SubjectProxy, 被访间者或者被访问资源的代理实现类,该类持有一个ISubject接口的具体 实例。在这个场景中,我们要对Subjectimpl进行代理,那么Subject Proxy现在持有的就是SubjectImpl的实例

Client。代表访问者的抽象角色,Client将会访问ISubject类型的对象或者资源。在这个场京 中,Client将会请求具体的SubjectImpl实例,但Client无法直接请求其真正要访问的资源 SubjectImpl, 而是必须要通过ISubject资源的访问代理类SubjectProxy进行。

SubjectImpl和SubjectProxy都实现了相同的接口ISubject, 而SubjectProxy内部持有 SubjectImpl的引用。当Client通过request ()请求服务的时候,Subj ectProxy将转发该请求给 SubjectImpl。从这个角度来说,Subject Proxy反而有多此一举之嫌了。不过,Subject Proxy的作 用不只局限于请求的转发,更多时候是对请求添加更多访问限制。SubjectImpl和SubjectProy之间 的调用关系,如图所示。

在将诮求转发给被代理对象Subjectimpl之前或者之后,都可以根据情况插入其他处理逻辑,比 如在转发之前记录方法执行开始时间,在转发之后记录结束时间,这样就能够对SubjectImpl的 request{)执行的时间进行检测。或者,可以只在转发之后对Subjectlmpl的request ()方法返回结 果进行覆盖,返回不同的值。甚至,可以不做请求转发,这样,就不会有SubjectImpl的访问发生。 如果你不希望某人访问你的Subj ectimpl, 这种场景正好适合。

代理对象SubjectProxy就像是SubjectImpl的影子,只不过这个影子通常拥有更多的功能。如果 Subject Impl是系统中的JoinJ)oint所在的对象,即目标对象,那么就可以为这个目标对象创建一个代 理对象,然后将横切逻辑添加到这个代理对象中。当系统使用这个代理对象运行的时候,原有逻辑的 实现和横切逻辑就完全融合到一个系统中。如图所示。

SpringAOP本质上就是采用这种代理机制实现的,但是,具体实现细节上有所不同,让我们来看一下为什么。

假设要对系统中所有的reguest()方法进行拦截,在每天午夜0点到次日6点之间,request调用不 被接受,那么,我们应该为Subjectlmpl提供一个ServiceControlSubjectProxy, 以添加这种横切 逻辑。这样就有了代码清单的ServiceControlSubjectProxy定义。

之后,我们使用该serviceControlSubjectProxy代替SubjectImpl使用,如以下代码所示:

但是,系统中可不一定就Isubject的实现类有request ()方法,IRequestable接口以及相应实 现类可能也有request (}方法,它们也是我们需要横切的关注点。IRequestable及其实现类见代码清单

为了能够为IRequestable相应实现类也织入以上的横切逻辑,我们又得提供对应的代理对象发现问题了没有?虽然Joinpoint相同(request()方法的执行),但是对应的目标对象类型是不 一样的。针对不一样的目标对象类型,我们要为其单独实现一个代理对象。而实际上,这些代理对象 所要添加的横切逻辑是一样的。当系统中存在成百上千的符合Pointcut匹配条件的目标对象时(悲观点 说,目标对象类型都不同),我们就要为这成百上千的目标对象创建成百上千的代理对象,不用我再 往下讲了吧?这么玩会死人的!

这种为对应的目标对象创建静态代理的方法,原理上是可行的,但具体应用上存在问题,所以要 寻找其他方法,以避免刚刚碰到的窘境……

动态代理

JDK 1.3之后引入了一种称之为动态代理(Dynamic Proxy)的机制。使用该机制,我们可以为指定 的接口在系统运行期间动态地生成代理对象,从而帮助我们走出最初使用静态代理实现AOP的窘境。

动态代理机制的实现主要由一个类和一个接口组成,即java.lang.reflect.Proxy类和java. lang. reflect. InvocationHandler接口。下面,让我们看一下,如何使用动态代理来实现之前的 "request服务时间控制”功能。虽然要为ISubject和IRequestable两种类型提供代理对象,但因为代 理对象中要添加的横切逻辑是一样的,所以,我们只需要实现一个InvocationHandler就可以了,其 定义见代码清单

然后,我们就可以使用Proxy类,根据RequestableInvocationHandler的逻辑,为ISubject和 IRequestable两种类型生成相应的代理对象实例,见代码清单。

即使还有更多的目标对象类型,只要它们依然织入的横切逻辑相同,用RequestUrlinvocation­Handler一个类并通过Proxy为它们生成相应的动态代理实例就可以满足要求。当Proxy动态生成的代 理对象上相应的接口方法被调用时,对应的InvocationHandler就会拦截相应的方法调用,并进行相 应处埋。

Invocationttandler就是我们实现横切逻辑的地方,它是横切逻辑的载体,作用跟Advice是一样 的。所以,在使用动态代理机制实现AOP的过程中,我们可以在InvocationHandler的基础上细化程 序结构,并根据Advice的类型,分化出对应不同Advice类型的程序结构。我们将在稍后看到SpringAOP 中的不同Advice类型实现以及结构规格。

动态代理虽好,但不能满足所有的需求。因为动态代理机制只能对实现了相应Interface的类使用, 如果某个类没有实现任何的Interface, 就无法使用动态代理机制为其生成相应的动态代理对象。虽然 面向接口编程应该是提倡的做法,但不排除其他的编程实践。对于没有实现任何Interface的目标对象, 我们需要寻找其他方式为其动态的生成代理对象。

默认情况下,如果Spring AOP发现目标对象实现了相应Interface, 则采用动态代理机制为其生成 代理对象实例。而如果目标对象没有实现任何Interface, Spring AOP会尝试使用一个称为CGLIB (Code Generation Library)的开源的动态字节码生成类库,为目标对象生成动态的代理对象实例。

动态字节码生成

使用动态字节码生成技术扩展对象行为的原理是,我们可以对目标对象进行继承扩展,为其生成 相应的子类,而子类可以通过覆写来扩展父类的行为,只要将横切逻辑的实现放到子类中,然后让系 统使用扩展后的目标对象的子类,就可以达到与代理模式相同的效果了。SubClass instanceof Superclass == true, 不是吗? (图演示了一个使用CGLIB进行对象行为扩展的示例。)

但是,使用继承的方式来扩展对象定义,也不能像静态代理模式那样,为每个不同类型的目标对 象都单独创建相应的扩展子类。所以,我们要借助于CGLIB这样的动态字节码生成库,在系统运行期 间动态地为目标对象生成相应的扩展子类。

为了演示CGLIB的使用以及最终可以达到的效果,我们定义的目标类如下所示:

CGLIB可以对实现了某种接口的类,或者没有实现任何接口的类进行扩展。但我们已经说过,可 以使用动态代理机制来扩展实现了某种接口的目标类,所以,这里主要演示没有实现任何接口的目标 类是如何使用CGLIB来进行扩展的。

要对Requestable类进行扩展,首先需要实现一个net.sf.cglib.proxy.Callback。不过更多 的时候,我们会直接使用net.sf. cglib. proxy. Methodinterceptor接口(Methodinterceptor扩 展了Callback接口)。代码清单给出了针对我们的Requestable所提供的Callback实现。

这样,ReguestCtrlCallback实现了对request ()方法请求进行访问控制的逻辑。现在我们要 通过CGLB的Enhancer为目标对象动态地生成一个子类,并将RequestUrlCallback中的横切逻辑附加到该子类中,代码如下所示:                                                   

通过为enhancer指定需要生成的子类对应的父类,以及Callback实现,'enhancer最终为我们生成 了需要的代理对象实例。

使用CGLIB对类进行扩展的唯一限制就是无法对final方法进行覆写。

AOP应用案例

异常处理

或许你已经在使用AOP的方式进行异常处理,但是可能并不知道这实际上对应着一个很有趣的术 语,叫做Fault Barrier。同样地,在接触这个术语之前,我实际上已经将这个概念所阐述的理念应用于 工作中,直到看到dev2dev网站上的一篇文章'" Effective Java Exception,才知道原来这种异常的处 理方式还对应这么一个有趣的术语。不过,在讲述Fault Barrier之前,我们有必要先来回顾一下Java中 异常处理的相关内容。

Java异常处理

异常处理是个很大的话题,限于篇幅,我们不可能详细讲述异常处理的方方面面。下面只是简单 地回顾一下Java中的异常类型和对应的处理方式,以便引出Fault Barrier的概念。

Java. 不是最早也不是唯一使用异常机制的语言。不过,Java却在引入Checked Exception的基础上为 其自身的异常处理添加了少许的新意。我们先来看一下Java中的异常层次体系大体上是一种什么样的 结构,如图所示。

在图中,我们将Java中的异常类型分为如下两类。

通常将java.lang.Error和java.lang.RuntimeException及其子类称之为unchecked exception。之所以如此称呼,是因为编译器不会对这些类型的异常进行编译期检查。因为 java. lang. Error我们通常关心不着,所以,狭义上将java.lang.RuntimeException及其 子类暂且称为unchecked exception也是可以的。

java.lang.Exception及其子类,但除去java.lang.RuntimeException分支,统称为checlced exception。一旦在程序的方法定义中声明了将会抛出.. checked exception", 调用程序就必须对 这些异常进行处理,而且编译器会在编译期间对这些异常类型进行检查。

unchecked exception通常对应系统中的严重异常情况,这些情况应用程序一般无法恢复,比如 数据库挂掉、网线连接中断、服务器崩溃等。所以,unchecked exception所提供的信息一般不 是为应用程序准备的,而是为人准备的,确切地说,是为了能够让系统维护人员能够根据所 提供的信息来判定到底哪里出了问题,以便人工干预。

checked exception引入Java后,一直是备受争议。不过,我觉得这与概念本身没有任何关系, 是否被应用于合适的场合是由人来决定的,而不是概念本身。checked exception常用于表明 系统中的某些罕见的非正常状态。对于一个业务方法来说,使用错误号(Error Code)的时代 是通过返回-1之类数字表明一些非正常状态,现在可以通过抛出不同类型的checked exception 来表明这些非正常状态,井要求调用方对这些非正常状态进行处理,而编译器对checked exception的检查可以进一步加强这种契约关系。通常checked exception是可恢复的,也是意料 之中的,它所提供的信息是面向应用程序的,应用程序可以根据系统逻辑对不同的checked exception类型进行有针对性的处理。

在技术文章'-'"Effective Java Exception"中,作者将unchecked exception对应的情况称之为.Fault, 而将checked exception对应的情况称之为Contingency。而Fault Barrier要处理的,就是对应Fault的情况, 即unchecked exception

Fault Barrier

对于checked exception来说,不同的类型可以有不同的处理方式,这完全是由系统逻辑来决定的, 调用方可以根据不同的类型,有针对性地对checked exception进行处理。反过来,对于unchecked exception来说,不同的类型则是没有太多必要的,因为不管你的应用程序抛出何种类型的unchecked exception, 最终都是需要人来进行干预,只要unchecked exception能够提供足够的信息,相应人员就可 以进行处理,几乎就是无差别对待。

当系统中多个地方都可能抛出unchecked exception的时候,在引入Fault Barrier概念之前,我们可 能会在每个调用的最顶层,分别添加异常处理逻辑对其进行处理。而就像前面所说的那样,unchecked exception实际上可以做的事情很少,通常就是记录日志、通知相应人员。所以,这些相同的逻辑实现 可以归井于一处进行处理,而不是让它们散落到系统的各处,也就是说,对于系统中的Fault来说,它实际上就是一种横切关注点(cross-cutting concern)。

鉴于此,我们完全可以实现一个对应Fault处理的Aspect. 让其对系统中的所有可能的Fault情况进 行统一的处理。这个专职于处理Fault的Aspect, 我们就称之为Fault Barrier。

实际上,我们从讲解SpringAOP的ThrowsAdvice开始,就提供了一个Fault Barrier的实现实例。在 该实例中,我们通过电子邮件的方式将系统中的Fault情况,也就是以unchecked exception形式给出的 信息转发给相关人员,并记录到日志。当然,如果可能,你还可以加入更多的处理,比如分析unchecked exception信息、为相关人员提供更加友好的系统信息等。

守注意任何的概念都会有特例,对于异常来说也是如此,比如unchecked exception也可以被相 夕、应的方法声明并被调用方捕获。不过,前面也说了,篇幅有限,元法完全囊括所有内容。特殊 的情况,如果后继内容有所涉及,我们会给子相应解释。

安全检查

如果你已经使用Java开发Web应用程序多年,那么一定不会对Filter感到陌生吧?

javax.servlet.Filter是Servlet规范为我们提供的一种AOP支持,通过它,我们可以为基于Servlet 的Web应用添加相应的资源访问控制(当然,还可以做很多其他事情)。不过,基于Filter的Web应 用的资源访问控制,仅仅是特定领域的安全检查需求。实际上,通过AOP, 我们可以为任何类型的应 用添加相应的安全支持。

在介绍AOP概念的时候就曾经提到过,安全检查属于系统的一种横切关注点,按照原先的方法进 行系统开发,势必让这些安全检查逻辑散落系统各处,所以,对付它的最好办法就是用AOP。在将系 统中可能需要安全检查的点排查清楚之后,我们就可以为这些点织入安全检查的逻辑了。

要为系统中某个点添加安全支待,最简单的办法就是提供一个拦截器,对所有访问该点的调用进 行拦截。所以,对于基本的安全检查的Aspect实现来说,如代码清单所示。

既然我们崇尚不重新发明轮子,在动手之前,有必要使用Google搜索一下是否有现成的,免去 人力物力的浪费。

实际上,作为基于Spring平台的一套安全框架,Acegi框架(也就是最新的Spring Security)已经可 以说在企业级应用的安全领域户名远扬了。它在Spring基础上,提供了完备的系统认证、授权、访问 控制等安全检查功能。Acegi框架最初是独立于Spring开发的,现在已经并入Spring Portfolio, 更名为 Spring Security, 可以在http://www.acegisecurity.org/获得有关Acegi的更多信息。

 缓存

AOP应用的另一个主要场景是为系统透明地添加缓存支持。缓存可以在很大程度上提升系统的性 能,但它不是业务需求,而是系统需求。在现有方法论的基础上为系统添加缓存支持,就会因为系统 中缓存需求的广泛分布,造成实现上的代码散落。

为了避免需要添加的缓存实现逻辑影响业务逻辑的实现,我们可以让缓存的实现独立于业务对象 的实现之外,将系统中的缓存需求通过AOP的Aspect封装,只在系统中某个点确切需要缓存支持 的情况下,才为其织入。

使用AOP为系统添加缓存很简单,如代码清单所示。(代码示例没有添加同步逻辑,在生产环 境下实现类似功能的时候需要考虑到这一点。)

在没有使用AOP之前,要为系统某个地方加入缓存的话,恐怕也是以差不多的逻辑实现的。 不过,现在不需要这么做了,原因如下。

现在已经有许多现成的Caching产品实现,如EhCache、JBossCache等。

Spring Modules项目提供了对现有Caching产品的集成,这样就可以通过外部声明的方式为系统 中的Joinpoint添加Caching支待。

AOP扩展

有关公开当前调用的代理对象的探讨

问题的现象

假设我们有如代码清单所示的目标对象类定义。当然,这样的类定义可以映射到系统中的任 何可能的业务对象。

该类定义中需要我们关注的是它的某个方法会调用同一对象上定义的其他方法。这通常是比较常 见的。在NestableinvocationBO类中,method1方法调用了同一对象的method2方法。

现在,我们要使用Spring AOP拦截该类定义的method1和method2方法,比如加入一个简单的性 能检测逻辑。那么可以定义一个PerformanceTraceAspect, 如代码清单所示。

我们的Around Advice定义会拦截compositePointcut ()所指定的Joinpoint, 即method1或者 method2的执行。

如果将PerformanceTraceAspect中定义的横切逻辑织入NestableinvocationBO, 然后运行如 下代码并查看结果:

发现问题没有?当我们从外部直接调用NestableinvocationBO对象的method2的时候,因为该 方法签名匹配PerformanceTracspect中Around Advice所对应的Pointcut定义,所以,Around Advice 逻辑得以执行,也就是说,PerforrnanceTraceAspect拦截method2的执行成功了。但是,当调用 method1的时候,却只有method1方法的执行拦截成功,而method1方法内部的method2方法执行却没 有被拦截,因为输出日志中只有PT in method [method1 ]的信息。

原因的分析

这种结果的出现,归根结底是由SpringAOP的实现机制造成的。我们知道,Spring AOP采用代理 模式实现AOP, 具体的横切逻辑会被添加到动态生成的代理对象中,只要调用的是目标对象的代理对 象上的方法,通常就可以保证目标对象上的方法执行可以被拦截。就像NestableInvocationBO的method2方法执行一样,当我们调用代理对象上的method2的时候,目标对象的method2就会被成功拦 截。

不过,代理模式的实现机制在处理方法调用的时序方面,会给使用这种机制实现的AOP产品造成 一个小小的缺憾“。我们来看一般的代理对象方法与目标对象方法的调用时序,如下所示:

在代理对象方法中,不管你如何添加横切逻辑,也不管你添加多少横切逻辑,有一点是确定的, 那就是,你终归需要调用目标对象上的同一方法来执行最初所定义的方法逻辑。

如果目标对象中原始方法调用依赖于其他对象,那没问题,我们可以为目标对象注入所依赖对象 的代理,井且可以保证相应Joinpoint被拦截并织入横切逻辑。而一旦目标对象中的原始方法调用直接 调用自身方法的时候,也就是说,它依赖于自身所定义的其他方法的时候,问题就来了,如图 示。

在代理对象的method1方法执行经历了层层拦截器之后,最终会将调用转向目标对象上的 method1, 之后的调用流程全部都是走在TargetObject上,当method1调用method2时,它调用的是 TargetObject的method2, 而不是Proxy Object上的method2。要知道,针对method2的横切逻辑,只 织入到了ProxyObject上的method.2方法中,所以,在method1中所调用的method2没

解决方案

知道了原因,我们就可以“对症下药”了。

当目标对象依赖于其他对象时,我们可以通过为目标对象注入依赖对象的代理对象,来解决相应 的拦截问题。那么,当目标对象依赖于自身时,我们也可以尝试将目标对象的代理对象公开给它只要让目标对象调用自身代理对象上的相应方法,就可以解决内部调用的方法没有被拦截的问题。

Spring AOP提供了AopContext来公开当前目标对象的代理对象,我们只要在目标对象中使用 AopContext.currentProxy()就可以取得当前目标对象所对应的代理对象。现在,我们重构目标对 象,让它直接调用它的代理对象的相应方法,如代码清单所示。

要使AopContext.currentProxy()生效,我们在生成目标对象的代理对象时,需要将ProxyConfig或者它的相应子类的exposeProxy属性设置为true, 如下所示:

任何匹配Pointcut定义的method2都得到了成功拦截。

问题是解决了,但解决的方式不是很雅观,因为我们的目标对象都直接绑定到了SpringAOP的具 体API上了。所以,我们考虑重构目标对象定义。既然我们知道能够通过AopContext. current Proxy () 取得当前目标对象对应的代理对象,那完全可以在目标对象中声明对其代理对象的依赖,而由IoC容 器来帮助我们注入这个代理对象。

注入的方式可以不一而足。

可以在目标对象中声明一个实例变量作为其代理对象的引用,然后由构造方法注入或者setter 方法注入将AopContext. currentProxy ()取得的Object注入给这个声明的实例变量。

在目标对象中声明一个getter方法,如getThis(), 然后通过Spring的IoC容器的方法注入或者 方法替换,将这个方法的逻辑替换为return AopContext.currentProxy()。这样,在调用 自身方法的时候,直接通过getThis () .method2 (); 就可以了。

声明一个Wrapper类,并且让目标对象依赖于这个类。在Wrapper类中直接声明一个getProxy()或者类似的方法,将return AopContext.currentProxy()类似逻辑添加到这个方法中,目 标对象只要getWrapper() .getProxy()就可以取得相应的代理对象。Wrapper类分离了目 标对象与Spring API的直接耦合。至于让这个Wra:Pper以Utility类出现,还是在目标对象中 直接构造,或者依赖注入到目标对象,这就由你来定了。

为类似的目标对象声明统一的接口定义,然后通过BeanPostProcessor处理这些接口实现类, 将实现类的某个取得当前对象的代理对象的方法逻辑覆盖掉。这与方法替换所使用的原理一 样,只不过可以借助Spring的loC容器进行批量处理而已。

至于其他更好的方式,还是留待读者自己去考虑吧,我就不嘐嗦了。

令注意 实际上,这种情况的出现仅仅是因为Spring AOP采用的是代理机制实现.如果像AspectJ 那样,直接将栈切逻辑织入目标对象,那么代理对象和目标对象实际上就合为一体了,调用也 不会出现这样的问题.如果你觉得以上处理方式不好接受,那么改用AspectJ也无妨。不过, 使用中同样会遇到这样或那样的问题,这简直是一定的。我的建议是用自己感觉合适的产品或 者做事方式.

发布了524 篇原创文章 · 获赞 80 · 访问量 15万+

猜你喜欢

转载自blog.csdn.net/xushiyu1996818/article/details/104088790