idea下aspectj程序運行示例
有些同學可能想自己編寫aspect程序進行測試練習,博主在這簡單介紹運行環境的搭建,首先博主使用的idea的IDE,因此只對idea進行介紹。首先通過maven倉庫下載工具包aspectjtools-1.8.9.jar,該工具包包含ajc核心編譯器,然后打開idea檢查是否已安裝aspectJ的插件:
配置項目使用ajc編譯器(替換javac)如下圖:
如果使用maven開發(否則在libs目錄自行引入jar)則在pom文件中添加aspectJ的核心依賴包,包含了AspectJ運行時的核心庫文件:
<dependency>
<groupId>aspectj</groupId>
<artifactId>aspectjtools</artifactId>
<version>1.8.10</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>1.6.12</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.7.4</version>
</dependency>
新建文件處創建aspectJ文件,然后就可以像運行java文件一樣,操作aspect文件了。
這里先進行一個簡單案例的演示。編寫一個HelloWord的類,然后利用AspectJ技術切入該類的執行過程。
/** * Created by zejian on 2017/2/15. */ public class HelloWord { public void sayHello(){ System.out.println("hello world !"); } public static void main(String args[]){ HelloWord helloWord =new HelloWord(); helloWord.sayHello(); } }
編寫AspectJ類,注意關鍵字為aspect(MyAspectJDemo.aj,其中aj為AspectJ的后綴),含義與class相同,即定義一個AspectJ的類
/** * Created by zejian on 2017/2/15. * 切面類 */ public aspect MyAspectJDemo { /** * 定義切點,日志記錄切點 */ pointcut recordLog():call(* HelloWord.sayHello(..)); /** * 定義切點,權限驗證(實際開發中日志和權限一般會放在不同的切面中,這里僅為方便演示) */ pointcut authCheck():call(* HelloWord.sayHello(..)); /** * 定義前置通知! */ before():authCheck(){ System.out.println("sayHello方法執行前驗證權限"); } /** * 定義后置通知 */ after():recordLog(){ System.out.println("sayHello方法執行后記錄日志"); } }
ok~,運行helloworld的main函數:
對於結果不必太驚訝,完全是意料之中。我們發現,明明只運行了main函數,卻在sayHello函數運行前后分別進行了權限驗證和日志記錄,事實上這就是AspectJ的功勞了。
AOP中抽象概念(切入點(pointcut)、通知(advice)、切面(aspect)、織入(weaving))解釋
對aspectJ有了感性的認識后,再來聊聊aspectJ到底是什么?AspectJ是一個java實現的AOP框架,它能夠對java代碼進行AOP編譯(一般在編譯期進行),讓java代碼具有AspectJ的AOP功能(當然需要特殊的編譯器),可以這樣說AspectJ是目前實現AOP框架中最成熟,功能最豐富的語言,更幸運的是,AspectJ與java程序完全兼容,幾乎是無縫關聯,因此對於有java編程基礎的工程師,上手和使用都非常容易。
在案例中,我們使用aspect關鍵字定義了一個類,這個類就是一個切面,它可以是單獨的日志切面(功能),也可以是權限切面或者其他,在切面內部使用了pointcut定義了兩個切點,一個用於權限驗證,一個用於日志記錄,而所謂的切點就是那些需要應用切面的方法,如需要在sayHello方法執行前后進行權限驗證和日志記錄,那么就需要捕捉該方法,而pointcut就是定義這些需要捕捉的方法(常常是不止一個方法的),這些方法也稱為目標方法,最后還定義了兩個通知,通知就是那些需要在目標方法前后執行的函數,如before()即前置通知在目標方法之前執行,即在sayHello()方法執行前進行權限驗證,另一個是after()即后置通知,在sayHello()之后執行,如進行日志記錄。到這里也就可以確定,切面就是切點和通知的組合體,組成一個單獨的結構供后續使用,下圖協助理解。
這里簡單說明一下切點的定義語法:關鍵字為pointcut,定義切點,后面跟着函數名稱,最后編寫匹配表達式,此時函數一般使用call()或者execution()進行匹配,這里我們統一使用call()
pointcut 函數名 : 匹配表達式
案例:recordLog()是函數名稱,自定義的,* 表示任意返回值,接着就是需要攔截的目標函數,sayHello(..)的..,表示任意參數類型。這里理解即可,后面Spring AOP會有關於切點表達式的分析,整行代碼的意思是使用pointcut定義一個名為recordLog的切點函數,其需要攔截的(切入)的目標方法是HelloWord類下的sayHello方法,參數不限。
pointcut recordLog():call(* HelloWord.sayHello(..));
關於定義通知的語法:首先通知有5種類型分別如下:
- before 目標方法執行前執行,前置通知
- after 目標方法執行后執行,后置通知
- after returning 目標方法返回時執行 ,后置返回通知
- after throwing 目標方法拋出異常時執行 異常通知
- around 在目標函數執行中執行,可控制目標函數是否執行,環繞通知
語法:
[返回值類型] 通知函數名稱(參數) [returning/throwing 表達式]:連接點函數(切點函數){
函數體
}
案例如下,其中要注意around通知即環繞通知,可以通過proceed()方法控制目標函數是否執行。
/** * 定義前置通知 * * before(參數):連接點函數{ * 函數體 * } */ before():authCheck(){ System.out.println("sayHello方法執行前驗證權限"); } /** * 定義后置通知 * after(參數):連接點函數{ * 函數體 * } */ after():recordLog(){ System.out.println("sayHello方法執行后記錄日志"); } /** * 定義后置通知帶返回值 * after(參數)returning(返回值類型):連接點函數{ * 函數體 * } */ after()returning(int x): get(){ System.out.println("返回值為:"+x); } /** * 異常通知 * after(參數) throwing(返回值類型):連接點函數{ * 函數體 * } */ after() throwing(Exception e):sayHello2(){ System.out.println("拋出異常:"+e.toString()); } /** * 環繞通知 可通過proceed()控制目標函數是否執行 * Object around(參數):連接點函數{ * 函數體 * Object result=proceed();//執行目標函數 * return result; * } */ Object around():aroundAdvice(){ System.out.println("sayAround 執行前執行"); Object result=proceed();//執行目標函數 System.out.println("sayAround 執行后執行"); return result; }
切入點(pointcut)和通知(advice)的概念已比較清晰,而切面則是定義切入點和通知的組合如上述使用aspect關鍵字定義的MyAspectJDemo,把切面應用到目標函數的過程稱為織入(weaving)。在前面定義的HelloWord類中除了sayHello函數外,還有main函數,以后可能還會定義其他函數,而這些函數都可以稱為目標函數,也就是說這些函數執行前后也都可以切入通知的代碼,這些目標函數統稱為連接點,切入點(pointcut)的定義正是從這些連接點中過濾出來的,下圖協助理解。
AspectJ的織入方式及其原理概要
經過前面的簡單介紹,我們已初步掌握了AspectJ的一些語法和概念,但這樣仍然是不夠的,我們仍需要了解AspectJ應用到java代碼的過程(這個過程稱為織入),對於織入這個概念,可以簡單理解為aspect(切面)應用到目標函數(類)的過程。對於這個過程,一般分為動態織入和靜態織入,動態織入的方式是在運行時動態將要增強的代碼織入到目標類中,這樣往往是通過動態代理技術完成的,如Java JDK的動態代理(Proxy,底層通過反射實現)或者CGLIB的動態代理(底層通過繼承實現),Spring AOP采用的就是基於運行時增強的代理技術,這里主要重點分析一下靜態織入,ApectJ采用的就是靜態織入的方式。ApectJ主要采用的是編譯期織入,在這個期間使用AspectJ的acj編譯器(類似javac)把aspect類編譯成class字節碼后,在java目標類編譯時織入,即先編譯aspect類再編譯目標類。
關於ajc編譯器,是一種能夠識別aspect語法的編譯器,它是采用java語言編寫的,由於javac並不能識別aspect語法,便有了ajc編譯器,注意ajc編譯器也可編譯java文件。為了更直觀了解aspect的織入方式,我們打開前面案例中已編譯完成的HelloWord.class文件,反編譯后的java代碼如下:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package com.zejian.demo; import com.zejian.demo.MyAspectJDemo; //編譯后織入aspect類的HelloWord字節碼反編譯類 public class HelloWord { public HelloWord() { } public void sayHello() { System.out.println("hello world !"); } public static void main(String[] args) { HelloWord helloWord = new HelloWord(); HelloWord var10000 = helloWord; try { //MyAspectJDemo 切面類的前置通知織入 MyAspectJDemo.aspectOf().ajc$before$com_zejian_demo_MyAspectJDemo$1$22c5541(); //目標類函數的調用 var10000.sayHello(); } catch (Throwable var3) { MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574(); throw var3; } //MyAspectJDemo 切面類的后置通知織入 MyAspectJDemo.aspectOf().ajc$after$com_zejian_demo_MyAspectJDemo$2$4d789574(); } }
顯然AspectJ的織入原理已很明朗了,當然除了編譯期織入,還存在鏈接期(編譯后)織入,即將aspect類和java目標類同時編譯成字節碼文件后,再進行織入處理,這種方式比較有助於已編譯好的第三方jar和Class文件進行織入操作,由於這不是本篇的重點,暫且不過多分析。
參考資料
http://blog.csdn.net/javazejian/article/details/56267036#神一樣的aspectj-aop的領跑者