首先,明确以下几个概念:
一般来讲,实现 AOP
主要有以下两种手段:静态代理、动态代理。
静态代理将要执行的织入点的操作和原有类封装成一个代理对象,在执行到相应的切点时执行代理的操作,这种方式较为笨拙,一般也不会采用这种方式来实现 AOP
。
动态代理是一种比较好的解决方案,通过在程序运行时动态生成代理对象来完成相关的 Advice
操作。Spring 便是通过动态代理的方式来实现 AOP
的。使用动态代理的方式也有一定的局限性,操作更加复杂,同时相关的类之间也会变得耦合起来。
相比较与使用代理的方式来实现 AOP
,AspectJ
是目前作为 AOP
(Aspect-Oriented Programming 面向切面编程) 实现的一种最有效的解决方案。
AspectJ
提供了三种方式来实现 AOP
,通过在类加载的不同时间段来完成相关代码的织入以达到目的。
具体有以下三种方式:
.class
文件中.class
文件中JVM
加载 .class
文件的时候将代码织入使用 AspectJ
主要依赖于以下两个依赖:
<properties> <!-- AspectJ 依赖的版本 --> <aspectj.version>1.9.7</aspectj.version> </properties> <!-- 织入的依赖 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>${aspectj.version}</version> </dependency> <!-- 运行时需要的依赖 --> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjrt</artifactId> <version>${aspectj.version}</version> </dependency>
定义一个 Account
类,以及相关的一些行为方法
public class Account { int balance = 20; // 提款操作 public boolean withDraw(int amount) { if (balance < amount) return false; balance -= amount; return true; } }
Aspect
可以通过使用 AspectJ
的语法来定义切面,也可以通过 Java
来定义
使用 AsjpectJ
定义切面
public aspect AccountAspect { // Aspect,注意概念的对应关系 final int MIN_BALANCE = 10; /* 定义切点,对应 Pointcut 这里的切点定义在调用 Account 对象在调用 witdraw 方法 */ pointcut callWithDraw(int amount, Account acc): call(boolean Account.withdraw(int)) && args(amount) && target(acc); /* 在上文定义的切点执行之前采取的行为,这就被称之为 Advice */ before(int amount, Account acc): callWithDraw(amount, acc) { System.out.println("[AccountAspect] 付款前总额: " + acc.balance); System.out.println("[AccountAspect] 需要付款: " + amount); } /* 在对应的切点执行前后采取的行为 */ boolean around(int amount, Account acc): callWithDraw(amount, acc) { if (acc.balance < amount) { System.out.println("[AccountAspect] 拒绝付款!"); return false; } return proceed(amount, acc); } /* 对应的切点执行后的采取的行为 */ after(int amount, Account balance): callWithDraw(amount, balance) { System.out.println("[AccountAspect] 付款后剩余:" + balance.balance); } }
使用 Java
来定义切面
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @Aspect public class ProfilingAspect { private final static Logger log = LoggerFactory.getLogger(ProfilingAspect.class); // 定义切点 @Pointcut("execution(* org.xhliu.aop.entity.Account.*(..))") public void modelLayer() {} // 在切点执行前后采取的行为,这里是记录方法调用的时间 @Around("modelLayer()") public Object logProfile(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); Object result = joinPoint.proceed(); log.info("[ProfilingAspect] 方法:【" + joinPoint.getSignature() + "】结束,耗时:" + (System.currentTimeMillis() - startTime)); return result; } }
由于 javac
无法编译 AspectJ
,因此首先需要加入相关的 AspectJ
插件来完成编译时的织入:
<!-- 编译时织入的 maven 插件 --> <plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.7</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> <goals> <!-- use this goal to weave all your main classes --> <goal>compile</goal> <!-- use this goal to weave all your test classes --> <goal>test-compile</goal> </goals> </execution> </executions> </plugin>
现在,定义一个 Main
方法来执行 Account
的方法:
import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class AspectApplication { private final static Logger log = LoggerFactory.getLogger(AspectApplication.class); public static void main(String[] args) { Account account = new Account(); log.info("================ 分割线 =================="); account.withDraw(10); account.withDraw(100); log.info("================ 结束 =================="); } }
此时运行 Main
方法(可能会存在缓存,使用 mvn clean
来清理原来的生成文件),会看到类似于如下的输出:
对 Main
方法所在的类进行反编译,得到类似如下图所示的结果:
可以看到,在生成的 .class
文件中已经添加了 Aspect
的相关内容,因此在运行时会执行在 AccountAspect
中定义的内容。
再查看 Account
编译后的类:
可以看到,Account.class
也已经被织入了一些 Aspect
的内容
以上操作都是在 shell
中完成,因为有的 IDE
将使用自己的一些特有的处理方式而不是使用插件。
# 使用 mvn 来启动相关的主类 mvn exec:java -D"exec.mainClass"="org.xhliu.aop.entity.AspectApplication"
一般来讲,使用编译后的织入方式已经足够了,但是试想一下这样的场景:现在已经得到了一个 SDK
,需要在这些 SDK
的类上定义一些切点的行为,这个时候只能针对编译后的 .class
文件进行进一步的织入,或者在加载 .class
时再织入。
在另一个项目中定义一个 UserAccount
的主类
package com.example.aopshare.entity; public class UserAccount { private int balance = 20; public UserAccount() { } public int getBalance(){return this.balance;} public void setBalance(int balance){ this.balance = balance; } public boolean withDraw(int amount) { if (this.balance < amount) { return false; } else { this.balance -= amount; return true; } } }
将这个项目打包到本地的 maven
仓库
mvn clean package mvn install
现在就可以直接在当前的项目中引用这个 SDK
了,加入对应的 gav
即可:
<dependency> <groupId>com.example</groupId> <artifactId>aop-share</artifactId> <version>1.0</version> </dependency>
为这个第三方库的 UserAccount
创建一个 Aspect
:
import com.example.aopshare.entity.UserAccount; public aspect UserAccountAspect { pointcut callWithDraw(int amount, UserAccount acc): call(boolean UserAccount.withDraw(int)) && args(amount) && target(acc); before(int amount, UserAccount acc): callWithDraw(amount, acc) { System.out.println("[UserAccountAspect] 付款前总额: " + acc.getBalance()); System.out.println("[UserAccountAspect] 需要付款: " + amount); } boolean around(int amount, UserAccount acc): callWithDraw(amount, acc) { if (acc.getBalance() < amount) { System.out.println("[UserAccountAspect] 拒绝付款!"); return false; } return proceed(amount, acc); } after(int amount, UserAccount balance): callWithDraw(amount, balance) { System.out.println("[UserAccountAspect] 付款后剩余:" + balance.getBalance()); } }
分割线——————————————————————————————
准备工作已经完成,现在正式开始实现编译后的织入,只需添加对应的插件即可完成:
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>aspectj-maven-plugin</artifactId> <version>1.7</version> <configuration> <complianceLevel>1.8</complianceLevel> <!-- 要处理的第三方 SDK,只有在这里定义的 SDK 才会执行对应后织入 --> <weaveDependencies> <weaveDependency> <groupId>com.example</groupId> <artifactId>aop-share</artifactId> </weaveDependency> </weaveDependencies> </configuration> <executions> <execution> <goals> <goal>compile</goal> </goals> </execution> </executions> </plugin>
现在再运行 Main
类,得到与编译时织入类似的输出:
在 JVM
加载 Class 对象的时候完成织入。Aspect
通过在启动时指定 Agent
来实现这个功能
加载时织入与编译后织入的使用场景十分相似,因此依旧以上文的例子来展示加载时织入的使用
首先,注释掉使用到的 aspect
编译插件,这回影响到这部分的测试
在项目的 resources
目录下的 META-INF
目录(如果没有就创建一个)中,添加一个 aop.xml
文件,具体内容如下所示:
<!DOCTYPE aspectj PUBLIC "-//AspectJ//DTD//EN" "http://www.eclipse.org/aspectj/dtd/aspectj.dtd"> <aspectj> <aspects> <!-- 使用加载时织入的方式只能通过定义具体的 Aspect 类来实现,因为 AspectJ 无法被 javac 编译 --> <aspect name="org.xhliu.aop.entity.UserAspect"/> <!-- 要监听的切点所在的包位置,即要执行对应切面方法的位置 --> <include within="org.xhliu.aop.entity..*"/> </aspects> </aspectj>
查看监听的 main
方法所在的类,类似下图所示:
与编译时织入和编译后织入不同,加载时织入不会修改原有的 .class
文件
在运行时需要添加相关的代理参数类来实现加载时的织入,具体的参数如下所所示:
# 注意将 -javaagent 对应的代理类修改为本地 maven 仓库对应的 java -javaagent:/home/lxh/.m2/repository/org/aspectj/aspectjweaver/1.9.7/aspectjweaver-1.9.7.jar -jar target/spring-aop-1.0-SNAPSHOT-jar-with-dependencies.jar
具体的输出如下所示:
具体的项目地址:https://github.com/LiuXianghai-coder/Spring-Study
参考:https://javadoop.com/post/aspectj