AOP(Aspect Orient Programming),也就是面向切面编程,作为面向对象编程的一种补充,当前已经成为一种比较成熟的编程思想,其实AOP问世的时间并不长,甚至在国内的翻译还不太统一(另有人翻译为“面向方面编程”)。AOP和OOP(Object Orient Programming,面向对象编程)互为补充,OOP将程序分解成各个层次的对象,而AOP则将程序运行过程分解成各个切面。可以这样理解:OOP是从静态角度考虑程序结构,而AOP则从动态角度考虑程序运行过程。
为什么需要AOP
在传统OOP变成立,以对象为核心,整个软件系统由系列相互依赖的对象组成,而这些对象被抽象成一个一个的类,并允许使用类继承来管理类与类之间从一般到特殊的关系。随着软件规模的增大,应用的逐渐升级,慢慢出现了一些OOP很难解决的问题。
我们可以通过分析、抽象出一系列具有一定属性与行为的对象,并通过这些对象之间的协作来形成一个完整的软件功能。由于对象可以继承,因此我们可以把具有相同功能或相同特性的属性抽象到一个层次分明的类结构体系中。随着软件规模的不断扩大,专业化分工越来越系列,以及OOP应用实践的不断增多,随之也暴露出一些OOP无法很好解决的问题。
假设系统中有3段完全相似的代码,这些代码通常会采用“复制”、“粘贴”方式来完成,通过这种复制和粘贴完成的代码在后期将很难维护:想想一下,如果有一天,这些被复制和粘贴的代码需要修改,那么,是不是会修改这3处呢?如果这段代码被复制和粘贴了100遍呢,1000遍呢,如何维护?大多数人会想到将这段代码抽取出来,作为一个公共的方法,在需要使用这段代码的地方,调用这个方法即可。这样如果这段代码需要修改,只需要修改这个公共的方法即可。但实际的情况是:即使将公共的部分抽取出来了,每个地方还是需要去显式调用这个方法,这能够解决大部分问题。但是对于一些更加特殊的情况:应用需要将公共的部分与调用的地方彻底分离,那又应该如何解决呢?
因为软件系统需求变更时很频繁的事情,假设系统前期设计方法1、2、3时只实现了核心业务,一段时间之后,我们需要对这些方法都进行事务控制;又过了一段时间,客户提出这些方法需要进行合法的用户验证,只有合法的用户才能调用这些方法;又过了一段时间,客户又提出这些方法需要增加日志记录;又过了一段时间……面对这种问题,我们应该怎么办呢?是不是每次先定义一个新的方法,然后再去修改方法1、2、3增加调用新的方法的代码块呢?这样做的工作量也不小啊!
我们希望有一种特殊的方法:我们只要定义该方法,无需在方法1、2、3中显式调用它,系统会“自动”调用该方法。
注意:上面的自动被加上了引号,是因为在编程过程中,没有所谓的自动的事情,在程序的世界里,任何事情都是由代码驱动的。这里的自动是指,无需开发者关心,由系统来驱动。
上面的想法听起来很神奇,甚至有些不切实际,但其实是完全可以实现的,实现这个需求的技术就是AOP。AOP专门用于处理系统中分布于各个模块(不同方法)中的交叉关注点的问题,在JavaEE应用中,常常通过AOP来处理一些具有横切性质的系统级服务,例如事务管理、安全检查、缓存、对象池等等,AOP已经成为一种非常常用的方案。
使用AspectJ实现AOP
AspectJ是一个基于Java语言的AOP框架,提供了强大的AOP功能,其他很多AOP框架都借鉴或采纳其中的一些思想。由于Spring 3.0的AOP与AspectJ进行了很好的集成,因此掌握AspectJ是学习Spring AOP的基础。
AspectJ是Java语言的一个AOP实现,其主要包括两个部分:第一个部分定义了如何表达、定义AOP编程中的语法规范,通过这个规范,我们可以方便的用AOP来解决Java语言中存在的交叉关注点的问题;另一个部分是工具部分,包括编译器、调试工具等。
AspectJ是最早、功能比较强大的AOP实现之一,对嵌套AOP机制都有较好的实现,很多其他语言的AOP实现,也借鉴或采纳了AspectJ中的很多设计。在Java领域,AspectJ中的很多语法结构基本上已成为AOP领域的标准。
从Spring 2.0开始,Spring AOP已经引入了对AspectJ的支持,并允许直接使用AspectJ进行AOP编程,而Spring自身的AOP API也努力与AspectJ保持一致。因此,学习SpringAOP就必然需要从AspectJ开始,因为它是Java领域最流行的AOP解决方案,我们甚至可以直接使用AspectJ进行AOP编程。
- AspectJ的下载和安装(安装AspectJ之前,请确保系统已经安装了JDK)。
- 下载AspectJ的最新稳定版: http://www.eclipse.org/aspectj/downloads.php#stable_release 下载下来后是一个jar包。
打开命令行,cd到该jar包所在的文件夹,运行java -jar aspectj-1.7.4.jar命令,打开AspectJ的安装界面。第一个界面是欢迎界面,直接next。
第二个界面中,选择jre的安装路径,next。
- 第三个界面中,选择AspectJ的安装路径,next。因为安装过程的实质是解压一个压缩包,并不需要太多的依赖于系统,因此路径可以任意选择,这里我选择和Java安装在一起。
- 安装完成后,按照界面提示,需要配置classpath和PATH,这里不做介绍。
至此,AspectJ安装完成。
AspectJ提供了编译、运行AspectJ的一些工具命令,这些命令放在AspectJ的bin目录下,而lib路径下的aspectjrt.jar则是AspectJ的运行时环境,所以我们需要分别添加这两个环境变量。
- AspectJ的使用入门
成功安装AspectJ后,在其安装目录下有如下结构:
bin:该路径下存放了AspectJ的常用命令,其中ajc最为常用,其作用类似于javac,用于对普通Java类进行编译时增强
docs:该路径下存放了AspectJ的使用说明、参考手册和API等文档
lib:该路径下的4个jar文件是AspectJ运行的核心类库
license和readme文件
这里要提到的是,一些文档、AspectJ的入门书籍,一谈到使用AspectJ,就认为必须使用Eclipse工具,似乎离开了该工具就不能使用AspectJ了。实际上,虽然AspectJ是Eclipse基金组织的开源项目,而且提供了Eclipse的AJDT(AspectJ Development Tools)插件来开发AspectJ应用,但AspectJ绝对无需依赖于Eclipse工具。
AspectJ的用法很简单,就像我们使用JDK编译、运行Java程序一样。下面通过一个简单的程序来示范AspectJ的用法:
public class HelloWorld {
public void sayHello(){
System.out.println("Hello AspectJ!");
}
public static void main(String args[]) {
HelloWorld h = new HelloWorld();
h.sayHello();
}
}
毫无疑问,结果将输出"Hello AspectJ!"字符串。假设现在客户需要在执行sayHello方法前启动事务,当该方法结束时关闭事务,在传统编程模式下,我们必须手动修改sayHello方法——如果改为使用AspectJ,则可以无需修改上面的sayHello方法。下面我们定义一个特殊的“类”:
public aspect TxAspect {
void around():call(void sayHello()) {
System.out.println("Transaction Begin");
proceed();
System.out.println("Transaction End");
}
}
可能有人已经发现,上面的类文件中不是使用class、interface或者enum来定义Java类,而是使用aspect——难道Java语言又增加关键字了?No!上面的TxAspect根本不是一个Java类,所以aspect也不是Java支持的关键字,它只是AspectJ才认识的关键字。
上面"void around"中的内容也不是方法,它只是指定当程序执行HelloWorld对象的sayHello方法时,执行这个代码块,其中proceed表示调用原来的sayHello方法。正如前面提到的,Java无法识别TxAspect.java文件中的内容所以我们需要使用ajc.exe来执行编译:
ajc HelloWorld.java TxAspect.java
我们可以把ajc命令理解为javac命令,都用于编译Java程序,区别是ajc命令可以识别AspectJ的语法。从这个角度看,我们可以将ajc命令当成一个增强版的javac命令。
运行该HelloWorld类依然无需任何改变:
java HelloWorld
其结果将是:
从上面的运行结果来看,我们可以完全不对HelloWorld.java文件做修改,也不用对执行HelloWorld的命令做修改,就可以实现上文中的实现事务管理的需求。上面的“Transaction Begin”和“Transaction End”仅仅是模拟事务的事件,实际开发中,用代码替换掉这段输出即可实现事务管理。
如果客户又提出了为方法增加日志的需求,那也很简单,我们可以再定义一个LogAspect类,如下:
//同样使用aspect作为“关键字”
public aspect LogAspect {
//定义一个名为logPointcut的PointCut,对应于HelloWorld对象的sayHello方法
pointcut logPointcut():execution(void HelloWorld.sayHello());
//在logPointcut之后指定下面的代码
after():logPointcut() {
System.out.println("Log Recoding");
}
}
上面的代码中定义了一个PointCut——logPointcut,等同于执行HelloWorld对象的sayHello方法,并指定在logPointcut之后执行简单的代码块,也就是说,在sayHello方法结束之后执行输出语句。使用如下命令编译这几个java文件:
ajc *.java
再次运行HelloWorld类,将输出以下结果:
由此可见,通过使用AspectJ提供的AOP支持,我们可以为sayHello方法不断增加新功能。
实际上,AspectJ允许同时为多个方法添加新功能,只要我们定义Pointcut时指定匹配更多的方法即可,下面是一个代码片段:
pointcut xxxPointcut:execution(void H*.say*());
上面的程序中的xxxPointcut将可以匹配所有以H开头的类,以say开头的方法名和返回值为void类型的所有方法。AspectJ甚至允许下面的形式:
pointcut xxxPointcut:execution(* H*.say*());
如果装有Java反编译工具,可以将HelloWorld.class进行反编译,我们将发现该HelloWorld.class文件不是由HelloWorld.java文件编译得到的,HelloWorld.class里面增加了很多新的内容——这表明AspectJ在编译时已经增强了HelloWorld.class的功能,因此AspectJ通常被称为编译时增强的AOP框架。
拓展:与AspectJ相对的还有另外一种AOP框架,它们不需要在编译的时候对目标类进行增强,而是运行时生成目标类的代理类,该代理类要么实现了目标类实现的相同接口,要么是目标类的子类。总之,代理类都对目标类进行了增强处理,前者是JDK动态代理的处理策略,后者是CGLIB代理的处理策略。Spring AOP以创建动态代理的方式来生成代理类,底层既可使用JDK动态代理,也可以采用CGLIB代理。一般来说,编译时增强的AOP框架在性能上更有优势——因为运行时动态增强的AOP框架需要每次运行时都进行动态增强。
【未完,待续】