引言
首先,明確以下幾個概念:
- 切面(Aspect):跨越多個對象的連接點的模塊化(簡單理解為監視切點的類)。
- 連接點(Joint Point):程序執行過程中的一個點,例如方法的的執行或者屬性的訪問
- 通知(Advice):在切面中特定的連接點采取的行為
- 切點(Pointcut):通過相關表達式匹配的連接點
一般來講,實現 AOP
主要有以下兩種手段:靜態代理、動態代理。
靜態代理將要執行的織入點的操作和原有類封裝成一個代理對象,在執行到相應的切點時執行代理的操作,這種方式較為笨拙,一般也不會采用這種方式來實現 AOP
。
動態代理是一種比較好的解決方案,通過在程序運行時動態生成代理對象來完成相關的 Advice
操作。Spring 便是通過動態代理的方式來實現 AOP
的。使用動態代理的方式也有一定的局限性,操作更加復雜,同時相關的類之間也會變得耦合起來。
相比較與使用代理的方式來實現 AOP
,AspectJ
是目前作為 AOP
(Aspect-Oriented Programming 面向切面編程) 實現的一種最有效的解決方案。
使用介紹
AspectJ
提供了三種方式來實現 AOP
,通過在類加載的不同時間段來完成相關代碼的織入以達到目的。
具體有以下三種方式:
- 編譯期(compiler-time)織入:在類進行編譯的時候就將相應的代碼織入到元類文件的
.class
文件中 - 編譯后(post-compiler)織入:在類編譯后,再將相關的代碼織入到
.class
文件中 - 加載時(load-time) 織入:在
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
具體的輸出如下所示: