2.3 AOP核心技术
本节主要介绍AOP的核心技术。
2.3.1 AOP名词解释
为了学习AOP的应用开发技术,首先需要了解AOP中专有名词的含义。按照由浅入深的顺序,它包含下列的专有名词。
● 关注点(Concern):一个关注点就是一个特定的目的,一块我们感兴趣的区域。从技术的角度来说,一个典型的软件系统包含一些核心的关注点和系统级的关注点。举个例子来说,一个信用卡处理系统的核心关注点是借贷/存入处理,而系统级的关注点则是日志、事务完整性、授权、安全及性能问题等。许多关注点——我们叫它横切关注点(Crosscutting Concerns)——会在多个模块中出现,使用现有的编程方法,横切关注点会横越多个模块,结果是使系统难以设计、理解、实现和演进;
● 切面(Aspect):如果一个关注点模块化,则这个关注点可能会横切多个对象。事务管理是Java EE应用中一个关于横切关注点的很好的例子。在Spring AOP中,切面可以使用通用类(基于模式的风格)或者在普通类中以@Aspect注解(@AspectJ风格)来实现;
● 连接点(Joinpoint):在程序执行过程中的某个特定的点,比如在某方法调用的时候或者处理异常的时候。在Spring AOP中,一个连接点总是代表一个方法的执行。通过声明一个org.aspectj.lang.JoinPoint类型的参数可以使通知(Advice)的主体部分获得连接点信息;
● 通知(Advice):在切面的某个特定的连接点上执行的动作。通知有各种类型,其中包括“around”、“before”和“after”等。通知的类型将在后面部分进行讨论。许多AOP框架,包括Spring,都以拦截器做通知模型,并维护一个以连接点为中心的拦截器链;
● 切入点(Pointcut):匹配连接点的断连。通知和一个切入点表达式关联,并在满足这个切入点的连接点上运行(例如,当执行某个特定名称的方法时)。切入点表达式及如何与连接点匹配是AOP的核心,Spring默认使用AspectJ切入点语法;
● 引入(Introduction):也被称为内部类型声明(Inter Type Declaration),用来声明额外的方法或者某个类型的字段。Spring允许引入新的接口(及一个对应的实现)到任何被代理的对象。例如,你可以使用一个引入来使bean实现IsModified接口,以便简化缓存机制;
● 目标对象(Target Object):被一个或者多个切面(Aspect)所通知(Advise)的对象。也有人把它叫做被通知(Advised)对象。既然Spring AOP是通过在运行时代理实现的,那么这个对象永远是一个被代理(Proxied)对象;
● AOP代理(AOP Proxy):AOP框架创建的对象,用来实现切面契约(Aspect Contract,包括通知方法执行等功能)。在Spring中,AOP代理可以是JDK动态代理或者CGLIB代理;
注意
Spring 2.0最新引入了基于模式(Schema Based)风格和@AspectJ注解风格的切面声明,对于使用这些风格的用户来说,代理的创建是透明的。
● 织入(Weaving):把切面(Aspect)连接到其他的应用程序类型或者对象上,并创建一个被通知(Advised)的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java AOP框架一样,在运行时完成织入。
2.3.2 AOP通知类型
通知(Advice)是切入点的可执行代码,AOP共包括以下5种类型的通知。
● 前置通知(Before Advice):在某连接点(Join Point)之前执行的通知,但这个通知不能阻止连接点前的执行(除非它抛出一个异常);
● 返回后通知(After Returning Advice):在某连接点(Join Point)正常完成后执行的通知。例如,一个方法没有抛出任何异常,正常返回;
● 抛出异常后通知(After Throwing Advice):在方法抛出异常,退出时执行的通知;
● 后通知(After Finally Advice):当某连接点退出的时候执行的通知(不论是正常返回还是异常退出);
● 环绕通知(Around Advice):包围一个连接点(Join Point)的通知,如方法调用,这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是继续执行连接点,还是直接返回它们自己的返回值或通过抛出异常来结束执行。
在应用AOP时通常会针对以上5种类型的通知来分别编写切入代码。
2.3.3 AOP织入过程
织入(Weave)是将切面应用到目标对象的过程。织入器(一个处理器)组装一个个关注点(这个过程叫做织入),就是依照提供给它的规则把不同的执行逻辑段混编起来的。
为了说明代码织入,让我们来看前面的HelloWorld的例子。通过AspectJ的编译,方面的代码会被编译为下面的类代码。
public class World { public void before_greeting() { System.out.print("Hi, "); } public void after_greeting() { System.out.println("World!"); } }
其中,两个通知函数变成了两个普通的类函数。织入器这时就会使用原有方面代码中匹配的切入点进行织入,产生与如下代码有相同效果的代码。
public class Hello { World world = new World(); public static void sayHello() { world.before_greeting(); System.out.print("Hello "); world.after_greeting(); } public static void main(String[] args) { sayHello(); } }
上面的函数sayHello()的前后就分别被织入了两个通知的函数。
2.3.4 AOP织入策略
下面列出了实现AOP织入的基本策略,并且按从简单到复杂的顺序排列。这些策略在各种不同的开源AOP框架中都有实现。
1.JDK动态代理
动态代理是J2SE 1.3以上版本的内置特性,它允许你凭空(on-the-fly)创建一个或更多接口的实现。动态代理内嵌在JDK中,排除了在各种环境下的奇怪行为带来的风险。JDK动态代理有个限制,就是它只能代理接口不能代理类。当然如果你用接口很好地设计了你的应用,这就不是一个问题了。
在使用动态代理的时候还要用一些反射机制,但在J2SE 1.4以上的JVM中这点性能消耗可以忽略不计。在代理接口时,Spring默认使用JDK动态代理。dynaop这个项目在代理接口时也使用这个策略。
2.字节码动态生成
在代理类时,Spring采用字节码动态生成。CGLIB(Code Generation Library)是做这个的一个流行工具,它通过动态生成子类来拦截方法。这些生成的子类可以改写父类的方法,并能用钩子(Hook)调用拦截器实现。CGLIB被广泛使用于Hibernate中,并且已经被证明是可靠的Java EE解决方案。
但它有一个限制,就是动态生成的子类不能改写和代理final方法。
3.自定义类加载器
使用自定义的类加载器,可以让你通知所创建的实例。这十分强大,因为它提供了修改新操作行为的机会。Jboss AOP和AspectWerkz使用这种方式,根据在XML文件中定义的方式加载和组织类。
这种方式最主要的威胁在于,Java EE服务器必须仔细地控制类加载层次,在一个服务器上工作得很好,可能在另一个服务器上就不能正常工作。
4.语言扩展
AspectJ是Java AOP框架实现的排头兵,它包含了语言的扩展并且使用自带的编译器,而不是使用简单的策略进行切面的组织。
2.3.5 AOP织入时机
对于AOP编程而言,程序的主要逻辑部分和Aspect功能部分的具体实现都可以采用传统的OO技术等实现,这里没有什么新东西。AOP最为特别并使其相对其他方法具有明显优点的部分就在于,它能够以多样的方式将程序中用到的多个方面灵活地织入(Weave)到一起,形成一个完整的应用程序。因而在学习AOP编程时,如何以准确、简洁、灵活的方式将各个不同的方面织入到一起,就成为了我们最需要注意的关键点。
大致上,织入操作可以发生在如下几个阶段。
● 编译时:在对源代码进行编译时,特殊的编译器允许我们通过某种方式指定程序中的各个方面进行Weave的规则,并根据这些规则生成编译完成的应用程序;
● 编译后:根据Weave规则对已经完成编译的程序模块进行Weave操作;
● 载入时:在载入程序模块的时候进行Weave操作;
● 运行时:在程序运行时,根据情况织入程序中的对象和方面。
在表2-1中列出了目前几种主流的AOP系统所支持的织入操作时机。
表2-1 AOP支持Weave时机列表
选择合适的织入时机对于AOP应用来说是非常关键的。针对具体的应用场合,我们需要作出不同的抉择。可以看到,AspectJ为我们提供了最多的选择,即使没有直接支持的运行时Weave,也可以通过一个简单的模式来实现。在使用Spring或JBoss提供的AOP框架时,我们可以利用AspectJ来补足这两个框架的不足之处,从而获得更为灵活的织入策略。
下面来分析不同的织入时机的优缺点。
1.编译时Weave
编译时织入,即在编译Java源代码时,将Aspect源代码织入到Class代码中,如图2-2所示。
图2-2 编译时织入
对于普通应用程序而言,在编译时进行Weave操作是最为直观的做法。由于在源程序中包含了应用的所有信息,因此这种方式通常支持最多种类的联结点。利用编译时Weave,我们能够使用AOP系统进行细粒度的Weave操作,例如读取或写入字段。在源代码编译之后形成的模块将丧失大量的信息,因此通常采用粗粒度的AOP方法。同时,对于传统的编译成为本地代码的语言如C++、Fortran等来说,编译完成后的模块往往跟操作系统平台相关,这就给建立统一的编译后、载入时及运行时Weave机制造成了困难。对于编译成为本地代码的语言而言,只有在编译时进行Weave才最为可行。
尽管编译时Weave具有功能强大、适应面广泛等优点,但它的缺点也很明显。首先,它需要程序员提供所有的源代码,因此对于模块化的项目就有点力不从心了。即使能够提供所有模块的源代码,它也造成了程序不能进行增量编译、编译时间变慢等不利之处。
2.编译后Weave
编译后织入,即是将Aspect的源代码,织入到编译后的Class二进制代码中,以形成新的Class代码,如图2-3所示。
图2-3 编译后织入
为了解决模块化编程的要求,有些AOP框架开始支持编译后Weave的功能。程序员只需要获得编译完成之后的模块,就能进行Weave操作。在AspectJ中,不管是程序的主逻辑部分还是方面,都可以在先编译成为模块之后进行Weave,而且主逻辑部分完全可以采用普通的JavaC编译。而在AspectC中,进行编译后Weave的要求是所有的程序模块都采用AspectC进行编译。可以看出,使用Java这样基于虚拟机的语言对于编写AOP程序是有优势的。
3.加载时Weave
尽管编译后Weave已经解决了在不能获得所有源代码时进行AOP编程的需要的问题,但是在这个框架流行的时代,我们需要更为灵活地安排我们的Weave操作。如果程序的主逻辑部分和Aspect作为不同的组件开发,那么最为合理的Weave时机就是在框架载入Aspect代码之时。如果在进行载入时Weave,则Weave操作之后的结果将不会被保存。程序的主逻辑部分和Aspect部分可以分别进行开发和编译,而Weave操作则在程序载入时发生。
AspectJ、Spring和JBoss都支持载入时Weave。在Spring和JBoss的AOP实现中,框架先于应用程序启动,由框架来负责Weave操作的进行。而在AspectJ中,一个特殊的类加载器被用于这个目的。这个类加载器可以方便地嵌入到框架应用程序中,从而能够为任意的框架提供AOP支持。
4.运行时Weave
运行时Weave可能是所有Weave方式中最为灵活的,程序在运行过程中可以为单个的对象指定是否需要Weave特定的Aspect。在JBoss项目中,利用运行时Weave的特性完成了JBoss Cache项目。在JBoss Cache中,如果一个对象被放置到Cache中,它的状态就将被CacheAOP监视,并且它的状态会被自动同步到一个分布式的缓存中。如果这个对象不需要被缓存,那么它就和AOP不发生任何关系,对它的修改不会引发Cache的同步操作。值得一提的是,尽管AspectJ没有明确提供运行时Weave的能力,但在AspectJ中可以通过一个简单的Pattern实现来运行时Weave。