先说结论:
Spring AOP采用的是JDK动态代理 + CGLIB动态代理模式。当当前类为接口的实现时,采用JDK动态代理,否则用CGLIB、
在传统 OOP 编程里以对象为核心,整个软件系统由系列相互依赖的对象所组成,而这些对象将被抽象成一个一个的类,并允许使用类继承来管理类与类之间一般到特殊的关系。随着软件规模的增大,应用的逐渐升级,慢慢出现了一些 OOP 很难解决的问题。
我们可以通过分析、抽象出一系列具有一定属性与行为的对象,并通过这些对象之间的协作来形成一个完整的软件功能。由于对象可以继承,因此我们可以把具有相同功能或相同特性的属性抽象到一个层次分明的类结构体系中。随着软件规范的不断扩大,专业化分工越来越系列,以及 OOP 应用实践的不断增多,随之也暴露出了一些 OOP 无法很好解决的问题。
现在假设系统中有 3 段完全相似的代码,这些代码通常会采用“复制”、“粘贴”方式来完成,通过这种“复制”、“粘贴”方式开发出来的软件如图 1 所示。
看到如图 1 所示的示意图,可能有的读者已经发现了这种做法的不足之处:如果有一天,图 1 中的深色代码段需要修改,那是不是要打开 3 个地方的代码进行修改?如果不是 3 个地方包含这段代码,而是 100 个地方,甚至是 1000 个地方包含这段代码段,那会是什么后果?
为了解决这个问题,我们通常会采用将如图 1 所示的深色代码部分定义成一个方法,然后在 3 个代码段中分别调用该方法即可。在这种方式下,软件系统的结构如图 2 所示。
图 2 通过方法调用实现系统功能
对于如图 2 所示的软件系统,如果需要修改深色部分的代码,只要修改一个地方即可,不管整个系统中有多少地方调用了该方法,程序无须修改这些地方,只需修改被调用的方法即可——通过这种方式,大大降低了软件后期维护的复杂度。
对于如图 2 所示的方法 1、方法 2、方法 3 依然需要显式调用深色方法,这样做能够解决大部分应用场景。但对于一些更特殊的情况:应用需要方法 1、方法 2、方法 3 彻底与深色方法分离——方法 1、方法 2、方法 3 无须直接调用深色方法,那如何解决?
因为软件系统需求变更是很频繁的事情,系统前期设计方法 1、方法 2、方法 3 时只实现了核心业务功能,过了一段时间,我们需要为方法 1、方法 2、方法 3 都增加事务控制;又过了一段时间,客户提出方法 1、方法 2、方法 3 需要进行用户合法性验证,只有合法的用户才能执行这些方法;又过了一段时间,客户又提出方法 1、方法 2、方法 3 应该增加日志记录;又过了一段时间,客户又提出……面对这样的情况,我们怎么办?通常有两种做法:
第一种做法显然不好,客户是上帝,我们应该尽量满足客户的需求。通常会采用第二种做法,那如何解决呢?是不是每次先定义一个新方法,然后修改方法 1、方法 2、方法 3,增加调用新方法?这样做的工作量也不小啊!我们希望有一种特殊的方法:我们只要定义该方法,无须在方法 1、方法 2、方法 3 中显式调用它,系统会“自动”执行该特殊方法。
上面想法听起来很神奇,甚至有一些不切实际,但其实是完全可以实现的,实现这个需求的技术就是 AOP。AOP 专门用于处理系统中分布于各个模块(不同方法)中的交叉关注点的问题,在 Java EE 应用中,常常通过 AOP 来处理一些具有横切性质的系统级服务,如事务管理、安全检查、缓存、对象池管理等,AOP 已经成为一种非常常用的解决方案。
AspectJ 是一个基于 Java 语言的 AOP 框架,提供了强大的 AOP 功能,其他很多 AOP 框架都借鉴或采纳其中的一些思想。
AspectJ 是 Java 语言的一个 AOP 实现,其主要包括两个部分:第一个部分定义了如何表达、定义 AOP 编程中的语法规范,通过这套语言规范,我们可以方便地用 AOP 来解决 Java 语言中存在的交叉关注点问题;另一个部分是工具部分,包括编译器、调试工具等。
AspectJ 是最早、功能比较强大的 AOP 实现之一,对整套 AOP 机制都有较好的实现,很多其他语言的 AOP 实现,也借鉴或采纳了 AspectJ 中很多设计。在 Java 领域,AspectJ 中的很多语法结构基本上已成为 AOP 领域的标准。
下载、安装 AspectJ 比较简单,读者登录 AspectJ 官网(http://www.eclipse.org/aspectj),即可下载到一个可执行的 JAR 包,使用 java -jar aspectj-1.x.x.jar 命令、多次单击“Next”按钮即可成功安装 AspectJ。
成功安装了 AspectJ 之后,将会在 E:\Java\AOP\aspectj1.6 路径下(AspectJ 的安装路径)看到如下文件结构:
一些文档、AspectJ 入门书籍,一谈到使用 AspectJ,就认为必须使用 Eclipse 工具,似乎离开了该工具就无法使用 AspectJ 了。
虽然 AspectJ 是 Eclipse 基金组织的开源项目,而且提供了 Eclipse 的 AJDT 插件(AspectJ Development Tools)来开发 AspectJ 应用,但 AspectJ 绝对无须依赖于 Eclipse 工具。
实际上,AspectJ 的用法非常简单,就像我们使用 JDK 编译、运行 Java 程序一样。下面通过一个简单的程序来示范 AspectJ 的用法,并分析 AspectJ 如何在编译时进行增强。
首先编写一个简单的 Java 类,这个 Java 类用于模拟一个业务组件。
清单 1.Hello.java
class Hello {
// 定义一个简单方法,模拟应用中的业务逻辑方法
public void sayHello() {
System.out.println("Hello AspectJ!");
}
// 主方法,程序的入口
public static void main(String[] args) {
Hello h = new Hello();
h.sayHello();
}
}
上面 Hello 类模拟了一个业务逻辑组件,编译、运行该 Java 程序,这个结果是没有任何悬念的,程序将在控制台打印“Hello AspectJ”字符串。
假设现在客户需要在执行 sayHello() 方法之前启动事务,当该方法执行结束时关闭事务,在传统编程模式下,我们必须手动修改 sayHello() 方法——如果改为使用 AspectJ,则可以无须修改上面的 sayHello() 方法。
下面我们定义一个特殊的 Java 类。
清单 2.TxAspect.java
public aspect TxAspect {
// 指定执行 Hello.sayHello() 方法时执行下面代码块
void around():call(void Hello.sayHello()){
System.out.println("开始事务 ...");proceed();System.out.println("事务结束 ...");}
}
可能读者已经发现了,上面类文件中不是使用 class、interface、enum 在定义 Java 类,而是使用了 aspect ——难道 Java 语言又新增了关键字?没有!上面的 TxAspect 根本不是一个 Java 类,所以 aspect 也不是 Java 支持的关键字,它只是 AspectJ 才能识别的关键字。
上面粗体字代码也不是方法,它只是指定当程序执行 Hello 对象的 sayHello() 方法时,系统将改为执行粗体字代码的花括号代码块,其中 proceed() 代表回调原来的 sayHello() 方法。
正如前面提到的,Java 无法识别 TxAspect.java 文件的内容,所以我们要使用 ajc.exe 命令来编译上面的 Java 程序。为了能在命令行使用 ajc.exe 命令,需要把 AspectJ 安装目录下的 bin 路径(比如 E:\Java\AOP\aspectj1.6\bin 目录)添加到系统的 PATH 环境变量中。接下来执行如下命令进行编译:
ajc -d . Hello.java TxAspect.java
我们可以把 ajc.exe 理解成 javac.exe 命令,都用于编译 Java 程序,区别是 ajc.exe 命令可识别 AspectJ 的语法;从这个意义上看,我们可以将 ajc.exe 当成一个增强版的 javac.exe 命令。
运行该 Hello 类依然无须任何改变,因为 Hello 类位于 lee 包下。程序使用如下命令运行 Hello 类:
java lee.Hello
运行该程序,将看到一个令人惊喜的结果:
开始事务 …
Hello AspectJ!
事务结束 …
从上面运行结果来看,我们完全可以不对 Hello.java 类进行任何修改,同时又可以满足客户的需求:上面程序只是在控制台打印“开始事务 …”、“结束事务 …”来模拟了事务操作,实际上我们可用实际的事务操作代码来代替这两行简单的语句,这就可以满足客户需求了。
如果客户再次提出新需求,需要在 sayHello() 方法后增加记录日志的功能,那也很简单,我们再定义一个 LogAspect,程序如下:
清单 3.LogAspect.java
public aspect LogAspect
{
// 定义一个 PointCut,其名为 logPointcut
// 该 PointCut 对应于指定 Hello 对象的 sayHello 方法
pointcut logPointcut():execution(void Hello.sayHello());
// 在 logPointcut 之后执行下面代码块
after():logPointcut()
{
System.out.println