AspectJ使用
一、AOP介紹
AOP:Aspect-Oriented Programming,面向切面編程,是一種新的方法論(編程范式),是對傳統 OOP(Object-Oriented Programming,面向對象編程)的補充。旨在通過允許橫切關注點的分離,提高模塊化。如在方法執行前、或執行后、或是在執行中出現異常后這些地方進行攔截處理或叫做增強處理。主要應用于:日志收集、事務管理、安全檢查、緩存、對象池管理等。
AOP實現的關鍵就在于AOP框架自動創建的AOP代理,AOP代理則可分為靜態代理(例如:原生AspectJ)和動態代理(例如:spring aop)兩大類,其中靜態代理是指使用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屬于靜態織入,通過修改代碼來實現,有如下幾個織入的時機:
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提供的注解)。代碼結構如下:
接下來,我們討論怎么樣將定義好的兩個 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,如下:
?此外,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信息如下:
<?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) 編譯/打包:
代碼結構如下,
?使用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
==================
到這里,就要結束這一小節了,這里順便再介紹下如果用 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?
總結
- 上一篇: 吊打面试官系列之:掌握兼容性测试21个知
- 下一篇: Mybatis-plus的查,增,删