AOP:Aspect-Oriented Programming,面向切面编程,是一种新的方法论(编程范式),是对传统 OOP(Object-Oriented Programming,面向对象编程)的补充。旨在通过允许横切关注点的分离,提高模块化。如在方法执行前、或执行后、或是在执行中出现异常后这些地方进行拦截处理或叫做增强处理。主要应用于:日志收集、事务管理、安全检查、缓存、对象池管理等。
AOP实现的关键就在于AOP框架自动创建的AOP代理,AOP代理则可分为静态代理(例如:spring aop)和动态代理(例如:原生AspectJ)两大类,其中静态代理是指使用AOP框架提供的命令进行编译,从而在编译阶段就可生成 AOP 代理类,因此也称为编译时增强;而动态代理则在运行时借助于JDK动态代理、CGLIB等在内存中“临时”生成AOP动态代理类,因此也被称为运行时增强。
AOP基本概念:
切入点(pointcut):在哪些类、哪些方法上切入,通常是一个正则表达式执行点(JoinPoint):通过pointcut选取出来的集合中的具体的一个执行点,我们就叫JoinPoint通知(advice):在方法前、方法后、方法前后、异常等做什么。切面(aspect):切面 = pointcut + advice。即在什么时机、什么地方、做什么。织入(weaving):把切面加入对象,并创建出代理对象的过程。 二、AspectJ介绍
AspectJ:全称Eclipse AspectJ,官网The AspectJ Project | The Eclipse Foundation 是Java社区里最完整最流行的AOP框架,即AOP的java版实现,它定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。(除了AspectJ外,还有很多AOP实现,例如ASMDex)
aspectJ可以单独使用,也可以整合到其它框架中。单独使用AspectJ时需要使用专门的编译器ajc。java的编译器是javac,AspectJ的编译器是ajc,aj是首字母缩写,c即compiler。
1、AspectJ原理:AspectJ属于静态织入,通过修改代码来实现,有如下几个织入的时机:
- 编译期织入(Compile-time weaving): 如类 A 使用 AspectJ 添加了一个属性,类 B 引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。编译后织入(Post-compile weaving): 也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。类加载后织入(Load-time weaving): 指的是在加载类的时候进行织入,要实现这个时期的织入,有几种常见的方法。1、自定义类加载器来干这个,这个应该是最容易想到的办法,在被织入类加载到 JVM 前去对它进行加载,这样就可以在加载的时候定义行为了。2、在 JVM 启动的时候指定 AspectJ 提供的 agent:-javaagent:xxx/xxx/aspectjweaver.jar。
AspectJ可以做Spring AOP干不了的事情,它是AOP编程的完全解决方案,Spring AOP则致力于解决企业级开发中最普遍的AOP(方法织入)。而不是成为像AspectJ一样的AOP方案。因为AspectJ在实际运行之前就完成了织入,所以说它生成的类是没有额外运行时开销的。
| Spring AOP | AspectJ |
| 在纯 Java 中实现 | 使用 Java 编程语言的扩展实现 |
| 不需要单独的编译过程 | 除非设置 LTW,否则需要 AspectJ 编译器 (ajc) |
| 只能使用运行时织入 | 运行时织入不可用。支持编译时、编译后和加载时织入 |
| 功能不强-仅支持方法级编织 | 更强大 - 可以编织字段、方法、构造函数、静态初始值设定项、最终类/方法等......。 |
| 只能在由 Spring 容器管理的 bean 上实现 | 可以在所有域对象上实现 |
| 仅支持方法执行切入点 | 支持所有切入点 |
| 代理是由目标对象创建的, 并且切面应用在这些代理上 | 在执行应用程序之前 (在运行时) 前, 各方面直接在代码中进行织入 |
| 比 AspectJ 慢多了 | 更好的性能 |
| 易于学习和应用 | 相对于 Spring AOP 来说更复杂 |
在实际生产中,我们用得最多的还是 Spring AOP。
2、AspectJ的使用:https://javadoop.com/post/aspectj前面介绍了 Spring AOP 的各种用法,包括随着 Spring 的演进而发展出来的几种配置方式。但是我们始终没有使用到 AspectJ,即使是在基于注解的 @AspectJ 的配置方式中,Spring 也仅仅是使用了 AspectJ 包中的一些注解而已,并没有依赖于 AspectJ 实现具体的功能,接下来将介绍AspectJ的使用。
2.1)代码如下
1)maven依赖:
org.aspectj aspectjrt1.8.13 org.aspectj aspectjweaver1.8.13
2)定义一个类,对其进行weaving:
package a.b.c.tftest.model;
public class Account {
public int balance = 20;
public boolean pay(int amount) {
if (balance < amount) {
return false;
}
balance -= amount;
return true;
}
}
3)接下来,我们定义两个Aspect来进行weaving演示:
A、AccountAspect:
用 AspectJ 的语法来写,对交易进行拦截,如此次交易超过余额,直接拒绝。AccountAspect 需要以 .aj 结尾,如我们在a.b.c.tftest.aspectj 下新建文件 AccountAspect.aj,内容如下:
package a.b.c.tftest.aspectj;
import a.b.c.tftest.model.Account;
public aspect AccountAspect {
pointcut callPay(int amount, Account account):
call(boolean a.b.c.tftest.model.Account.pay(int)) && args(amount) && target(account);
before(int amount, Account account): callPay(amount, account) {
System.out.println("[AccountAspect]付款前总金额: " + account.balance);
System.out.println("[AccountAspect]需要付款: " + amount);
}
boolean around(int amount, Account account): callPay(amount, account) {
if (account.balance < amount) {
System.out.println("[AccountAspect]拒绝付款!");
return false;
}
return proceed(amount, account);
}
after(int amount, Account balance): callPay(amount, balance) {
System.out.println("[AccountAspect]付款后,剩余:" + balance.balance);
}
}
B、ProfilingAspect:用 Java 来写,用于记录方法的执行时间
package a.b.c.tftest.aspectj;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class ProfilingAspect {
@Pointcut("execution(* a.b.c.tftest.model.*.*(..))")
public void modelLayer() {
}
@Around("modelLayer()")
public Object logProfile(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
System.out.println("[ProfilingAspect]方法: 【" + joinPoint.getSignature() + "】结束,用时: " + (System.currentTimeMillis() - start));
return result;
}
}
4)定一个主类:
package a.b.c.tftest;
import a.b.c.tftest.model.Account;
import cn.edu.nuc.test.User;
public class App {
public static void main(String[] args) {
testCompileTime();
}
public static void testCompileTime() {
Account account = new Account();
System.out.println("==================");
account.pay(10);
account.pay(50);
System.out.println("==================");
}
}
可以看到:AspectJ的语法比较难理解,使用AspectJ提供的注解来写还是比较方便的(Spring AOP也是使用了AspectJ提供的注解)。代码结构如下:
接下来,我们讨论怎么样将定义好的两个 Aspects 织入到我们的 Account 的付款方法 pay(amount) 中,也就是三种织入时机分别是怎么实现的。
2.2【Complie-Time Weaving】示例
这是最简单的使用方式,在编译期的时候进行织入,这样编译出来的 .class 文件已经织入了我们的代码,在 JVM 运行的时候其实就是加载了一个普通的被织入了代码的类。
1)aspject-maven插件:
采用 maven 进行管理,可以在
tftest org.codehaus.mojo aspectj-maven-plugin1.11 1.8 1.8 1.8 true true ignore UTF-8 false compile org.apache.maven.plugins maven-assembly-plugin2.4 a.b.c.tftest.Test assembly/assembly.xml jar-with-dependencies make-assembly package single org.eclipse.m2e lifecycle-mapping1.0.0 org.codehaus.mojo aspectj-maven-plugin[1.0,) test-compile compile
说明:如果是eclipse,使用这个插件可能会报如下错误:Plugin execution not covered by lifecycle configuration: org.codehaus.mojo:aspectj-maven-plugin:1.11:compile (execution: default, phase: compile) ,解决方法就是按照上面方式添加一个
2)然后通过mvn package打包:
编译后,可以看到AccountAspect.aj已经被编译成了class,如下:
此外,App代码也被修改了:
Account类也被修改了:
执行App后,输出:
2.3 【Post-Compile Weaving】示例:
Post-Compile Weaving 和 Compile-Time Weaving 非常类似,我们也是直接用场景来说。
假设上面的 Account 类在test.jar 包中,我们的工程 tftest.jar依赖了这个 jar 包。由于 Account 这个类已经被编译出来了,我们要对它的方法进行织入,就需要用到编译后织入。
为了方便测试,新建一个test工程,里面就一个类 User,代码和 Account 一样,mvn package打成test.jar包,里面就这一个User类。同时在tftest工程里引入test.jar,然后也复制 AccountAspect 一份出来,命名为 UserAspect,稍微修改修改就可以用来处理 User 类了。
1)test工程代码:
mvn信息如下:
4.0.0 cn.edu.nuc test0.0.1-SNAPSHOT test UTF-8 1.8 1.8 maven-clean-plugin 3.1.0 maven-compiler-plugin 3.8.0 maven-jar-plugin 3.0.2 maven-install-plugin 2.5.2 maven-deploy-plugin 2.8.2
代码如下:
package cn.edu.nuc.test;
public class User {
public int balance = 20;
public boolean pay(int amount) {
if (balance < amount) {
return false;
}
balance -= amount;
return true;
}
}
然后使用mvn install 将test.jar安装到本地仓库。
2)tftest工程:
首先引入test依赖
cn.edu.nuc test0.0.1-SNAPSHOT
然后,添加UserAspect类:
package a.b.c.tftest.aspectj;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class UserAspect {
@Pointcut("execution(* cn.edu.nuc.test.*.*(..))")
public void modelLayer() {
}
@Around("modelLayer()")
public Object logProfile(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
System.out.println("[userAspect]方法: 【" + joinPoint.getSignature() + "】结束,用时: " + (System.currentTimeMillis() - start));
return result;
}
}
最后修改App主类:
public class App {
public static void main(String[] args) {
testCompileTime();
}
public static void testCompileTime() {
User user = new User();
user.pay(10);
}
}
3)修改插件:
tftest org.codehaus.mojo aspectj-maven-plugin1.11 1.8 cn.edu.nuc testcompile org.apache.maven.plugins maven-assembly-plugin2.4 a.b.c.tftest.Test assembly/assembly.xml jar-with-dependencies make-assembly package single org.eclipse.m2e lifecycle-mapping1.0.0 org.codehaus.mojo aspectj-maven-plugin[1.0,) test-compile compile
然后执行,mvn clean package 最后,运行App主类,输出:
[userAspect]方法: 【boolean cn.edu.nuc.test.User.pay(int)】结束,用时: 1
从输出上看,UserAspect 对 User 进行了织入。而且是在User已经编译后进行的织入。
2.4 【Load-Time Weaving】示例:
最后,我们要介绍的是 LTW 织入,正如 Load-Time 的名字所示,它是在 JVM 加载类的时候做的织入。AspectJ 允许我们在启动的时候指定 agent 来实现这个功能。
这里还是用到了最初的tftest工程,首先,要注释掉之前在 pom.xml 中用于编译期和编译后织入使用的插件,免得影响我们的测试。(一旦我们去掉了 aspectj 的编译插件,那么 .aj 的文件是不会被编译的)
1)main/resource/meta-INF下建立aop.xml文件
定义了切面类和要织入的目标类范围。
2)切面类:
package a.b.c.tftest.aspectj;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
@Aspect
public class ProfilingAspect {
@Pointcut("execution(* a.b.c.tftest.model.*.*(..))")
public void modelLayer() {
}
@Around("modelLayer()")
public Object logProfile(ProceedingJoinPoint joinPoint) throws Throwable {
long start = System.currentTimeMillis();
Object result = joinPoint.proceed();
System.out.println("[ProfilingAspect]方法: 【" + joinPoint.getSignature() + "】结束,用时: " + (System.currentTimeMillis() - start));
return result;
}
}
目标类:Account.java 同上
3)App主类:
package a.b.c.tftest;
import a.b.c.tftest.model.Account;
public class App {
public static void main(String[] args) {
testCompileTime();
}
public static void testCompileTime() {
Account account = new Account();
System.out.println("==================");
account.pay(10);
account.pay(50);
System.out.println("==================");
}
}
4)pom.xml文件:
4.0.0 a.b.c tftest0.0.1-SNAPSHOT tftest UTF-8 1.8 1.8 org.aspectj aspectjrt1.8.13 org.aspectj aspectjweaver1.8.13 tftest org.apache.maven.plugins maven-assembly-plugin2.4 a.b.c.tftest.App assembly/assembly.xml jar-with-dependencies make-assembly package single
5) 编译/打包:
代码结构如下,
使用mvn package打包后,反编译结构如下:
可以看到,编译后没有进行织入。
然后执行:
java -jar ./target/tftest-jar-with-dependencies.jar
输出:
==================
==================
然后执行:
java -javaagent:/Users/knowliu/documents/mvn_repo/org/aspectj/aspectjweaver/1.8.10/aspectjweaver-1.8.10.jar -jar ./target/tftest-jar-with-dependencies.jar [AppClassLoader@18b4aac2] info AspectJ Weaver Version 1.8.13 built on Wednesday Nov 15, 2017 at 19:26:44 GMT [AppClassLoader@18b4aac2] info register classloader sun.misc.Launcher$AppClassLoader@18b4aac2 [AppClassLoader@18b4aac2] info using configuration file:/Users/knowliu/documents/workspace1/tftest/target/tftest-jar-with-dependencies.jar!/meta-INF/aop.xml [AppClassLoader@18b4aac2] info register aspect a.b.c.tftest.aspectj.ProfilingAspect [AppClassLoader@18b4aac2] weaveinfo Join point 'method-execution(boolean a.b.c.tftest.model.Account.pay(int))' in Type 'a.b.c.tftest.model.Account' (Account.java:11) advised by around advice from 'a.b.c.tftest.aspectj.ProfilingAspect' (ProfilingAspect.java)
输出:
==================
[ProfilingAspect]方法: 【boolean a.b.c.tftest.model.Account.pay(int)】结束,用时: 1
[ProfilingAspect]方法: 【boolean a.b.c.tftest.model.Account.pay(int)】结束,用时: 0
==================
参考:https://javadoop.com/post/aspectj



