当OOP/OOSD(Object-Oriented Software Development)被提出来,以取代过去的基于过程化编程的开发方法的时候,或许那个时代的人都会以为,面向对象编程和面向对象的软件开发(OOP/OOSD)就是我们一直所追求的那颗能够搞定一切的“银弹”。即使面向对象的软件开发模式,依然不能很好地解决软件开发中的所有问题。
对于系统中普遍的业务关注点,OOP可以很好地对其进行分解并使之模块化,但却无法更好地避免类似系统需求的实现在系统中各处散落这样的问题。
AOP全称为Aspect-Oriented Programming,中文通常翻译为面向方面编程。使用AOP,我们可以对类似于Logging和Security等系统需求进行模块化的组织。
Aspect之对于AOP,就相当于Class之对于OOP。AOP仅是对OOP方法的一种补足,当我们把以Class形式模块化的业务需求和以Aspect形式模块化的系统需求拼装在一起的时候,整个系统就算完成了。
现时代的AOP实现都需要“寄生”于OOP的主权领土中,系统的维度也依然保持曾经OOP持有的“维度世界纪录”。
AOP是一种理念,要实现这种理念通常需要一种实现的方式。与OOP需要相应的语言支持一样,AOP也需要某种语言以帮助实现相应的概念实体,我们统称这些实现AOP的语言为AOL,即Aspect-Oriented Language。
将AOP组件集成到OOP组件的过程中,在AOP中称之为织入(Weare)过程。
将AOP的Aspect织入到OOP系统的实现方式可谓千差万别。织入过程是处于AOP和OOP的开发工程之外的,而且对于整个系统的实现是透明的,开发者只需要关注相应的业务需求实现,或者系统需求的实现即可。当所有业务需求和系统需求以模块化的形式开发完成之后,通过织入过程就可以将整个的软件系统集成并付诸使用。
静态AOP的优点是,Aspect直接以Java字节码的形式编译到Java类中,Java虚拟机可以像通常一样加载Java类运行,不会对整个系统的运行造成任何的性能损失。就是灵活性不够。如果横切关注点需要改变织入到系统的位置,就需要重新修改Aspect定义文件,然后使用编译器重新编译Aspect并重新织入到系统中。
Aspect跟Class一样最终以Class身份作为系统的一等公民存在,与静态AOP最大的不同就是,AOP的织入过程在系统运行开始之后进行,而不是预先编译到系统类中,而且织入信息大都采用外部XML文件格式保存,可以在调整织入点已经织入逻辑单元的同时,不必变更系统其他模块,甚至在系统运行的时候,也可以动态更改织入逻辑。但动态AOP在引入灵活性以及易用性的同时,也会不可避免的引入相应的性能问题。
Java平台上的AOP实现机制
动态代理:
JDK1.3之后,引入了动态代理(Dynamic Proxy)机制,可以在运行期间,为相应的接口动态生成对应的代理对象。所以,我们可以将横切关注点逻辑封装到动态代理的InvocationHandler中,然后在系统运行期间,根据横切关注点需要织入的模块位置,将横切逻辑织入到相应的代理类中。这种方法实现的唯一缺点或者说优点就是,所有需要织入横切关注点逻辑的模块类都得实现相应的接口,因为动态代理机制只针对接口有效。Spring AOP默认情况下采用这种机制实现AOP机能。
动态字节码增强:
可以为需要织入横切逻辑的模块类在运行期间,通过字节码增强技术,为这些系统模块类生成相应的子类,而将横切逻辑加入到这些子类中,让应用程序在执行期间使用的是这些动态生成的子类,从而达到将横切逻辑织入系统的目的。Spring AOP在无法采用动态代理机制进行AOP功能扩展的时候,会使用CGLIB库的动态字节码增强支持来实现AOP的功能扩展。
自定义类加载器:
可以通过自定义类加载器的方式完成横切逻辑到系统的织入,自定义类加载器通过读取外部文件规定的织入规则和必要信息,在加载class文件期间就可以将横切逻辑添加到系统模块类的现有逻辑中,然后将改动后的class交给Java虚拟机运行。
AOP的相关重要概念
Joinpoint
在系统运行之前,AOP的功能模块都需要织入到OOP的功能模块中。所以,要进行这种织入过程,我们需要知道在系统的哪些执行点上进行织入操作,这些将要在其之上进行织入操作的系统执行点就称之为Joinpoint。基本上,只有允许,程序执行过程中的任何点都可以作为横切逻辑的织入点,而所有这些执行点都是Joinpoint。
常见的Joinpoint类型:
方法调用(Method Call)。当某个方法被调用的时候所处的程序执行点。
方法调用执行(Method Call execution)。称之为方法执行或许更简洁,该Joinpoint类型代表的是某个方法内部执行开始时点,应该与方法调用类型的Joinpoint进行区分。方法调用是在调用对象的执行点,而方法执行则是在被调用到的方法逻辑执行的时点。对于同一对象,方法调用要先于方法执行。
构造方法调用(Constructor Call)。程序执行过程中对某个对象调用其构造方法进行初始化的时点。
字段设置(Field Set)
字段获取(Field Get)
异常处理执行(Exception Handler Execution)。该类型的Joinpoint对应程序执行过程中,在某些类型异常抛出后,对应的异常处理逻辑执行的时点。
Pointcut
Pointcut的概念代表的是Joinpoint的表达方式。将横切逻辑织入当前系统的过程中,需要参照Pointcut规定的Joinpoint信息,才可以知道应该往系统的哪些Joinpoint上织入横切逻辑。
Pointcut的表达方式
直接指定Joinpoint所在的方法名称。这种形式的Pointcut表达方式比较简单,而且功能单一,通常只限于支持方法级别Joinpoint的AOP框架,或者只是方法调用类型的Joinpoint。即使是只针对方法级别的Joinpoint,因为系统中需要织入横切逻辑的方法可能很多,一个一个地指定则过于不便。
正则表达式。这是比较普遍的Pointcut表达式,可以充分利用正则表达式的强大功能,来归纳表达需要符合某种条件的多组Joinpoint。
使用特定的Pointcut表达语言。这是一种最为强大的表达Pointcut的方式,灵活性也很好,但其具体实现起来可能过于复杂。Spring从发布2.0版本之后,借助于AspectJ的Pointcut表述语言解释器,现在也支持使用AspectJ的Pointcut表达语言来指定Pointcut。
Advice
Advice是单一横切关注点逻辑的载体,它代表将会织入到Joinpoint的横切逻辑。如果将Aspect比作OOP中的Class,那么Advice就相当于Class中的Method。按照Advice在Joinpoint位置执行时机的差异或者完成功能的不同,Adive可以分成多种具体形式。
1、Before Advice
Befor Advice是在Joinpoint指定位置之前执行的Advice类型。通常,它不会中断程序执行流程,但如果必要,可以通过在Before Advice中抛出异常的方式来中断当前程序流程。
2、After Advice
After Advice就是在相应连接点之后执行的Advice类型。
3、Around Advice
Around Advice对附加其上的Joinpoint进行“包裹”,可以在Jointpoint之前和之后都指定相应的逻辑,甚至于中断或者忽略Joinpoint处原来程序流程的执行。
4、Introduction
Introduction不是根据横切逻辑在Joinpoint处的执行时机来区分的,而是根据它可以完成的功能而区分别于其他Advice类型。Introduction可以为原有对象添加新的特性或者行为。
Aspect
Aspect是对系统中的横切关注点逻辑进行模块化封装的AOP概念实体。Aspect可以包含多个Joinpoint已经相关Advice定义。Spring AOP最初没有“完全”确切的实体对应真正的Aspect的概念。在2.0发布之后,因为集成了AspectJ,所以可以通过使用@AspectJ的注解并结合普通的POJO来声明Aspect。
织入和织入器
只有经过织入过程之后,以Aspect模块化的横切关注点才会集成到OOP的现存系统中,而完成织入过程的那个,就称之为织入器(Weaver)。AOP使用一组类来完成最终的织入操作,ProxyFactory类则是Spring AOP中最通用的织入器。Java平台各AOP实现的织入器形式不一而足,唯一相同的就是他们的职责,即完成横切关注点逻辑到系统的最终织入。
目标对象
符合Pointcut所指定的条件,将在织入过程中被织入横切逻辑的对象,称为目标对象(Target Object)。
Spring AOP的实现机制
动态代理
动态代理机制的实现主要由一个类和一个接口组成,即java.lang.reflect.Proxy类和java.lang.reflect.InvocationHandler接口。当Proxy动态生成的代理对象上相应的接口方法被调用时,对应的InvocationHandler就会拦截相应的方法调用,并进行相应的处理。InvocationHandler就是我们实现横切逻辑的地方,它是横切逻辑的载体,作用跟Advice是一样的。动态代理机制只能对实类现了相应Interface的类使用,如果某个类没有实现如何的Interface,就无法使用动态代理机制为其生成相应的动态代理对象。默认情况下,如果Spring AOP发现目标对象实现了相应的Interface,则采用动态代理的机制为其生成代理对象实例。而如果目标对象没有实现如何Interface,Spring AOP会尝试使用一个称为CGLIB(Code Generation Library)的开源的动态字节码生成类库,为目标对象生成动态的代理对象实现。
动态字节码生成
使用动态字节码生成技术扩展对象行为的原理是,我们可以对目标对象进行继承扩展,为其生成相应的子类,而子类可以通过覆写来扩展父类的行为,只要将横切逻辑的实现放到子类中,然后让系统使用扩展后的目标对象子类,就可以达到与代理模式相同的效果了。CGLIB可以对实现了某种接口的类,或者没有实现任何接口的类进行扩展。使用CGLIB对类进行扩展的唯一限制就是无法对final方法和类进行覆写。
Spring AOP中的Joinpoint
AOP的Joinpoint可以有许多种类型,如构造方法调用、字段的设置及获取、方法调用、方法执行等。但是,在Spring AOP中,仅支持方法级别的Joinpoint。
Spring中以接口org.springframework.aop.Pointcut作为其AOP框架中所有Pointcut的最顶层抽象,该接口定义了两个方法来帮忙捕捉系统中的相应Joinpoint,并提供了一个TruePointcut类型实例。
常见的Pointcut:
1、NameMatchMethodPointcut
最简单的Pointcut实现,属于StaticMethodMatcherPointcut的子类,可以根据自身指定的一组方法名称与Joinpoint处的方法的方法名称进行匹配。NameMathchMethodPointcut除了可以指定方法名,以对指定的Joinpoint进行匹配,还可以使用“*”通配符,实现简单的模糊匹配。
2、JdkRegexpMehtodPointcut和Perl5RegexpMethodPointcut
StaticMethodMatcherPointcut的子类中有一个专门提供基于正则表达式的实现分支,以抽象类AbstractRegexpMethodPointcut为统帅。与NameMatchMethodPointcut相似,AbstractRegexpMethodPointcut声明了pattern和patterns属性,可以指定一个或多个正则表达式的匹配模式。