AO技术学习之AspectJ

关键词:Aspect Oriented,AspectJ,面向方面
作者:BIce 创建时间:2012-05-02 15:35:33

         由于最近正在对Aspect Oriented技术进行深入学习,遂记录一下关于AO,主要是关于AO典型技术AspectJ的总结。

         AO(Aspect Oriented)技术的提出是由Xereo Palo Alto Research Center1990年提出的,其真正迈入实用是在近几年,提出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技术也扩展到了各个主流开发语言阵营:

JavaAspectJSpring AOPJBoss AOP等等

C++Aspect C++

C#Aspect #等等

         其中发展得最好的是Java阵营的AOP技术,其中AspectJ是比较好的、用得也比较多的AOP技术,本文主要记录对Java阵营的AOPAspectJ的学习。

3. AOP技术的主要概念

本节主要介绍一下AOP技术的主要概念。

         AOP技术主要有两个功能:(不修改源程序的情况下)

1) 修改源程序的动态执行流:也就是修改源代码的执行逻辑

2) 修改源程序的静态属性:向源代码中Java类的加入新的属性或方法(AspectJ支持,Spring AOPJBoss AOP不支持)

为了实现这两个功能,首先先定义一些概念(AOP框架一般都有的概念)。

3.1 joinpoint

         joinpoint连接点:主要只在OO源代码中可以被AO技术进行切入的执行点,在AspectJ中,Java类方法的调用、属性的访问、异常的出现等,都可以作为AspectJ的连接点。joinpointJava源代码中存在的某些位置,我们在AOP中要给出如何指出这些点的方法。

3.2 pointcut

         pointcut切入点:首先pointcutjoinpoint,我们可以通过AOP语法来定义pointcut,也就是我们将要切入的某些joinpoint

3.3 advice

         advice通知:在定义了pointcut之后,我们也就定义好了一些Java源码中的固定点,advice是定义我们要在pointcut切入的具体操作。

3.4 mixin

         mixin混入:和其意义一样,Mixin的动作是向一个已有的源代码中混入新的属性或方法的方式。

3.5 aspect

         aspect方面:和Java中的类类似,有抽象和继承的结构,是AOP技术中封装以上pointcutadvicemixin等元素的元素,一般表现为一个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 AOPJBoss AOP使用修改类路径的方式来实现,需要完全实现被织入的类),此方法动态性较好,在本质上支持方面的动态模型,提供方面的热部署,且有一定执行效率优势,也是一种主流的编织方式。

3.         运行时织入:

在运行时需要被织入的类已经加载到了JVM,通过拦截和基于代理的机制完成切入点匹配来完成Aspect的织入。具体实现使用解释器修改、反射、动态代理和Just In Time(JIT)技术来实现。缺点是只能在方法调用级进行织入且执行效率比较低。

5. AspectJ技术小结

         上面介绍了AOP技术的概念和原理,下面就简单总结下AspectJ技术中的这些概念的定义和实现的机制。

         AspectJ使用类修改,在编译时修改被织入类.class字节代码的方式来实现Aspect的织入,下面总结一下AspectJAOP概念的定义语法。

5.1 joinpoint连接点

         AspectJjoinpoint有:

方法调用、方法调用执行、构造器连接点、字段引用、字段赋值、类静态初始化、对象初始化、异常处理执行。

joinpoint是源代码中的一些点,在pointcut中使用,它的签名如下。

连接点分类

连接点签名

解释

方法调用

访问修饰符 返回值类型 类名.方法名(参数列表)

调用处,参数结合之前

方法执行

同上

函数体,参数结合之后

构造器调用

访问修饰符 类名.new(参数列表) throws 异常

构造器方法调用,同方法调用

构造器执行

同上

构造器方法执行,同方法执行

对象初始化

同上

执行完父类构造器调用,本对象构造函数返回之前时

字段引用

访问修饰符 字段类型 类名.字段名

读取字段值时

字段赋值

同上

修改字段值时

类静态初始化

Pointcut中指明类名即可

static{}块中代码执行时

异常处理执行

Pointcut中指明异常类别即可

某类异常触发,进入Catch块时

 

5.2 pointcut切入点

         AspectJpointcut就是源程序中的某些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()

AspectAdvice执行时触发

 

2.         控制流切入点:

匹配从属于某控制流中的连接点有cflowcflowbelow两种

cflow

cflow(切入点)

匹配在某个切入点开始的控制流(如某个方法调用的执行体)中出现的所有连接点,包括切入点本身定义的连接点,获取的连接点太多,一般不建议单独使用,可以加上&&进行条件限定来指定切入点

cflowbelow

cflowbelow(切入点)

同上,但不包括切入点本身定义的连接点,典型使用是用来搜索非递归调用,也不建议单独使用。

 

3.         词汇结构切入点:

匹配一段源代码中的连接点有withinwithincode两种

within

within(类名)

对某个类中的所有连接点进行匹配

withincode

withincode(方法签名)

对某个方法中的所有连接点进行匹配

 

另外,在定义切入点的时候还可以定义匿名的切入点,在要使用切入点的地方用出去访问限定符和切入点名的定义式即可

5.2.2 切入点定义-通配符

         在定义切入点的时候,我们使用通配符来简化切入点的定义,通配符有*..+三种。

*

匹配任意数量的任何字符,不包括“.

..

代表任意类型,任何个数的参数

+

表示子类型,包括子类和子接口

 

5.2.3 切入点定义-逻辑运算

         在定义切入点的时候,有时需要定义比较复杂的切入点,我们可以通过使用逻辑操作符来完成定义。逻辑操作符有与(&&),或(||),非(!)三种,它们的操作数都必须是Pointcut,有了逻辑操作符我们就可以定义比较复杂的切入点。

         另外可以通过获取的上下文参数使用if(表达式)来对连接点进行匹配。

5.2.4 切入点定义-上下文匹配和获取

         在定义切入点的时候,我们还可以利用上下文,来对连接点进行匹配,上下文一共有三种:thistargetargs。它们不但可以用于匹配连接点,还可以在对应Advice中使用。(thistarget都不匹配静态方法连接点)

this

this(类名或对象标识符)

如参数为类别则用来匹配当前的执行对象(当前执行在哪个对象的代码中)的类别,

如参数为对象标识符则是为了匹配连接点时,将执行对象返回给切入点

target

target(类名或对象表示符)

如参数为类别则用来匹配call,set,get方法的目标对象类,与this用法一样,

如参数为对象标识符则用来将调用、或者访问属性所在对象(被调用方)返回给切入点,常与call一起使用

args

args(类型或对象标识符列表)

用来获取传递给连接点的参数

         需要注意的是,所有this,target,args中以获取上下文为目的的标识符,一定要在切入点的参数中给出定义[类型 标识符]才可以。

5.3 advice

         切入点定义完毕,我们以及确定了需要织入Aspect的位置,那么Aspect的具体操作呢?这就是advice的内容。

         Advice是在pointcut匹配的joinpoint处执行的代码,根据应用到连接点的时间不同,分为三类beforeafteraround

before

pointcut …

before(Advice要使用的参数,包括thistargetargs):

pointcut Name(参数标识符){

  //执行体

}

在连接点之前执行

around

和上面类似,不过around需要指定一个返回类型

替换连接点代码块的执行,其返回类型要与对应的连接点相同,如果要执行原方法,则要调用proceed(),否则不会执行。如果调用proceed()且获取了上下文,传递给proceed方法的上下文要与获取的上下文数量类型一样。

after

分为

afterafter()

after returningafter() returning

after throwingafter() 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和类一样,是一个封装的容器,它包含了PointcutAdviceMixin等的实现,aspect是一个实现横切的基本单元,可以有自己的属性和方法,甚至有自己的继承体系(可以继承于一个类或方面,实现某个接口,有抽象方面的概念等),但是方面不能直接实例化对象。

5.6 AspectJ执行流程

         执行AspectJ的时候,我们需要使用ajc编译器,对Aspect和需要织入的Java Source Code进行编译,得到字节码后,可以使用java命令来执行。

         ajc编译器会首先调用javacJava源代码编译成字节码,然后根据我们在Aspect中定义的pointcut找到相对应的Java Byte Code部分,使用对应的advice动作修改源代码的字节码,同时根据mixin的内容修改Java字节码,对其进行mixin操作。最后得到了经过Aspect织入的Java字节码,然后就可以正常使用这个字节码了。

5.7 AspectJ反射机制

         Java一样,AspectJ也提供了类似的反射机制,用于在advice执行体中访问一些连接点相关的信息。AspectJ提供的反射对象一共有三个,分别是thisJoinPointthisJoinPointStaticPartthisEnclosingJoinPointStaticPart

thisJoinPoint

包含连接点的动态信息,可以访问thistarget对象和参数,标签名,连接点类型等等。

thisJoinPointStaticPart

可以访问到连接点的静态信息,包括连接点类型,连接点调用对象和签名信息

thisEnclosingJoinPointStaticPoint

包含了连接点的静态信息,也就是连接点的上下文,不同类型的连接点,封装内容有所不同。

5.8 AspectJ的优先级

         在对于某个切入点,有多个advices的情况,我们需要一种给方面定义优先级的机制,aspectJ的优先级定义机制如下:

1.         方面之间的优先级

我们可以单独建立一个Aspect文件来定义方面之间的优先级关系,其定义语法如下:

declare precedence : Aspect1 ,Aspect2…

这样定义的优先级是Aspect1的优先级高于Aspect2.

2.         advice的优先级

三种advicebefore,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.参考文献

         《面向方面软件开发的理论、技术与实践》 王斌,盛津芳 主编。

 

留言功能已取消,如需沟通,请邮件联系博主sunswk@sina.com,谢谢:)