AspectJ 簡介


引言

首先,明確以下幾個概念:

  • 切面(Aspect):跨越多個對象的連接點的模塊化(簡單理解為監視切點的類)。
  • 連接點(Joint Point):程序執行過程中的一個點,例如方法的的執行或者屬性的訪問
  • 通知(Advice):在切面中特定的連接點采取的行為
  • 切點(Pointcut):通過相關表達式匹配的連接點

​ 一般來講,實現 AOP 主要有以下兩種手段:靜態代理、動態代理。

​ 靜態代理將要執行的織入點的操作和原有類封裝成一個代理對象,在執行到相應的切點時執行代理的操作,這種方式較為笨拙,一般也不會采用這種方式來實現 AOP

​ 動態代理是一種比較好的解決方案,通過在程序運行時動態生成代理對象來完成相關的 Advice 操作。Spring 便是通過動態代理的方式來實現 AOP 的。使用動態代理的方式也有一定的局限性,操作更加復雜,同時相關的類之間也會變得耦合起來。

​ 相比較與使用代理的方式來實現 AOPAspectJ是目前作為 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 方法所在的類,類似下圖所示:

2021-11-12 23-05-51 的屏幕截圖.png

與編譯時織入和編譯后織入不同,加載時織入不會修改原有的 .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

具體的輸出如下所示:

2021-11-12 23-10-08 的屏幕截圖.png

具體的項目地址:https://github.com/LiuXianghai-coder/Spring-Study

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM