AspectJ 动态代理的实际演练
1. AOP 的概念引入
AspectJ 从其命名就可以看出其强调面向切面编程,什么是面向切面编程?
Aspect Oriented Programming(AOP),俗称面向切面编程。切面是相对于方法栈的入栈和压栈而言的。你可以想象每一个方法都是一块面包片,而切面就是面包片之间的间隙。
下图是一个方法栈:


刀从吐司最前端开始切,每新切一刀,相当于程序计数器 +1。
面向切面编程的含义就是将方法栈切开,然后在合适的切面上涂点苹果酱、花生酱。如果在吐司片后面涂,相当于在这个代码段执行后执行一些额外方法,如果在一片吐司片正面涂,相当于在这个代码块执行前执行一些额外的代码。
AOP 涉及一些该领域的专业习惯用语,其中比较重要的有:
- 通知( Advice):切面的工作被称为通知,换句话说,第三方功能即为通知。
- 按照通知在切面作用顺序,分为:前置通知 Before、后置通知 After、环绕通知 Around。按功能分还有返回通知、异常通知。
- 连接点(Join point):描述一种能力:可能插入切点的方法栈点,比如调用的方法、抛出异常时、修改一个字段时。
- 切点(Pointcut):真正插入第三方功能的程序点。
- 切面(Aspect):通知+切点,即第三方类库干什么和在哪里做。
- 织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。创建的代理类既可以在编译期间完成,也可以在运行期间完成。
2. AspectJ 是什么?
能实现 AOP 思想的工具有很多,比如 JDK 动态代理、CGLIB 动态字节码增强技术以及 AspectJ,AspectJ 和其他技术最大的区别就在于其是在编译期进行,而另外两者都是在运行时执行。具体来说,它们的区别可以用下图表示:
技术 |
代理逻辑 |
内部原理 |
JDK 动态代理 |
动态代理 |
在运行时将被代理类封装到代理类中,然后产生代理类实例 |
CGLIB 动态代理 |
动态代理 |
在运行时增强被代理类的字节码,直接生成字节码增强后的代理类实例 |
AspectJ |
静态代理 |
在编译期间将切面织入被代理类中,对运行时生成代理类实例的逻辑透明 |
AspectJ 有着其独特的语法特性,具体可以看 The AspectJTM Programming Guide,其提供了两套配置方式:
- 基于 Java 注解的配置方式;
- 基于 aspect 文件的切面描述方法,通常需要 IDE 安装额外的插件才能够进行语法检查;
AspectJ 的主要作用就是作为 Java 的一个面向 AOP 的框架而存在,下一节将讲述最朴素地使用 AspectJ 的用法。
3. AspectJ 的用法
aspectJ.jar 解压有的目录如下:
1
2
3
4
5
6
7
8
9
10
11
|
bin/
├── aj
├── aj5
├── ajbrowser
├── ajc
└── ajdoc
lib/
├── aspectjrt.jar
├── aspectjtools.jar
├── aspectjweaver.jar
└── org.aspectj.matcher.jar
|
这当中重点的文件是四个 jar 包中的前三个,bin 文件夹中的脚本其实都是调用这些 jar 包的命令。
- aspectjrt.jar 包主要是提供运行时的一些注解,静态方法等等东西,通常我们要使用 aspectJ 的时候都要使用这个包。
- aspectjtools.jar 包主要是提供赫赫有名的 ajc 编译器,可以在编译期将将 java 文件或者 class 文件或者 aspect 文件定义的切面织入到业务代码中。通常这个东西会被封装进各种 IDE 插件或者自动化插件中。
- aspectjweaverjar 包主要是提供了一个 java agent 用于在类加载期间织入切面 (Load time weaving)。并且提供了对切面语法的相关处理等基础方法,供 ajc 使用或者供第三方开发使用。这个包一般我们不需要显式引用,除非需要使用 LTW。
上面的说明其实也就指出了 aspectJ 的几种标准的使用方法 (参考文档):
- 编译时织入,利用 ajc 编译器替代 javac 编译器,直接将源文件 (java 或者 aspect 文件) 编译成 class 文件并将切面织入进代码。
- 编译后织入,利用 ajc 编译器向 javac 编译期编译后的 class 文件或 jar 文件织入切面代码。
- 加载时织入,不使用 ajc 编译器,利用 aspectjweaver.jar 工具,使用 java agent 代理在类加载期将切面织入进代码。
三种用法如下图所示:

基于 Maven 插件自动构建的 Aspect 方式实际上采用的是方式 ②,下一节会有提到。
4. AspectJ 的具体使用案例
4.1 基于 aspectj 文件的 AspectJ
这种说法比较蛋疼,其实我想说明的是这种不兼容 javac 的一种切面表示形式。比如当前我们有一个业务类 App.java:
1
2
3
4
5
6
7
8
9
10
11
|
public class App {
public void say() {
System.out.println("App say");
}
public static void main(String[] args) {
App app = new App();
app.say();
}
}
|
我们希望对在 say 函数里加一个切面, 那就创建一个 AjAspectj.aj 的文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public aspect AjAspect {
//定义切面
pointcut say():
execution(* App.say(..));
//切面之前调用
before(): say() {
System.out.println("AjAspect before say");
}
//切面之后调用
after(): say() {
System.out.println("AjAspect after say");
}
}
|
这样我们就能实现切面的功能。可这个 aj 文件的语法虽然跟 java 很类似,但是毕竟还是不能用 javac 来编译,如果我们要用这个的话就必须使用 ajc 编译器。使用的方法大概有这几种:
- 调用命令直接编译 (直接使用 ajc 命令或者调用 java -jar aspectjtools.jar);
- 使用 IDE 集成的 ajc 编译器编译;
- 使用自动化构建工具的插件编译;
其实 2,3 两点的本质都是使用 asp ectjtools.jar,最简单的调用方法如下:
1
2
3
4
5
6
|
#!/usr/bin/env bash
ASPECTJ_TOOLS=/home/myths/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar
ASPECTJ_RT=/home/myths/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -sourceroots .
|
调用 aspectjtools.jar 包,指定 aspectjrt 的 classpath,以及需要编译的路径,这样就会生成 AjAspectj.aj 以及 App.java 对应的 class 文件。我们反编译一下看看:
AjAspectj.class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
import java.io.PrintStream;
import org.aspectj.lang.NoAspectBoundException;
public class AjAspect
{
private static Throwable ajc$initFailureCause;
public static final AjAspect ajc$perSingletonInstance;
public static AjAspect aspectOf()
{
if (ajc$perSingletonInstance == null) {
throw new NoAspectBoundException("AjAspect", ajc$initFailureCause);
}
return ajc$perSingletonInstance;
}
public static boolean hasAspect()
{
return ajc$perSingletonInstance != null;
}
private static void ajc$postClinit()
{
ajc$perSingletonInstance = new AjAspect();
}
static
{
try
{
}
catch (Throwable localThrowable)
{
ajc$initFailureCause = localThrowable;
}
}
//切面方法的定义
public void ajc$before$AjAspect$1$682722c()
{
System.out.println("AjAspect before say");
}
public void ajc$after$AjAspect$2$682722c()
{
System.out.println("AjAspect after say");
}
}
|
App.class:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
import java.io.PrintStream;
public class App
{
public void say()
{
try
{
AjAspect.aspectOf().ajc$before$AjAspect$1$682722c();//织入了切面
System.out.println("App say");
}
catch (Throwable localThrowable)
{
AjAspect.aspectOf().ajc$after$AjAspect$2$682722c();//织入了切面
throw localThrowable;
}
AjAspect.aspectOf().ajc$after$AjAspect$2$682722c();
}
public static void main(String[] args)
{
App app = new App();
app.say();
}
}
|
可以看出 AspectJ 的织入原理的特点:
不过,虽然事实上这种基于 aj 文件的切面描述方法比基于 java 注解的切面描述方法用起来要灵活的多,但是由于他无法摆脱 ajc 的支持,而且本身不兼容 java 语法导致难以统一编码规范,加上需要较多额外的学习成本,因此事实上很多项目还是不怎么用这种方式,更多的还是采用了兼容 java 语法的用注解定义切面的方式。
4.2 基于 Java 注解的 AspectJ
下面我们主要还是着力考虑下基于 java 注解的切面使用方法。
先建一个普通的项目看看,老样子,从 maven 的 maven-archetype-quickstart 开始,pom.xm 文件里我们一般只需要加上 AspectJ Runtime 的依赖即可,但是由于后续需要利用到 AspectJ 的编译工具,因此还要引入 AspectJ Tools 依赖与 AspectJ Weaver 依赖。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.8.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
|
如果使用高版本,可能会遇到命令不兼容的错误。
创建 Proxyee 类:
1
2
3
4
5
|
public class Proxyee {
public void sayHello(String str){
System.out.println("Hello " + str);
}
}
|
创建切面类 ProxyAspect 类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
@Aspect
public class ProxyAspect {
//定义切点
@Pointcut("execution(* aspectjproxy.Proxyee.sayHello(..))")
public void jointPoint(){}
//定义前置通知
@Before("jointPoint()")
public void before(){
System.out.println("-------Log.log()-------");
}
//定义后置通知
@After("jointPoint()")
public void after(){
System.out.println("-------Log.log()-------");
}
}
|
接下来我们就来尝试下 4 种不同的编译方式。
4.2.1 编译时织入
编译时织入其实就是使用 ajc 来进行编译,暂时不使用自动化构建工具,我们先在项目根目录下手动写一个编译脚本 compile.sh:
1
2
3
4
5
6
|
#!/usr/bin/env bash
ASPECTJ_WEAVER=~/.m2/repository/org/aspectj/aspectjweaver/1.8.13/aspectjweaver-1.8.13.jar
ASPECTJ_RT=~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
ASPECTJ_TOOLS=~/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar
java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -source 1.5 -sourceroots src/main/java/aspectjproxy -d target/classes
|
- -cp 参数指定 aspectjrt.jar 的路径;
- -source 1.5 参数指定支持 java1.5 以后的注解;
- -sourceroots 指明编译的文件夹;
- -d 指明输出路径;
注意事项:这里需要你将路径修改为自己本地 Maven 项目 jar 包的存放路径。
此时成的字节码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
|
package aspectjproxy;
import org.aspectj.lang.NoAspectBoundException;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class ProxyAspect {
public ProxyAspect() {
}
@Before("jointPoint()")
public void before() {
System.out.println("-------Log.log()-------");
}
@After("jointPoint()")
public void after() {
System.out.println("-------Log.log()-------");
}
//单例模式
public static ProxyAspect aspectOf() {
if (ajc$perSingletonInstance == null) {
throw new NoAspectBoundException("aspectjproxy.ProxyAspect", ajc$initFailureCause);
} else {
return ajc$perSingletonInstance;
}
}
public static boolean hasAspect() {
return ajc$perSingletonInstance != null;
}
static {
try {
ajc$postClinit();
} catch (Throwable var1) {
ajc$initFailureCause = var1;
}
}
}
|
以及被代理类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
package aspectjproxy;
public class Proxyee {
public Proxyee() {
}
public void sayHello(String str) {
try {
ProxyAspect.aspectOf().before();//前置织入
System.out.println("Hello " + str);//原方法
} catch (Throwable var3) {
ProxyAspect.aspectOf().after();//后置织入
throw var3;
}
ProxyAspect.aspectOf().after();
}
}
|
可见:注解版本的编译器对应的字节码可读性更好一点,但是实现的内部原理是相同的:AspectJ 注解类还是通过单例模式被代理类的调用,最终实现编译时织入。
4.2.2 编译后织入
1
2
3
4
5
6
7
8
9
10
11
12
|
#!/usr/bin/env bash
# 编译后织入,即使用的是 ajc 编译器间接来编译 class 文件,前置的 class 文件由 javac 编译器生成
## 第一步使用 javac 命令先生产原始的 class 文件
javac -cp ~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar -d target/classes src/main/java/aspectjproxy/*.java
## 第二步接着使用 ajc 编译器接收上述 class 文件,然后生产新的 class 文件并覆盖原文件
ASPECTJ_WEAVER=~/.m2/repository/org/aspectj/aspectjweaver/1.8.13/aspectjweaver-1.8.13.jar
ASPECTJ_RT=~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
ASPECTJ_TOOLS=~/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar
java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -source 1.5 -inpath target/classes/aspectjproxy -d target/classes
|
第一步中生成的 .class 文件实际上就是 javac 直接生成的字节码文件,没有实现 AspectJ 的方法织入,如下所示:
1
2
3
4
5
6
7
8
9
10
|
package aspectjproxy;
public class Proxyee {
public Proxyee() {
}
public void sayHello(String var1) {
System.out.println("Hello " + var1);
}
}
|
以及
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package aspectjproxy;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class ProxyAspect {
public ProxyAspect() {
}
@Pointcut("execution(* aspectjproxy.Proxyee.sayHello(..))")
public void jointPoint() {
}
@Before("jointPoint()")
public void before() {
System.out.println("-------Log.log()-------");
}
@After("jointPoint()")
public void after() {
System.out.println("-------Log.log()-------");
}
}
|
第二步则会生成同编译时织入方式一样的字节码文件。
4.2.3 加载时织入 (LTW)
LTW 的含义是:Load-time weaving,类加载时织入。
前两种织入方法都依赖于 ajc 的编译工具,LTW 却通过 java agent 机制在内存中操作类文件,可以不需要 ajc 的支持做到动态织入。
为了实现 LTW,我们需要在资源目录下配置 META-INF/aop.xml 文件,来告知类加载器我们当前注册的切面。
在上面的项目中,我们其实只需要创建 src/main/resources/META-INF/aop.xml:
1
2
3
4
5
|
<aspectj>
<aspects>
<aspect name="aspectjproxy.ProxyAspect"/>
</aspects>
</aspectj>
|
这样,我们就可以先使用 javac 编译源文件,再使用 java agent 在运行时织入:
1
2
3
4
5
6
7
|
#!/usr/bin/env bash
# 编译时织入,即使用的是 ajc 编译器直接来编译 java 文件,生产 class 文件
ASPECTJ_WEAVER=~/.m2/repository/org/aspectj/aspectjweaver/1.8.13/aspectjweaver-1.8.13.jar
ASPECTJ_RT=~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
ASPECTJ_TOOLS=~/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar
java -javaagent:$ASPECTJ_WEAVER -cp $ASPECTJ_RT:target/classes/ aspectjproxy.Proxyee
|
控制台输出:
——-Log.log()——-
Hello Spongecaptain
——-Log.log()——-
注意事项:因为这里是加载时进行织入,因此必须要有 main 方法,因此这里手下你要对代理类加入 main 方法,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class Proxyee {
public void sayHello(String str){
System.out.println("Hello " + str);
}
public static void main(String[] args) {
final Proxyee proxyee = new Proxyee();
proxyee.sayHello("Spongecaptain");
}
}
|
4.2.4 maven 自动化构建
显然,自己写脚本还是比较麻烦的,如果用如 maven 这样的自动化构建工具的话就会方便很多,codehaus 提供了一个 ajc 的编译插件 aspectj-maven-plugin,我们只需要在 build/plugins 标签下加上这个插件的配置即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.10</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<complianceLevel>1.8</complianceLevel>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
|
这个插件会绑定到编译期,采用的应该是编译后织入的方式,在 maven-compiler-plugin 处理完之后再工作的。插件其实是对 aspectjtools.jar 的一个 mojo 封装。
注意事项:每一种方式执行前应当将上一个方式产生的 class 文件删除掉,避免这一步执行的是上一步产生的结果。
第四节主要引用于:https://blog.mythsman.com/post/5d301cf2976abc05b34546be
3. AspectJ 和 Spring AOP 之间的关系
Spring AOP 底层没有使用 AspectJ 来实现,其基于 JDK 动态代理以及 CGLIB 运行时字节码增强:
- 被代理的方法属于接口:使用 JDK 动态代理;
- 被代理的方法不属于接口:使用 CGLIB 运行时字节码增强;
那么为什么 Spring AOP 依赖于 AspectJ 呢?
事实上 Spring AOP 仅仅使用了 AspectJ 的相关注解以及 AspectJ 的相关切面概念和语法。因此 Spring AOP 实际上仅仅依赖于 AspectJ 的 aspectjrt.jar 包,其主要是提供运行时的一些注解,静态方法等等东西。而其他两个用于产生织入代码的工具 jar 包并没有依赖。
你可以在 org.springframework.aop jar 包中看到很多类都依赖于 org.aspectj.lang.annotation
packages,而后者位于 AspectJ 的 aspectjrt.jar 包中。