一、AOP介绍

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属于静态织入,通过修改代码来实现,有如下几个织入的时机:

  1. ​编译期织入(Compile-time weaving): 如类 A 使用 AspectJ 添加了一个属性,类 B 引用了它,这个场景就需要编译期的时候就进行织入,否则没法编译类 B。
  2. 编译后织入(Post-compile weaving): 也就是已经生成了 .class 文件,或已经打成 jar 包了,这种情况我们需要增强处理的话,就要用到编译后织入。
  3. 类加载后织入(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依赖:

<!-- aspectj -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>

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提供的注解)。代码结构如下:

AspectJ使用_maven

接下来,我们讨论怎么样将定义好的两个 Aspects 织入到我们的 Account 的付款方法 pay(amount) 中,也就是三种织入时机分别是怎么实现的。

2.2【Complie-Time Weaving】示例

这是最简单的使用方式,在编译期的时候进行织入,这样编译出来的 .class 文件已经织入了我们的代码,在 JVM 运行的时候其实就是加载了一个普通的被织入了代码的类。

1)aspject-maven插件:

采用 maven 进行管理,可以在 <build> 中加入以下的插件:

<build>
<finalName>tftest</finalName>
<plugins>
<!-- 编译期织入 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.11</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<source>1.8</source>
<target>1.8</target>
<showWeaveInfo>true</showWeaveInfo>
<verbose>true</verbose>
<Xlint>ignore</Xlint>
<encoding>UTF-8</encoding>
</configuration>
<executions>
<execution>
<configuration>
<skip>false</skip>
</configuration>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>
<!--打包插件-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<mainClass>a.b.c.tftest.Test</mainClass>
</manifest>
</archive>
<descriptors>
<descriptor>assembly/assembly.xml</descriptor>
</descriptors>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>

<pluginManagement>
<plugins>
<!--This plugin's configuration is used to store Eclipse
m2e settings only. It has no influence on the Maven build itself. -->
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<versionRange>[1.0,)</versionRange>
<goals>
<goal>test-compile</goal>
<goal>compile</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute />
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>

说明:如果是eclipse,使用这个插件可能会报如下错误:Plugin execution not covered by lifecycle configuration: org.codehaus.mojo:aspectj-maven-plugin:1.11:compile (execution: default, phase: compile) ,解决方法就是按照上面方式添加一个<pluginManagement>

2)然后通过mvn package打包:

编译后,可以看到AccountAspect.aj已经被编译成了class,如下:

AspectJ使用_maven_02

 此外,App代码也被修改了:

AspectJ使用_jar_03

Account类也被修改了:

AspectJ使用_maven_04

 执行App后,输出:

AspectJ使用_jar_05 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信息如下:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>cn.edu.nuc</groupId>
<artifactId>test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>test</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<artifactId>maven-clean-plugin</artifactId>
<version>3.1.0</version>
</plugin>
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.0</version>
</plugin>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<version>3.0.2</version>
</plugin>
<plugin>
<artifactId>maven-install-plugin</artifactId>
<version>2.5.2</version>
</plugin>
<plugin>
<artifactId>maven-deploy-plugin</artifactId>
<version>2.8.2</version>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

代码如下: 

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依赖

<dependency>
<groupId>cn.edu.nuc</groupId>
<artifactId>test</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

然后,添加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)修改插件:

<build>
<finalName>tftest</finalName>
<plugins>
<!--编译后织入-->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<version>1.11</version>
<configuration>
<complianceLevel>1.8</complianceLevel>
<weaveDependencies>
<weaveDependency>
<groupId>cn.edu.nuc</groupId>
<artifactId>test</artifactId>
</weaveDependency>
</weaveDependencies>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
</goals>
</execution>
</executions>
</plugin>

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<mainClass>a.b.c.tftest.Test</mainClass>
</manifest>
</archive>
<descriptors>
<descriptor>assembly/assembly.xml</descriptor>
</descriptors>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>

<pluginManagement>
<plugins>
<!--This plugin's configuration is used to store Eclipse
m2e settings only. It has no influence on the Maven build itself. -->
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.codehaus.mojo</groupId>
<artifactId>aspectj-maven-plugin</artifactId>
<versionRange>[1.0,)</versionRange>
<goals>
<goal>test-compile</goal>
<goal>compile</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute />
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>

然后执行,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文件

<?xml version="1.0" encoding="UTF-8"?>
<aspectj>
<aspects>
<aspect name="a.b.c.tftest.aspectj.ProfilingAspect"/>
<weaver options="-verbose -showWeaveInfo">
<include within="a.b.c.tftest..*"/>
</weaver>
</aspects>
</aspectj>

定义了切面类和要织入的目标类范围。

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文件:

<?xml version="1.0" encoding="UTF-8"?>

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>a.b.c</groupId>
<artifactId>tftest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>tftest</name>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<!-- aspectj -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.8.13</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.13</version>
</dependency>
</dependencies>

<build>
<finalName>tftest</finalName>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<configuration>
<archive>
<manifest>
<mainClass>a.b.c.tftest.App</mainClass>
</manifest>
</archive>
<descriptors>
<descriptor>assembly/assembly.xml</descriptor>
</descriptors>
<descriptorRefs>
<descriptorRef>jar-with-dependencies</descriptorRef>
</descriptorRefs>
</configuration>
<executions>
<execution>
<id>make-assembly</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

5) 编译/打包:

代码结构如下,

AspectJ使用_java_06

 使用mvn package打包后,反编译结构如下:

AspectJ使用_java_07

可以看到,编译后没有进行织入。

然后执行:

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

==================

到这里,就要结束这一小节了,这里顺便再介绍下如果用 maven 跑测试的话怎么搞。

首先,我们往 surefire 插件中加上 javaagent:

<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.10</version>
<configuration>
<argLine>
-javaagent:/xxx/aspectjweaver-1.8.13.jar
</argLine>
<useSystemClassLoader>true</useSystemClassLoader>
<forkMode>always</forkMode>
</configuration>
</plugin>

然后,我们就可以用 ​​mvn test​​ 看到织入效果了。还是那句话,不要用 IDE 进行测试,因为 IDE 太“智能”了。

参考:https://javadoop.com/post/aspectj