1. 介绍
1.1面向切面的编程
面向切面的编程(AOP,Aspect Oriented Programming),主要实现的目的是针对业务处理过程中的切面进行提取,它所面对是处理过程中的某个步骤或阶段,以获得逻辑过程中各部分之间低耦合性的隔离效果。
软件设计的一个重要原则,就是要清晰分离各种关注点,然后分而治之,各个击破,最后形成同一的解决方案。然而在目前的技术框架下,通常系统级关注点在逻辑上相互独立,在实现上趋向于和若干核心模块交织。就如下图所示,业务逻辑和安全,日志,一致性交织在了一块,源程序变成一些为不同目的而编写在一起的混乱物。
AOP专为关注点而生,其目标使设计和代码更加模块化、更具结构性,使关注点局部化而不是分散在整个系统中,同时和系统其它部分保持良好定义的接口,从而真正达到不仅在设计上分离,实现上也分离的目的。AOP被认为是后面向对象时代的一种新的重要的程序设计技术。
1.2 AspectJ
AspectJ是一个面向切面编程的框架,它扩展了Java语言。AspectJ定义了AOP语法所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。
编织(weave),是将核心关注点和横切关注点有机融合的过程。其中,核心关关注点表示业务功能和业务逻辑,横切关注点表示业务功能的功能约束。AspectJ目前支持以下三种编织的方式:
※ 编译时编织(compile-time weaving):在编译的过程中进行编织
※ 编译后编织(post-compile weaving):对已经编译好的字节码进行编织
※ 加载时编织(load-time weaving):在类被加载进JVM的时候进行编织,在加载时通过aop.xml对编织的对象进行控制
AspectJ中引入下列这些概念
连接点(Join Point):程序执行过程中明确定义的可以被识别的点或位置。
切入点(Pointcut):连接点的集合,通过连接点标识明确定义需要收集的连接点和有关参数值,它是联系方面和类的桥梁。比如:
pointcut callSetter( ): call (public void HelloWorld.set*(..))
其中:
pointcut说明声明的是一个切入点,命名 callSetter,后面的空括号表示该切入点不需要上下文信息。
call表示该切入点捕获的是对指定方法的调用,指定的方法是在类HelloWorld中声明的公有的、返回值为空、以set开头、拥有任意参数的方法。
通知(Advice):在符合特定条件的情况下执行具体动作的代码的语言构造子称为通知,即声明在连接点被调用时应该执行的动作。依据切入点的语义,将通知分为以下三类:
※ Before advice:在连接点之前执行;
※ After advice :在连接点之后执行;
※ Around advice :代替原连接点处方法的执行。
方面(Aspect):一个方面是封装横切关注点的一个独立的标准单元,类似于面向对象编程中的类。其中包括通知和切入点的定义。
以下是一个简单的例子,例如有一个类HelloWorld,
public class HelloWorld {
public void sayHello() {
System.out.println("Hello, world!");
}
public static void main(String[] argv) {
HelloWorld hw = new HelloWorld();
hw.sayHello();
}
}
现在想在sayHello方法被调用之前和之后多打印一行输出,可编写如下的代码
public aspect SayAspect {
pointcut say():call(void HelloWorld.sayHello());
before():say() {
System.out.println("before say hello....");
}
after():say() {
System.out.println("after say hello....");
}
}
也可以采用标记的方法编写
@Aspect
public class SayAspect {
@Pointcut(“execution(void HelloWorld.sayHello())”)
public void say() {}
@Before(“say()”)
public before(){
System.out.println("before say hello....");
}
@After(“say()”)
public after(){
System.out.println("after say hello....");
}
}
2. 在测试中的使用
2.1测试需求
在测试中测试人员需要构造各种各样的测试场景。但是在一些系统中,特别是在一个复杂的系统中,一些测试场景不能够容易地被创建,比如:
※ 随机性事件,比如需要测试条件A下,程序分别在条件B和条件C下的运行情况。但由于产生条件A在程序中是随机的,测试人员要么需要修改代码,要么只能重复地执行这个用例,直到程序产生条件A为止;
※ 异常,通常测试人员需要求助于不同的第三方的工具来模拟不同的异常,另外有时异常的触发不够直接,时机不好掌握;
※ 复杂的流程,一些bug在特定的交互时序下才会实现,而特定的时序从程序外部无法控制,这时测试人员只有改动程序的代码才能重现。
这里以HDFS中的写流程为例,讲讲在测试过程中遇到的一些问题。下图是一个典型的客户端写3副本的流程。
首先,在测试中需要构造异常在第一个DataNode触发,中间DataNode触发和最后一个DataNode触发的情况。但是,每次客户端构造pipeline中的3个DataNode的顺序是随机的。
其次,目前异常的触发是通过apihook来进行,apihook可以在写指定文件和接收发送来自指定端口的消息时产生异常。但是就如上图所示,create和stream时用的是同一个socket,但是apihook无法区分create和stream过程。
2.2使用AspectJ进行测试
对于测试而言,AspectJ为测试人员提供了一种注入代码的方法,从而可以在不改变原有代码的情况下,对指定方法的行为进行控制,更方便地构造出测试人员想要的场景。并且可以不依赖于其它的工具。
下面我们来看看如何使用AspectJ来解决2.1中提到的hdfs写流程问题,在这里采用加载时编织的方式。
1. 首先根据2.1中提到的问题,编写三个Asepct,然后编译并打成一个jar包,如inject.jar
OrderAspect.java 在Client从NameNode获取到DataNode时,对这些DataNode依据机器名进行重排序
public aspect OrderAspect {
pointcut order():call(* DFSClient.DFSOutputStream.locateFollowingBlock(..));
LocatedBlock around():order() {
LocatedBlock locateBlock = proceed();
/* DataNode的信息存在loacteBlock中,在此进行重排序 */
return locateBlock;
}
}
CreateAspect.java在创建pipeline的时候抛出IOException异常
public aspect CreateAspect {
pointcut pipeline():call(* DataXceiver.writeBlock(..));
before() throws IOException :pipeline() {
throw new IOException("by aspectj");
}
}
StreamAspect.java在传输数据的时候抛出IOException异常
public aspect StreamAspect {
pointcut write():call( * BlockReceiver.receivePacket());
after() throws IOException : write() {
throw new IOException("by aspectj");
}
}
2. 之后就可以通过更改对应结点的aop.xml的配置来控制要注入的代码。例如,现在想测试当中间的DataNode在传输数据的过程中抛出异常的情况,对应结点的aop.xml可以如下图配置。由于通过OrderAspect在建立pipeline之前对DataNode进行了排序,因此我们可以明确哪个DataNode处于pipeline中间的位置。配置好好重启集群,配置项就可以生效了。
3. 在hadoop中,我们可以采取类似的方法使用AsepctJ进行测试。
a) 平常我们可以把对应的测试中要注入的代码合到项目中,在编译整个项目时,这些注入的代码都被打包到一个jar包中,比如hadoop-2-inject.jar。
b) 在执行一个测试用例前,根据测试的需要配置aop.xml(使用加载时编织的方法)控制要注入的代码,并发送到相应的节点上,然后启动集群进行测试。
由于每个测试流程都是一样的,修改aop.xml,分发到对应结点上,启动集群,测试,所以也可以方便的自动化。
apihook是通过一种间接的方式触发我们需要的代码逻辑,而AspectJ提供了一种更直接、方便的方法控制代码逻辑。
3. 使用指南
这里以1.2中HelloWorld为例子,介绍如何使用AspectJ
首先从http://www.eclipse.org/aspectj/下载最新的jar包,并进行解包,并设置变量
ASPECT_LIB =”解包路径/lib”
3.1编译时编织
如果采用编译时编织的方法,则使用以下命令进行编译
java –classpath $ASPECT_LIB/aspectjrt.jar:$ASPECT_LIB/aspectjtools.jar:$CLASSPATH org.aspectj.tools.ajc.Main *.java
运行的时候用如下命令:
java –classpath $ASPECT_LIB/aspectjrt.jar:$CLASSPATH HelloWorld
3.2加载时编织
如果采用加载时编织的方法,首先对HelloWorld.java进行编译
Javac HelloWorld.java
然后对SayAspect.java进行编译,并输出一个aop.xml的文件
java –classpath $ASPECT_LIB/aspectjrt.jar:$ASPECT_LIB/aspectjtools.jar:$CLASSPATH org.aspectj.tools.ajc.Main –outxml SayAspect.java
执行完成后可以看到当前目录下新生成了一个META-INF目录,目录中有一个aop-ajc.xml文件,该文件供编织时使用。
运行的时候用如下命令:
java –classpath $ASPECT_LIB/aspectjrt.jar:$CLASSPATH -javaagent:$ASPECT_LIB/aspectjweaver.jar HelloWorld
(作者:pyjin)