關於AspectJ你可能不知道的那些事

轉載:https://www.colabug.com/2102191.html 

請移步原鏈接查看原文

前兩天看了一些關於spring aop以及AspectJ的文章,但是總是感覺非常的亂,有的說spring aop跟aspectj相互獨立,有的說spring aop依賴於aspectj,有的甚至直接把兩者混爲一談。很多專門講Aspectj的文章也只是搬運了AspectJ的語法,就那麼一兩點東西,講來講去也沒有什麼新意。甚至很多甚至都是 面向IDE編程
(教你怎麼安裝插件,點擊菜單),對AspectJ的使用方式和工作原理都不去分析,離開了IDE的支持甚至連編譯都不會了。我認爲咱們這些碼農平時習慣用IDE並沒有問題,但是不僅要做到 會用IDE
,而且要做到 超越IDE
,這樣才能站到更高一點的視角看出工具的本來面目而不是受工具的侷限。

當然,我吐槽了這麼多其實並不是想標新立異,只是想找一個寫文章的理由。雖然從某種方面講,可能也算是” 茴香豆的X種寫法
“,但是既然我自己樂在其中,那麼開心就好嘍。

爲什麼用AspectJ

爲什麼用AspectJ,我的理解是兩個字” 方便
“。我們知道面向切面編程( Aspect Oriented Programming
)有諸多好處,但是在使用AspectJ之前我們一般是怎麼編寫切面的呢?我想一般來說應該是三種吧: 靜態代理
, jdk動態代理
, cglib動態代理
。但是我們知道,靜態代理的重用性太差,一個代理不能同事代理多種類;動態代理可以做到代理的重用,但是即使這樣,他們調用起來還是比較麻煩,除了寫切面代碼以外,我們還需要將代理類耦合進被代理類的調用階段,在創建被代理類的時候都要先創建代理類,再用代理類去創建被代理類,這就稍微有點麻煩了。比如我們想在現有的某個項目裏統一新加入一些切面,這時候就需要創建切面並且侵入原有代碼,在創建對象的時候添加代理,還是挺麻煩的。

說到底,這種麻煩出現的本質原因是,代理模式並沒有做到切面與業務代碼的解耦。雖然將切面的邏輯獨立進了代理類,但是 決定是否使用切面的權利
仍然在業務代碼中。這才導致了上面這種麻煩。

(當然,話不能說的這麼絕對,如果有那種類似Spring的IoC容器,將類的創建都統一託管起來,我們只需要將切面用配置文件進行註冊,容器會根據註冊信息在創建bean的時候自動加上代理,這也是比較方便的。不過並不是所有框架都提供IoC機制的吧。。。)

既然代理模式這麼麻煩,那麼AspectJ又是通過什麼方式來避免這個麻煩的呢?

我總結AspectJ提供了 兩套
強大的機制:

第一套是 切面語法
。就是網上到處都是的那種所謂”AspectJ使用方法”,這套東西做到了將 決定是否使用切面的權利
還給了切面。在寫切面的時候就可以決定哪些類的哪些方法會被代理,從而 從邏輯上
不需要侵入業務代碼。由於這套語法實在是太有名,導致很多人都誤以爲AspectJ等於切面語法,其實不然。

第二套是 織入工具
。剛纔講到切面語法能夠讓切面 從邏輯上
與業務代碼解耦,但是 從操作上
來講,當JVM運行業務代碼的時候,他甚至無從得知旁邊還有個類想橫插一刀。。。這個問題大概有兩種解決思路,一種就是提供註冊機制,通過額外的配置文件指明哪些類受到切面的影響,不過這還是需要干涉對象創建的過程;另外一種解決思路就是在編譯期(或者類加載期)我們優先考慮一下切面代碼,並將切面代碼通過某種形式插入到業務代碼中,這樣業務代碼不就知道自己被“切”了麼?這種思路的一個實現就是 aspectjweaver
,就是這裏的 織入工具

AspectJ究竟怎麼用

一提起AspectJ,其實我感覺絕大多數人都會聯想到Spring。畢竟,大多數人都是通過spring才接觸到了AspectJ。可事實上Spring只是用到了AspectJ的冰山一角,侷限於Spring恐怕是不能很好的理解AspectJ的,因此這一節我講不涉及任何spring的東西,單看下AspectJ。

事實上AspectJ提供了兩套對切面的描述方法,一種就是我們常見的 基於java註解
切面描述的方法,這種方法兼容java語法,寫起來十分方便,不需要IDE的額外語法檢測支持;另外一種是 基於aspect文件
的切面描述方法,這種語法本身並不是java語法,因此寫的時候需要IDE的插件支持才能進行語法檢查。

AspectJ相關jar包

AspectJ其實是eclipse基金會的一個項目, 官網
就在eclipse官網裏。官網裏提供了一個aspectJ.jar的下載鏈接,但其實這個鏈接只是一個安裝包,把安裝包裏的東西解壓後就是一個文檔+腳本+jar包的程序包,其中比較重要的是如下部分:

myths@pc:~/aspectj1.8$ tree bin/ lib/
bin/
├── aj
├── aj5
├── ajbrowser
├── ajc
└── ajdoc
lib/
├── aspectjrt.jar
├── aspectjtools.jar
├── aspectjweaver.jar
└── org.aspectj.matcher.jar

當然,這些jar包並不總是需要從官網下載,很多情況下在maven等中心庫中直接找會更方便。

這當中重點的文件是四個jar包中的前三個,bin文件夾中的腳本其實都是調用這些jar包的命令。

  • aspectjrt.jar包主要是提供 運行時
    的一些註解,靜態方法等等東西,通常我們要使用aspectJ的時候都要使用這個包。
  • aspectjtools.jar包主要是提供赫赫有名的 ajc編譯器
    ,可以在編譯期將將java文件或者class文件或者aspect文件定義的切面織入到業務代碼中。通常這個東西會被封裝進各種IDE插件或者自動化插件中。
  • aspectjweaverjar包主要是提供了一個java agent用於在 類加載期
    間織入切面(Load time weaving)。並且提供了對切面語法的相關處理等基礎方法,供ajc使用或者供第三方開發使用。這個包一般我們不需要顯式引用,除非需要使用LTW。

上面的說明其實也就指出了aspectJ的幾種標準的使用方法(參考 文檔
):

  1. 編譯時織入
    ,利用ajc編譯器替代javac編譯器,直接將源文件(java或者aspect文件)編譯成class文件並將切面織入進代碼。
  2. 編譯後織入
    ,利用ajc編譯器向javac編譯期編譯後的class文件或jar文件織入切面代碼。
  3. 加載時織入
    ,不使用ajc編譯器,利用aspectjweaver.jar工具,使用java agent代理在類加載期將切面織入進代碼。

基於aspectj文件的AspectJ

這種說法比較蛋疼,其實我想說明的是這種不兼容javac的一種切面表示形式。比如當前我們有一個業務類App.java:

public class App{

    public void say(){
        System.out.println("App say");
    }

    public static void main(String[] args){
        App app = new App();
        app.say();
    }
}

我們希望對在say函數里加一個切面,那就創建一個AjAspectj.aj的文件:

public aspect AjAspect {

    pointcut say():
            execution(* App.say(..));
    before(): say() {
        System.out.println("AjAspect before say");
    }
    after(): say() {
        System.out.println("AjAspect after say");
    }
}

這樣我們就能實現切面的功能。可這個aj文件的語法雖然跟java很類似,但是畢竟還是不能用javac來編譯,如果我們要用這個的話就必須使用ajc編譯器。使用的方法大概有這幾種:

  1. 調用命令
    直接編譯(直接使用ajc命令或者調用java -jar aspectjtools.jar)
  2. 使用 IDE集成
    的ajc編譯器編譯
  3. 使用 自動化構建工具
    的插件編譯 
    其實2,3兩點的本質都是使用aspectjtools.jar,最簡單的調用方法如下:

     

    #!/usr/bin/env bash
    
    ASPECTJ_TOOLS=/home/myths/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar
    ASPECTJ_RT=/home/myths/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
    
    java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -sourceroots .
    

調用aspectjtools.jar包,指定aspectjrt的classpath,以及需要編譯的路徑,這樣就會生成AjAspectj.aj以及App.java對應的class文件。我們反編譯一下看看:

AjAspectj.class:

import java.io.PrintStream;
import org.aspectj.lang.NoAspectBoundException;

public class AjAspect
{
  private static Throwable ajc$initFailureCause;
  public static final AjAspect ajc$perSingletonInstance;
  
  public static AjAspect aspectOf()
  {
    if (ajc$perSingletonInstance == null) {
      throw new NoAspectBoundException("AjAspect", ajc$initFailureCause);
    }
    return ajc$perSingletonInstance;
  }
  
  public static boolean hasAspect()
  {
    return ajc$perSingletonInstance != null;
  }
  
  private static void ajc$postClinit()
  {
    ajc$perSingletonInstance = new AjAspect();
  }
  
  static
  {
    try
    {
      
    }
    catch (Throwable localThrowable)
    {
      ajc$initFailureCause = localThrowable;
    }
  }
  
  public void ajc$before$AjAspect$1$682722c()
  {
    System.out.println("AjAspect before say");
  }
  
  public void ajc$after$AjAspect$2$682722c()
  {
    System.out.println("AjAspect after say");
  }
}

App.class:

import java.io.PrintStream;

public class App
{
  public void say()
  {
    try
    {
      AjAspect.aspectOf().ajc$before$AjAspect$1$682722c();System.out.println("App say");
    }
    catch (Throwable localThrowable)
    {
      AjAspect.aspectOf().ajc$after$AjAspect$2$682722c();throw localThrowable;
    }
    AjAspect.aspectOf().ajc$after$AjAspect$2$682722c();
  }
  
  public static void main(String[] args)
  {
    App app = new App();
    app.say();
  }
}

調用App.class,發現切面成功生效:

$ java -cp ~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar:.  App
AjAspect before say
App say
AjAspect after say

我們發現aj文件的確被編譯成了一個單例類,並且生成了一些切面方法,這些方法被織入進了App類中的say方法體中,可以說是非常的暴力了。(這裏順便吐槽一波IntelliJ自帶的反編譯器真的很 
,還是 jd-gui
好用)。

不過,雖然事實上這種基於aj文件的切面描述方法比基於java註解的切面描述方法用起來要 靈活的多
,但是由於他無法擺脫ajc的支持,而且本身不兼容java語法導致難以統一編碼規範,加上需要較多額外的學習成本,因此事實上很多項目還是不怎麼用這種方式,更多的還是採用了兼容java語法的用註解定義切面的方式。

基於java註解的AspectJ

下面我們主要還是着力考慮下基於java註解的切面使用方法。

準備

先建一個普通的項目看看,老樣子,從maven的maven-archetype-quickstart開始,pom.xml,pom文件裏我們一般只需要加上aspetjrt的依賴即可。:

    4.0.0

    com.mythsman.test
    aspect-test
    1.0-SNAPSHOT
    jar

    work
    http://maven.apache.org

    
        UTF-8
    

    
        
            org.aspectj
            aspectjrt
            1.8.9
        
    
    
    
        
            
                org.apache.maven.plugins
                maven-compiler-plugin
                3.7.0
                
                    1.8
                    1.8
                
        
    

創建App.java文件:

package com.mythsman.test;

public class App{

    public void say(){
        System.out.println("App say");
    }

    public static void main(String[] args){
        App app = new App();
        app.say();
    }
}

創建切面類AnnoAspect.java:

package com.mythsman.test;

import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

@Aspect
public class AnnoAspect{

    @Pointcut("execution(* com.mythsman.test.App.say(..))")
    public void jointPoint(){
    }

    @Before("jointPoint()")
    public void before(){
        System.out.println("AnnoAspect before say");
    }


    @After("jointPoint()")
    public void after(){
        System.out.println("AnnoAspect after say");
    }

}

當前項目結構應該是這樣的:

.
├── pom.xml
├── src
│   └── main
│       ├── java
│       │   └── com
│       │       └── mythsman
│       │           └── test
│       │               └── App.java
│       │               ├── AnnoAspect.java

其實就是創建了一個對App類進行切面的AnnoAspect類,這個類需要加上@Aspect註解用以聲明這是一個切面,以及其他相關切面語法。接下來我們就來嘗試下三種不同的編譯方式。

編譯時織入

編譯時織入其實就是使用ajc來進行編譯,暫時不使用自動化構建工具,我們先在項目根目錄下手動寫一個編譯腳本compile.sh:

#!/usr/bin/env bash
ASPECTJ_WEAVER=/home/myths/.m2/repository/org/aspectj/aspectjweaver/1.8.13/aspectjweaver-1.8.13.jar
ASPECTJ_RT=/home/myths/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
ASPECTJ_TOOLS=/home/myths/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar

java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -source 1.5 -sourceroots src/main/java/ -d target/classes

調用aspectjtools.jar,在-cp裏指明aspectjrt.jar的路徑,-source 1.5指明支持java1.5以後的註解,-sourceroots指明編譯的文件夾,-d指明輸出路徑。

這樣就會生成AnnoAspect.class和App.class兩個文件。

AnnoAspect.class:

package com.mythsman.test;

import java.io.PrintStream;
import org.aspectj.lang.NoAspectBoundException;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

@Aspect
public class AnnoAspect
{
  public static AnnoAspect aspectOf()
  {
    if (ajc$perSingletonInstance == null) {
      throw new NoAspectBoundException("com.mythsman.test.AnnoAspect", ajc$initFailureCause);
    }
    return ajc$perSingletonInstance;
  }
  
  public static boolean hasAspect()
  {
    return ajc$perSingletonInstance != null;
  }
  
  static
  {
    try
    {
      ajc$postClinit();
    }
    catch (Throwable localThrowable)
    {
      ajc$initFailureCause = localThrowable;
    }
  }
  
  @Before("jointPoint()")
  public void before()
  {
    System.out.println("AnnoAspect before say");
  }
  
  @After("jointPoint()")
  public void after()
  {
    System.out.println("AnnoAspect after say");
  }
}

App.class

package com.mythsman.test;

import java.io.PrintStream;

public class App
{
  public void say()
  {
    try
    {
      AnnoAspect.aspectOf().before();System.out.println("App say");
    }
    catch (Throwable localThrowable)
    {
      AnnoAspect.aspectOf().after();throw localThrowable;
    }
    AnnoAspect.aspectOf().after();
  }
  
  public static void main(String[] args)
  {
    App app = new App();
    app.say();
  }
}

我們發現ajc對AnnoAspect的處理方法與跟AjAspect的處理方法類似,都是將類聲明成單例,並且識別AspectJ語法,將相關函數織入到App中。

運行(在項目根目錄執行):

$ java -cp ~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar:src/main/java/ com.mythsman.test.App 
AnnoAspect before say
App say
AnnoAspect after say

編譯後織入

編譯後織入其實就是在javac編譯完成後,用ajc再去處理class文件得到新的、織入過切面的class文件。

仍然是上面的項目,我們先用javac編譯一下:

$ javac -cp ~/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar  -d target/classes src/main/java/com/mythsman/test/*.java

編譯成功後生成了AnnoAspect.class以及App.class。顯然,這兩個class文件反編譯後還是源文件的樣子,並沒有什麼用,因此這時候執行App的main函數發現切面並沒有生效。因此我們仍然需要用ajc來處理:

!/usr/bin/env bash
ASPECTJ_WEAVER=/home/myths/.m2/repository/org/aspectj/aspectjweaver/1.8.13/aspectjweaver-1.8.13.jar
ASPECTJ_RT=/home/myths/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
ASPECTJ_TOOLS=/home/myths/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar

java -jar $ASPECTJ_TOOLS -cp $ASPECTJ_RT -source 1.5 -inpath target/classes -d target/classes

這樣就把target/classes中原來的class文件替換成了織入後的class文件。反編譯之後發現與採用 編譯期織入
方法的結果基本相同。

加載時織入(LTW)

前兩種織入方法都依賴於ajc的編譯工具,LTW卻通過java agent機制在內存中操作類文件,可以不需要ajc的支持做到 動態織入

不過,這裏有一個挺有意思的問題,我們知道編譯期一定會編譯AnnoAspect類,那麼這時候通過切面語法我們就可以找到他要處理的App類,這大概就是編譯階段織入的大概流程。但是如果在類加載期處理的話,當類加載到App類的時候,我們並不知道這個類需要被AnnoAspect處理。。。因此爲了實現LTW,我們肯定要有個配置文件,來告訴類加載器,某某某切面需要優先考慮,他們很可能會影響其他的類。

爲了實現LTW,我們需要在資源目錄下配置META-INF/aop.xml文件,來告知類加載器我們當前註冊的切面。

在上面的項目中,我們其實只需要創建src/main/resources/META-INF/aop.xml:

 

這樣,我們就可以先使用javac編譯源文件,再使用java agent在運行時織入:

#!/usr/bin/env bash
ASPECTJ_WEAVER=/home/myths/.m2/repository/org/aspectj/aspectjweaver/1.8.13/aspectjweaver-1.8.13.jar
ASPECTJ_RT=/home/myths/.m2/repository/org/aspectj/aspectjrt/1.8.9/aspectjrt-1.8.9.jar
ASPECTJ_TOOLS=/home/myths/.m2/repository/org/aspectj/aspectjtools/1.8.9/aspectjtools-1.8.9.jar

java -javaagent:$ASPECTJ_WEAVER -cp $ASPECTJ_RT:target/classes/ com.mythsman.test.App

運行結果:

AnnoAspect before say
App say
AnnoAspect after say

當然,如果可以使用ajc的話,我們也可以通過-outxml參數來自動生成xml文件。

maven自動化構建

顯然,自己寫腳本還是比較麻煩的,如果用如maven這樣的自動化構建工具的話就會方便很多,codehaus提供了一個ajc的編譯插件aspectj-maven-plugin,我們只需要在build/plugins標籤下加上這個插件的配置即可:

    org.codehaus.mojo
    aspectj-maven-plugin
    1.10
    
        1.8
        1.8
        1.8
    
    
        
            
                compile
            
        
    

這個插件會綁定到編譯期,採用的應該是編譯後織入的方式,在maven-compiler-plugin處理完之後再工作的。

不要以爲這個插件多厲害,說白了他其實就是對aspectjtools.jar的一個mojo 封裝
而已,去看他的依賴樹就會很清楚。

如何判斷是織入還是代理

這個問題很有意思,也是非常容易被搞混的,尤其是在討論spring aop的時候。我們知道spring裏有很多基於動態代理的設計,而我們知道動態代理也可以被用作面向切面的編程,但是spring aop本身卻支持aspectj的切面語法,而且spring-aop這個包也引用了aspectj,我們知道aspectj是通過織入的方式來實現aop的。。。那麼 spring aop究竟是通過織入還是代理來實現aop的呢

沒錯就是動態代理

其實spring aop還是通過 動態代理
來實現aop的,即使不去看他的源碼,我們也可以通過簡單的實驗來得到這個結論。

根據aspectj的使用方式,我們知道,如果要向代碼中織入切面,那麼我們要麼採用ajc編譯,要麼使用aspectjweaver的agent代理。但是spring既沒有依賴任何aspectjtools的相關jar包,雖然依賴了aspectjweaver這個包,但是並沒有添加agent代理。當然,也存在一種可能就是spring利用aspectjweaver這個包自己實現了動態織入,但是從可複用的角度講,spring真的會自己重新造輪子?如果真的重新造了那爲啥不脫離aspectj徹底重新造,而是用一半造一半呢?

而且,我們知道用織入和用動態代理有一個很大的區別,如果使用織入的話,那麼調業務對象的getClass()方法獲得的類名就是這個類本身實現的類名;但是如果使用動態代理的話,調用getClass()方法獲得的類名就是動態代理類的類名了。做一個簡單的實驗我們就可以發現,如果我們使用spring aop來對某一個service進行切面處理,那麼調用getClass()方法獲得的結果就是:

com.mythsman.test.Myservice$$EnhancerBySpringCGLIB$$3afc9148

顯然,雖然spring aop採用了aspectj語法來定義切面,但是在實現切面邏輯的時候還是採用CGLIB來進行動態代理的方法。

強行織入?

當然,如果我們想,我們也可以強行採用織入的方式,不過我們就不能將切面類註冊爲spring的bean,並且採用ajc插件編譯或者java agent在類加載時織入。

發佈了2 篇原創文章 · 獲贊 0 · 訪問量 1836
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章