AOP技术学习之AspectJ
由于最近正在对Aspect Oriented技术进行深入学习,遂记录一下关于AO,主要是关于AO典型技术AspectJ的总结。
AO(Aspect Oriented)技术的提出是由Xereo Palo Alto Research Center在1990年提出的,其真正迈入实用是在近几年,提出AO为了解决的是在OO技术中,由于横切(Cross-cut)关系的存在,导致模块化下降的问题。需要注意的是:AO技术不是为了替代OO技术出现的,它的出现是为了对OO技术进行辅助和增强
1.基本概念
1.1关注点
在AO技术中,软件系统可以由一组关注点来组成,关注点是系统开发过程中所关心的方面,如系统的功能、安全性和性能等等。(可以理解为一系列需求,只不过这些需求由不同的Stakeholder来关注)
1.2横切关注点
虽然在OO技术中我们已经强调了模块化、封装的作用,也已经有了如类、包等模块化机制,但是系统中还是存在的一些关注点(主要来自系统的非功能性需求)典型如:系统的业务逻辑、系统数据持久存储、系统安全性和系统日志,不能在一个单独模块中完成。虽然它们的实现执行代码可以封装到一个单独模块中,但是关于它们的调用代码会分散到系统的各个模块,导致了大量代码的重复和纠缠。如上所示的日志等关注点这就是AO技术中提出的横切关注点(Cross-cut Concern),它与系统中很多需要用到它的关注点,都有横切关系。实现横切关注点的代码不能在一个模块完成,分散于很多实现其他关注点的模块中。
由于有横切关注点的存在,导致了系统中存在着大量的重复代码,也就间接导致了系统的维护和修改困难加大,OO技术对这点是没有办法的,这也就是AO提出的基本出发点:实现关注点分离、模块化横切关注点,提高系统模块化性质。
2. AO的发展和应用
到了今天,AO技术已经发展成为一个可以在工程中实际应用的技术,AO技术现在的形式有:AOP(面向方面编程,典型体现为AspectJ):AO与现有OO技术的具体融合技术;AOSD(面向方面的软件开发):已经是一套从需求到实现都有的比较完整软件工程方法。
另外关于AO技术的应用,现在已经扩展到各个领域,甚至可以应用于OO技术的软件测试中(由于可以不修改代码来修改控制流,可以提供类似Mock的方式.)。
而AOP技术也扩展到了各个主流开发语言阵营:
Java:AspectJ、Spring AOP、JBoss AOP等等
C++:Aspect C++
C#:Aspect #等等
其中发展得最好的是Java阵营的AOP技术,其中AspectJ是比较好的、用得也比较多的AOP技术,本文主要记录对Java阵营的AOP:AspectJ的学习。
3. AOP技术的主要概念
本节主要介绍一下AOP技术的主要概念。
AOP技术主要有两个功能:(不修改源程序的情况下)
1) 修改源程序的动态执行流:也就是修改源代码的执行逻辑
2) 修改源程序的静态属性:向源代码中Java类的加入新的属性或方法(AspectJ支持,Spring AOP和JBoss AOP不支持)
为了实现这两个功能,首先先定义一些概念(AOP框架一般都有的概念)。
3.1 joinpoint
joinpoint连接点:主要只在OO源代码中可以被AO技术进行切入的执行点,在AspectJ中,Java类方法的调用、属性的访问、异常的出现等,都可以作为AspectJ的连接点。joinpoint是Java源代码中存在的某些位置,我们在AOP中要给出如何指出这些点的方法。
3.2 pointcut
pointcut切入点:首先pointcut是joinpoint,我们可以通过AOP语法来定义pointcut,也就是我们将要切入的某些joinpoint。
3.3 advice
advice通知:在定义了pointcut之后,我们也就定义好了一些Java源码中的固定点,advice是定义我们要在pointcut切入的具体操作。
3.4 mixin
mixin混入:和其意义一样,Mixin的动作是向一个已有的源代码中混入新的属性或方法的方式。
3.5 aspect
aspect方面:和Java中的类类似,有抽象和继承的结构,是AOP技术中封装以上pointcut、advice、mixin等元素的元素,一般表现为一个java文件。
AOP的使用
一般,我们使用AOP实现Aspect的时候,首先要使用Pointcut指定出在源程序中想要切入的点,然后定义具体要进行修改的动作Advice和需要mixin的内容,形成Aspect文件,最后就可以使用AOP具体框架的织入机制对需要进行织入的部分进行处理了。
4. AOP的原理
这里简单介绍AOP技术实现的原理。在前面说过AOP技术支持两个功能:在不修改源程序的情况下修改源程序的动态执行流和静态属性(AspectJ支持),那么AOP是如何实现这两个功能的呢?
Aspect实现策略
Aspect的实现策略有四种:类包装器、类替代、类修改、解释器修改,下面简单介绍。
1. 类包装器:
在调用类和被调用类(要进行AO切入的类)之间加入一个中间类,中间类可以使用代理模式或者继承的方法,这个中间类就是经过AO切入的类了,使调用类调用这个中间类,就可以完成AO对被调用类的修改了。(Spring AOP就使用动态代理的方式)
2. 类替代:
使用经AO技术修改后的类,替换修改之前的类,来达到对目标类的修改。具体可使用改变类路径或者创建自定义类加载器的形式来实现。(JBoss AOP使用)
3. 类修改:
主流的AOP实现技术,通过自定义AOP编译器,将Aspect与目标类代码进行编译,修改目标类代码生成的.class字节代码来修改目标类的执行流和属性和方法。需要借助BCEL等修改源代码字节代码的工具。(AspectJ使用)
4. 解释器修改:
通过修改JVM解释器的执行流程来进行AO编织。(如Prose使用Java Platform Debugger Architecture来实现)通过JVM截获的目标方法调用,然后对方法调用进行Advice来实现。比较负载,效率比较低,且仅应用于某些JVM。
Aspect Weave时间策略
Aspect 何时织入目标代码也是个很重要的问题,时间大概分为编译时织入、加载时织入和运行时织入。
1. 编译时织入:
在编译阶段就将Aspect织入目标代码,AspectJ使用的就是这种,它通过使用自定义的编译器将Aspect 织入目标类的.class字节代码中,来完成Aspect的织入。优点是执行效率高,缺点是编译时间长。
2. 加载时织入:
通过修改类路径或者自定义类加载器的方式在类加载时进行拦截,修改需要织入方面的类,实现Aspect的织入。(Spring AOP和JBoss AOP使用修改类路径的方式来实现,需要完全实现被织入的类),此方法动态性较好,在本质上支持方面的动态模型,提供方面的热部署,且有一定执行效率优势,也是一种主流的编织方式。
3. 运行时织入:
在运行时需要被织入的类已经加载到了JVM,通过拦截和基于代理的机制完成切入点匹配来完成Aspect的织入。具体实现使用解释器修改、反射、动态代理和Just In Time(JIT)技术来实现。缺点是只能在方法调用级进行织入且执行效率比较低。
5. AspectJ技术小结
上面介绍了AOP技术的概念和原理,下面就简单总结下AspectJ技术中的这些概念的定义和实现的机制。
AspectJ使用类修改,在编译时修改被织入类.class字节代码的方式来实现Aspect的织入,下面总结一下AspectJ中AOP概念的定义语法。
5.1 joinpoint连接点
AspectJ的joinpoint有:
方法调用、方法调用执行、构造器连接点、字段引用、字段赋值、类静态初始化、对象初始化、异常处理执行。
joinpoint是源代码中的一些点,在pointcut中使用,它的签名如下。
连接点分类 | 连接点签名 | 解释 |
方法调用 | 访问修饰符 返回值类型 类名.方法名(参数列表) | 调用处,参数结合之前 |
方法执行 | 同上 | 函数体,参数结合之后 |
构造器调用 | 访问修饰符 类名.new(参数列表) throws 异常 | 构造器方法调用,同方法调用 |
构造器执行 | 同上 | 构造器方法执行,同方法执行 |
对象初始化 | 同上 | 执行完父类构造器调用,本对象构造函数返回之前时 |
字段引用 | 访问修饰符 字段类型 类名.字段名 | 读取字段值时 |
字段赋值 | 同上 | 修改字段值时 |
类静态初始化 | 在Pointcut中指明类名即可 | 类static{}块中代码执行时 |
异常处理执行 | 在Pointcut中指明异常类别即可 | 某类异常触发,进入Catch块时 |
5.2 pointcut切入点
AspectJ的pointcut就是源程序中的某些joinpoint,是实际织入动作(advice)的触发条件,具体定义如:
public pointcut myPointcut():call(void BookObject.setBookClassName(string));
由例子可看出,定义一个pointcut,大概形式为:
访问描述符|pointcut关键字|pointcut名字|需要的参数|pointcut类型|要切入的joinpoint签名
5.2.1 切入点分类定义
切入点大致可以分为:分类切入点,控制流切入点,词汇结构切入点三类,下面简单给出定义及具体用法解释。
1. 分类切入点:
连接点种类 | 切入点语法 | 解释 |
方法调用 | call(方法签名) | 在方法被调用时触发 |
方法执行 | execution(方法签名) | 在方法执行时触发,和call的区别是,假设A.method1()中调用了B.method2(),那么如果方法签名为B.method2(),使用call,则Advice会被织入到方法的调用类A.method1()中,而使用execution则会使Advice织入到B.method2()中。(call在参数结合之前,execution在参数结合之后) |
构造器调用 | call(构造器签名) | 构造器函数调用时触发 |
构造器执行 | execution(构造器签名) | 构造器函数执行时触发,与构造器函数调用区别类似方法执行和触发的区别 |
对象初始化 | initialization(构造器签名) | 执行完父类构造器调用,本对象构造函数返回之前时 |
对象预初始化 | preinitialization(构造器签名) | 发生在进入捕获构造函数之后,以及调用任何超类构造函数之前 |
字段引用 | get(字段签名) | 访问字段值触发 |
字段赋值 | set(字段签名) | 设置字段值触发 |
类静态初始化 | staticinitialization(类名) | 类执行Static体时触发 |
异常处理 | handler(异常类型) | 出现某种异常时,进入Catch块时触发 |
通知执行 | adviceexecution() | 在Aspect的Advice执行时触发 |
2. 控制流切入点:
匹配从属于某控制流中的连接点有cflow和cflowbelow两种
cflow | cflow(切入点) | 匹配在某个切入点开始的控制流(如某个方法调用的执行体)中出现的所有连接点,包括切入点本身定义的连接点,获取的连接点太多,一般不建议单独使用,可以加上&&进行条件限定来指定切入点 |
cflowbelow | cflowbelow(切入点) | 同上,但不包括切入点本身定义的连接点,典型使用是用来搜索非递归调用,也不建议单独使用。 |
3. 词汇结构切入点:
匹配一段源代码中的连接点有within和withincode两种
within | within(类名) | 对某个类中的所有连接点进行匹配 |
withincode | withincode(方法签名) | 对某个方法中的所有连接点进行匹配 |
另外,在定义切入点的时候还可以定义匿名的切入点,在要使用切入点的地方用出去访问限定符和切入点名的定义式即可
5.2.2 切入点定义-通配符
在定义切入点的时候,我们使用通配符来简化切入点的定义,通配符有*,..,+三种。
* | 匹配任意数量的任何字符,不包括“.” |
.. | 代表任意类型,任何个数的参数 |
+ | 表示子类型,包括子类和子接口 |
5.2.3 切入点定义-逻辑运算
在定义切入点的时候,有时需要定义比较复杂的切入点,我们可以通过使用逻辑操作符来完成定义。逻辑操作符有与(&&),或(||),非(!)三种,它们的操作数都必须是Pointcut,有了逻辑操作符我们就可以定义比较复杂的切入点。
另外可以通过获取的上下文参数使用if(表达式)来对连接点进行匹配。
5.2.4 切入点定义-上下文匹配和获取
在定义切入点的时候,我们还可以利用上下文,来对连接点进行匹配,上下文一共有三种:this、target、args。它们不但可以用于匹配连接点,还可以在对应Advice中使用。(this和target都不匹配静态方法连接点)
this | this(类名或对象标识符) | 如参数为类别则用来匹配当前的执行对象(当前执行在哪个对象的代码中)的类别, 如参数为对象标识符则是为了匹配连接点时,将执行对象返回给切入点 |
target | target(类名或对象表示符) | 如参数为类别则用来匹配call,set,get方法的目标对象类,与this用法一样, 如参数为对象标识符则用来将调用、或者访问属性所在对象(被调用方)返回给切入点,常与call一起使用 |
args | args(类型或对象标识符列表) | 用来获取传递给连接点的参数 |
需要注意的是,所有this,target,args中以获取上下文为目的的标识符,一定要在切入点的参数中给出定义[类型 标识符]才可以。
5.3 advice
切入点定义完毕,我们以及确定了需要织入Aspect的位置,那么Aspect的具体操作呢?这就是advice的内容。
Advice是在pointcut匹配的joinpoint处执行的代码,根据应用到连接点的时间不同,分为三类before,after,around
before | pointcut … before(Advice要使用的参数,包括this,target,args): pointcut Name(参数标识符){ //执行体 } | 在连接点之前执行 |
around | 和上面类似,不过around需要指定一个返回类型 | 替换连接点代码块的执行,其返回类型要与对应的连接点相同,如果要执行原方法,则要调用proceed(),否则不会执行。如果调用proceed()且获取了上下文,传递给proceed方法的上下文要与获取的上下文数量类型一样。 |
after | 分为 after:after() after returning:after() returning after throwing:after() throwing 三种 | after:无论连接点执行如何都会执行 after returning:只有在连接点正常返回之后才会执行 after throwing:只有在连接点抛出异常后才执行 |
5.4 mixin
AspectJ还提供对于目标代码进行静态属性修改的特性,主要有下面几种类型的修改:
1. 向目标代码中加入新方法和属性
如果想要向目标代码中加入新的方法和属性,可以在将对它进行织入的Aspect中定义相应的方法和属性即可(声明方式和普通Java类的方法属性一致)。
2. 修改目标类的继承关系或实现接口
AspectJ支持一种叫做declare parents的机制,可以用来改变目标类的继承关系和实现接口,在将对目标代码进行织入的Aspect中加入:
declare parents: Foo implements IM1;
public …// IM1中的接口实现
来对Foo类实现IM1接口
5.5 aspect
Aspect和类一样,是一个封装的容器,它包含了Pointcut、Advice、Mixin等的实现,aspect是一个实现横切的基本单元,可以有自己的属性和方法,甚至有自己的继承体系(可以继承于一个类或方面,实现某个接口,有抽象方面的概念等),但是方面不能直接实例化对象。
5.6 AspectJ执行流程
执行AspectJ的时候,我们需要使用ajc编译器,对Aspect和需要织入的Java Source Code进行编译,得到字节码后,可以使用java命令来执行。
ajc编译器会首先调用javac将Java源代码编译成字节码,然后根据我们在Aspect中定义的pointcut找到相对应的Java Byte Code部分,使用对应的advice动作修改源代码的字节码,同时根据mixin的内容修改Java字节码,对其进行mixin操作。最后得到了经过Aspect织入的Java字节码,然后就可以正常使用这个字节码了。
5.7 AspectJ反射机制
和Java一样,AspectJ也提供了类似的反射机制,用于在advice执行体中访问一些连接点相关的信息。AspectJ提供的反射对象一共有三个,分别是thisJoinPoint,thisJoinPointStaticPart,thisEnclosingJoinPointStaticPart。
thisJoinPoint | 包含连接点的动态信息,可以访问this,target对象和参数,标签名,连接点类型等等。 |
thisJoinPointStaticPart | 可以访问到连接点的静态信息,包括连接点类型,连接点调用对象和签名信息 |
thisEnclosingJoinPointStaticPoint | 包含了连接点的静态信息,也就是连接点的上下文,不同类型的连接点,封装内容有所不同。 |
5.8 AspectJ的优先级
在对于某个切入点,有多个advices的情况,我们需要一种给方面定义优先级的机制,aspectJ的优先级定义机制如下:
1. 方面之间的优先级
我们可以单独建立一个Aspect文件来定义方面之间的优先级关系,其定义语法如下:
declare precedence : Aspect1 ,Aspect2…
这样定义的优先级是Aspect1的优先级高于Aspect2.
2. advice的优先级
三种advice:before,around,after,不论他们从属的Aspect优先级如何,before一定连接点之前触发,after一定在连接点之后触发,而around则复杂些,优先级高的around封装了优先级低的around,也就是只有在高优先级的around执行了proceed()之后,优先级低的around才可能会执行。
3. 方面内部的优先级
一个Aspect内部的,对应与一个连接点的同类型Advice,按照书写顺序在前的优先级高。
4. 当方面直接有继承关系时,子方面的优先级高于父方面的。
5. 在mixin的时候,如果有重名的属性或方法出现,优先级高的mixin会覆盖掉优先级低的。
5.9 AspectJ异常软化
异常软化是AspectJ的一个应用,它可以使某个特定切入点的可控异常转化为不可控异常,它取消了在调用者方法中处理或抛出异常的必要性(如果是可控异常,正常的Java代码要求调用放进行Try Catch处理或者添加Throw声明,否则语法错误)。在使用了异常软化之后,AspectJ替我们完成了Try Catch处理,简化了我们的工作
异常软化的语法如下:
declare soft: 异常类别 :切入点[execution|call 匹配的方法]
使用execution,加入的try catch在实际的被调用方法中,使用call,加入的try catch将在调用方法中。
6.参考文献
《面向方面软件开发的理论、技术与实践》 王斌,盛津芳 主编。