你們都知道OOP,即ObjectOriented Programming,面向對象編程。而本文要介紹的是AOP。AOP是Aspect Oriented Programming的縮寫,中譯文爲面向切向編程。OOP和AOP是什麼關係呢?首先:html
圖1中所示爲AndroidFramework中的模塊。OOP世界中,你們畫的模塊圖基本上是這樣的,每一個功能都放在一個模塊裏。很是好理解,並且確實簡化了咱們所處理問題的難度。java
OOP的精髓是把功能或問題模塊化,每一個模塊處理本身的家務事。但在現實世界中,並非全部問題都能完美得劃分到模塊中。舉個最簡單而又常見的例子:如今想爲每一個模塊加上日誌功能,要求模塊運行時候能輸出日誌。在不知道AOP的狀況下,通常的處理都是:先設計一個日誌輸出模塊,這個模塊提供日誌輸出API,好比Android中的Log類。而後,其餘模塊須要輸出日誌的時候調用Log類的幾個函數,好比e(TAG,...),w(TAG,...),d(TAG,...),i(TAG,...)等。android
在沒有接觸AOP以前,包括我在內,想到的解決方案就是上面這樣的。可是,從OOP角度看,除了日誌模塊自己,其餘模塊的家務事絕大部分狀況下應該都不會包含日誌輸出功能。什麼意思?以ActivityManagerService爲例,你能說它的家務事裏包含日誌輸出嗎?顯然,ActivityManagerService的功能點中不包含輸出日誌這一項。但實際上,軟件中的衆多模塊確實又須要打印日誌。這個日誌輸出功能,從總體來看,都是一個面上的。而這個面的範圍,就不侷限在單個模塊裏了,而是橫跨多個模塊。git
AOP的目標就是解決上面提到的不cool的問題。在AOP中:sql
講了這麼多,仍是先來看個例子。在這個例子中,咱們要:編程
注意,本文的例子代碼在https://code.csdn.net/Innost/androidaopdemo上。ubuntu
先來看沒有AOP的狀況下,代碼怎麼寫。主要代碼都在AopDemoActivity中api
[-->AopDemoActivity.java]安全
public class AopDemoActivity extends Activity { private static final String TAG = "AopDemoActivity"; onCreate,onStart,onRestart,onPause,onResume,onStop,onDestory返回前,都輸出一行日誌 protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.layout_main); Log.e(TAG,"onCreate"); } protected void onStart() { super.onStart(); Log.e(TAG, "onStart"); } protected void onRestart() { super.onRestart(); Log.e(TAG, "onRestart"); } protectedvoid onResume() { super.onResume(); Log.e(TAG, "onResume"); checkPhoneState會檢查app是否申明瞭android.permission.READ_PHONE_STATE權限 checkPhoneState(); } protected void onPause() { super.onPause(); Log.e(TAG, "onPause"); } protected void onStop() { super.onStop(); Log.e(TAG, "onStop"); } protected void onDestroy() { super.onDestroy(); Log.e(TAG, "onDestroy"); } private void checkPhoneState(){ if(checkPermission("android.permission.READ_PHONE_STATE")== false){ Log.e(TAG,"have no permission to read phone state"); return; } Log.e(TAG,"Read Phone State succeed"); return; } private boolean checkPermission(String permissionName){ try{ PackageManager pm = getPackageManager(); //調用PackageMangaer的checkPermission函數,檢查本身是否申明使用某權限 int nret = pm.checkPermission(permissionName,getPackageName()); return nret == PackageManager.PERMISSION_GRANTED; }...... } }
代碼很簡單。可是從這個小例子中,你也會發現要是這個程序比較複雜的話,處處都加Log,或者在某些特殊函數加權限檢查的代碼,真的是一件挺繁瑣的事情。數據結構
AOP雖然是方法論,但就好像OOP中的Java同樣,一些先行者也開發了一套語言來支持AOP。目前用得比較火的就是AspectJ了,它是一種幾乎和Java徹底同樣的語言,並且徹底兼容Java(AspectJ應該就是一種擴展Java,但它不是像Groovy[1]那樣的拓展。)。固然,除了使用AspectJ特殊的語言外,AspectJ還支持原生的Java,只要加上對應的AspectJ註解就好。因此,使用AspectJ有兩種方法:
l 徹底使用AspectJ的語言。這語言一點也不難,和Java幾乎同樣,也能在AspectJ中調用Java的任何類庫。AspectJ只是多了一些關鍵詞罷了。
l 或者使用純Java語言開發,而後使用AspectJ註解,簡稱@AspectJ。
Anyway,不論哪一種方法,最後都須要AspectJ的編譯工具ajc來編譯。因爲AspectJ實際上脫胎於Java,因此ajc工具也能編譯java源碼。
AspectJ如今託管於Eclipse項目中,官方網站是:
題外話:AspectJ東西比較多,可是AOP作爲方法論,它的學習和體會是須要一步一步,而且必定要結合實際來的。若是一會兒講太多,反而會疲倦。更可怕的是,有些膽肥的同窗要是一股腦把全部高級玩法全弄上去,反而得不償失。這就是是方法論學習和其餘知識學習不同的地方。請你們切記。
Join Points(之後簡稱JPoints)是AspectJ中最關鍵的一個概念。什麼是JPoints呢?JPoints就是程序運行時的一些執行點。那麼,一個程序中,哪些執行點是JPoints呢?好比:
理論上說,一個程序中不少地方均可以被看作是JPoint,可是AspectJ中,只有如表1所示的幾種執行點被認爲是JPoints:
表1 AspectJ中的Join Point
Join Points |
說明
|
示例 |
method call |
函數調用 |
好比調用Log.e(),這是一處JPoint |
method execution |
函數執行 |
好比Log.e()的執行內部,是一處JPoint。注意它和method call的區別。method call是調用某個函數的地方。而execution是某個函數執行的內部。 |
constructor call |
構造函數調用 |
和method call相似 |
constructor execution |
構造函數執行 |
和method execution相似 |
field get |
獲取某個變量 |
好比讀取DemoActivity.debug成員 |
field set |
設置某個變量 |
好比設置DemoActivity.debug成員 |
pre-initialization |
Object在構造函數中作得一些工做。 |
不多使用,詳情見下面的例子 |
initialization |
Object在構造函數中作得工做 |
詳情見下面的例子 |
static initialization |
類初始化 |
好比類的static{} |
handler |
異常處理 |
好比try catch(xxx)中,對應catch內的執行 |
advice execution |
這個是AspectJ的內容,稍後再說 |
|
表1列出了AspectJ所承認的JoinPoints的類型。下面咱們來看個例子以直觀體會一把。
圖2 示例代碼
圖2是一個Java示例代碼,下面咱們將打印出其中全部的join points。圖3所示爲打印出來的join points:
圖3 全部的join points
圖3中的輸出爲從左到右,咱們來解釋紅框中的內容。先來看左圖的第一個紅框:
再來看左圖第二個紅框,它表示TestBase的類的初始化,因爲源碼中爲TestBase定義了static塊,因此這個JPoint清晰指出了源碼的位置是at:Test.java:5
接着看左圖第三個紅框,它和對象的初始化有關。在源碼中,咱們只是構造了一個TestDerived對象。它會先觸發TestDerived Preinitialization JPoint,而後觸發基類TestBase的PreInitialization JPoint。注意紅框中的before和after 。在TestDerived和TestBase所對應的PreInitialization before和after中都沒有包含其餘JPoint。因此,Pre-Initialization應該是構造函數中一個比較基礎的Phase。這個階段不包括類中成員變量定義時就賦值的操做,也不包括構造函數中對某些成員變量進行的賦值操做。
而成員變量的初始化(包括成員變量定義時就賦值的操做,好比源碼中的int base = 0,以及在構造函數中所作的賦值操做,好比源碼中的this.derived = 1000)都被囊括到initialization階段。請讀者對應圖三第二個紅框到第三個紅框(包括第3個紅框的內容)看看是否是這樣的。
最後來看第5個紅框。它包括三個JPoint:
圖2的示例代碼我也放到了https://code.csdn.net/Innost/androidaopdemo上。請小夥伴們本身下載玩耍。具體的操做方法是:
我已經編譯並提交了一個test.jar到git上,小夥伴們能夠執行一把玩玩!
pointcuts這個單詞很差翻譯,此處直接用英文好了。那麼,Pointcuts是什麼呢?前面介紹的內容可知,一個程序會有不少的JPoints,即便是同一個函數(好比testMethod這個函數),還分爲call類型和execution類型的JPoint。顯然,不是全部的JPoint,也不是全部類型的JPoint都是咱們關注的。再次以AopDemo爲例,咱們只要求在Activity的幾個生命週期函數中打印日誌,只有這幾個生命週期函數纔是咱們業務須要的JPoint,而其餘的什麼JPoint我不須要關注。
怎麼從一堆一堆的JPoints中選擇本身想要的JPoints呢?恩,這就是Pointcuts的功能。一句話,Pointcuts的目標是提供一種方法使得開發者可以選擇本身感興趣的JoinPoints。
在圖2的例子中,怎麼把Test.java中全部的Joinpoint選擇出來呢?用到的pointcut格式爲:
pointcuttestAll():within(Test)。
AspectJ中,pointcut有一套標準語法,涉及的東西不少,還有一些比較高級的玩法。我本身琢磨了半天,需不須要把這些內容一股腦都搬到此文呢?回想我本身學習AOP的經歷,好像看了幾本書,記得比較清楚的都是簡單的case,而那些複雜的case則是到實踐中,確實有需求了,纔回過頭來,從新查閱文檔來實施的。恩,那就一步一步來吧。
好了。JPoint的介紹就先到此。如今你們對JoinPoint應該有了一個很直觀的體會,簡單直白粗暴點說,JoinPoint就是一個程序中的關鍵函數(包括構造函數)和代碼段(staticblock)。
爲何AspectJ首先要定義好JoinPoint呢?你們仔細想一想就能明白,以打印log的AopDemo爲例,log在哪裏打印?天然是在一些關鍵點去打印。而誰是關鍵點?AspectJ定義的這些類型的JPoint就能知足咱們絕大部分需求。
注意,要是想在一個for循環中打印一些日誌,而AspectJ沒有這樣的JPoint,因此這個需求咱們是沒法利用AspectJ來實現了。另外,不一樣的軟件框架對錶1中的JPoint類型支持也不一樣。好比Spring中,不是全部AspectJ支持的JPoint都有。
直接來看一個例子,如今我想把圖2中的示例代碼中,那些調用println的地方找到,該怎麼弄?代碼該這麼寫:
public pointcut testAll(): call(public * *.println(..)) && !within(TestAspect) ;
注意,aspectj的語法和Java同樣,只不過多了一些關鍵詞
咱們來看看上述代碼
第一個public:表示這個pointcut是public訪問。這主要和aspect的繼承關係有關,屬於AspectJ的高級玩法,本文不考慮。
pointcut:關鍵詞,表示這裏定義的是一個pointcut。pointcut定義有點像函數定義。總之,在AspectJ中,你得定義一個pointcut。
testAll():pointcut的名字。在AspectJ中,定義Pointcut可分爲有名和匿名兩種辦法。我的建議使用named方法。由於在後面,咱們要使用一個pointcut的話,就能夠直接使用它的名字就好。
testAll後面有一個冒號,這是pointcut定義名字後,必須加上。冒號後面是這個pointcut怎麼選擇Joinpoint的條件。
本例中,call(public * *.println(..))是一種選擇條件。call表示咱們選擇的Joinpoint類型爲call類型。
public **.println(..):這小行代碼使用了通配符。因爲咱們這裏選擇的JoinPoint類型爲call類型,它對應的目標JPoint必定是某個函數。因此咱們要找到這個/些函數。public 表示目標JPoint的訪問類型(public/private/protect)。第一個*表示返回值的類型是任意類型。第二個*用來指明包名。此處不限定包名。緊接其後的println是函數名。這代表咱們選擇的函數是任何包中定義的名字叫println的函數。固然,惟一肯定一個函數除了包名外,還有它的參數。在(..)中,就指明瞭目標函數的參數應該是什麼樣子的。好比這裏使用了通配符..,表明任意個數的參數,任意類型的參數。
再來看call後面的&&:AspectJ能夠把幾個條件組合起來,目前支持 &&,||,以及!這三個條件。這三個條件的意思不用我說了吧?和Java中的是同樣的。
來看最後一個!within(TestAspectJ):前面的!表示不知足某個條件。within是另一種類型選擇方法,特別注意,這種類型和前面講到的joinpoint的那幾種類型不一樣。within的類型是數據類型,而joinpoint的類型更像是動態的,執行時的類型。
上例中的pointcut合起來就是:
圖4展現了執行結果:
圖4 新pointcut執行結果 |
我在圖2所示的源碼中,爲Test類定義了一個public static void println()函數,因此圖4的執行結果就把這個println給匹配上了。
看完例子,咱們來說點理論。
pointcuts中最經常使用的選擇條件和Joinpoint的類型密切相關,好比圖5:
圖5 不一樣類型的JPoint對應的pointcuts查詢方法
以圖5爲例,若是咱們想選擇類型爲methodexecution的JPoint,那麼pointcuts的寫法就得包括execution(XXX)來限定。
除了指定JPoint類型外,咱們還要更進一步選擇目標函數。選擇的根據就是圖5中列出的什麼MethodSignature,ConstructorSignature,TypeSinature,FieldSignature等。名字聽起來陌生得很,其實就是指定JPoint對應的函數(包括構造函數),Static block的信息。好比圖4中的那個println例子,首先它的JPoint類型是call,因此它的查詢條件是根據MethodSignature來表達。一個Method Signature的完整表達式爲:
@註解 訪問權限 返回值的類型 包名.函數名(參數) @註解和訪問權限(public/private/protect,以及static/final)屬於可選項。若是不設置它們,則默認都會選擇。以訪問權限爲例,若是沒有設置訪問權限做爲條件,那麼public,private,protect及static、final的函數都會進行搜索。 返回值類型就是普通的函數的返回值類型。若是不限定類型的話,就用*通配符表示 包名.函數名用於查找匹配的函數。可使用通配符,包括*和..以及+號。其中*號用於匹配除.號以外的任意字符,而..則表示任意子package,+號表示子類。 好比: java.*.Date:能夠表示java.sql.Date,也能夠表示java.util.Date Test*:能夠表示TestBase,也能夠表示TestDervied java..*:表示java任意子類 java..*Model+:表示Java任意package中名字以Model結尾的子類,好比TabelModel,TreeModel 等 最後來看函數的參數。參數匹配比較簡單,主要是參數類型,好比: (int, char):表示參數只有兩個,而且第一個參數類型是int,第二個參數類型是char (String, ..):表示至少有一個參數。而且第一個參數類型是String,後面參數類型不限。在參數匹配中, ..表明任意參數個數和類型 (Object ...):表示不定個數的參數,且類型都是Object,這裏的...不是通配符,而是Java中表明不定參數的意思
是否是很簡單呢?
Constructorsignature和Method Signature相似,只不過構造函數沒有返回值,並且函數名必須叫new。好比: public *..TestDerived.new(..): public:選擇public訪問權限 *..表明任意包名 TestDerived.new:表明TestDerived的構造函數 (..):表明參數個數和類型都是任意 再來看Field Signature和Type Signature,用它們的地方見圖5。下面直接上幾個例子: Field Signature標準格式: @註解 訪問權限 類型 類名.成員變量名 其中,@註解和訪問權限是可選的 類型:成員變量類型,*表明任意類型 類名.成員變量名:成員變量名能夠是*,表明任意成員變量 好比, set(inttest..TestBase.base):表示設置TestBase.base變量時的JPoint Type Signature:直接上例子 staticinitialization(test..TestBase):表示TestBase類的static block handler(NullPointerException):表示catch到NullPointerException的JPoint。注意,圖2的源碼第23行截獲的實際上是Exception,其真實類型是NullPointerException。可是因爲JPointer的查詢匹配是靜態的,即編譯過程當中進行的匹配,因此handler(NullPointerException)在運行時並不能真正被截獲。只有改爲handler(Exception),或者把源碼第23行改爲NullPointerException才行。
以上例子,讀者均可以在aspectj-test例子中本身都試試。
除了根據前面提到的Signature信息來匹配JPoint外,AspectJ還提供其餘一些選擇方法來選擇JPoint。好比某個類中的全部JPoint,每個函數執行流程中所包含的JPoint。
特別強調,不論什麼選擇方法,最終都是爲了找到目標的JPoint。
表2列出了一些經常使用的非JPoint選擇方法:
表2 其它經常使用選擇方法
關鍵詞 |
說明
|
示例 |
within(TypePattern) |
TypePattern標示package或者類。TypePatter可使用通配符 |
表示某個Package或者類中的全部JPoint。好比 within(Test):Test類中(包括內部類)全部JPoint。圖2所示的例子就是用這個方法。 |
withincode(Constructor Signature|Method Signature) |
表示某個構造函數或其餘函數執行過程當中涉及到的JPoint |
好比 withinCode(* TestDerived.testMethod(..)) 表示testMethod涉及的JPoint withinCode( *.Test.new(..)) 表示Test構造函數涉及的JPoint |
cflow(pointcuts) |
cflow是call flow的意思 cflow的條件是一個pointcut |
好比 cflow(call TestDerived.testMethod):表示調用TestDerived.testMethod函數時所包含的JPoint,包括testMethod的call這個JPoint自己 |
cflowbelow(pointcuts) |
cflow是call flow的意思。 |
好比 cflowblow(call TestDerived.testMethod):表示調用TestDerived.testMethod函數時所包含的JPoint,不包括testMethod的call這個JPoint自己 |
this(Type) |
JPoint的this對象是Type類型。 (其實就是判斷Type是否是某種類型,便是否知足instanceof Type的條件) |
JPoint是代碼段(不管是函數,異常處理,static block),從語法上說,它都屬於一個類。若是這個類的類型是Type標示的類型,則和它相關的JPoint將所有被選中。 圖2示例的testMethod是TestDerived類。因此 this(TestDerived)將會選中這個testMethod JPoint |
target(Type) |
JPoint的target對象是Type類型 |
和this相對的是target。不過target通常用在call的狀況。call一個函數,這個函數可能定義在其餘類。好比testMethod是TestDerived類定義的。那麼 target(TestDerived)就會搜索到調用testMethod的地方。可是不包括testMethod的execution JPoint |
args(TypeSignature) |
用來對JPoint的參數進行條件搜索的 |
好比args(int,..),表示第一個參數是int,後面參數個數和類型不限的JPoint。
|
上面這些東西,建議讀者:
注意:this()和target()匹配的時候不能使用通配符。
圖6給出了修改示例和輸出:
圖6 示例代碼和輸出結果
注意,不是全部的AOP實現都支持本節所說的查詢條件。好比Spring就不支持withincode查詢條件。
恭喜,看到這個地方來,AspectJ的核心部分就掌握一大部分了。如今,咱們知道如何經過pointcuts來選擇合適的JPoint。那麼,下一步工做就很明確了,選擇這些JPoint後,咱們確定是須要幹一些事情的。好比前面例子中的輸出都有before,after之類的。這其實JPoint在執行前,執行後,都執行了一些咱們設置的代碼。在AspectJ中,這段代碼叫advice。簡單點說,advice就是一種Hook。
ASpectJ中有好幾個Hook,主要是根據JPoint執行時機的不一樣而不一樣,好比下面的:
before():testAll(){ System.out.println("before calling: " + thisJoinPoint);//打印這個JPoint的信息 System.out.println(" at:" + thisJoinPoint.getSourceLocation());//打印這個JPoint對應的源代碼位置 }
testAll()是前面定義的pointcuts,而before()定義了在這個pointcuts選中的JPoint執行前咱們要乾的事情。
表3列出了AspectJ所支持的Advice的類型:
表3 advice的類型
關鍵詞 |
說明
|
示例 |
before() |
before advice |
表示在JPoint執行以前,須要乾的事情 |
after() |
after advice |
表示JPoint本身執行完了後,須要乾的事情。 |
after():returning(返回值類型) after():throwing(異常類型) |
returning和throwing後面均可以指定具體的類型,若是不指定的話則匹配的時候不限定類型 |
假設JPoint是一個函數調用的話,那麼函數調用執行完有兩種方式退出,一個是正常的return,另一個是拋異常。 注意,after()默認包括returning和throwing兩種狀況 |
返回值類型 around() |
before和around是指JPoint執行前或執行後備觸發,而around就替代了原JPoint |
around是替代了原JPoint,若是要執行原JPoint的話,須要調用proceed |
注意,after和before沒有返回值,可是around的目標是替代原JPoint的,因此它通常會有返回值,並且返回值的類型須要匹配被選中的JPoint。咱們來看個例子,見圖7。
圖7 advice示例和結果
圖7中:
注意:從技術上說,around是徹底能夠替代before和after的。圖7中第二個紅框還把after給註釋掉了。若是不註釋掉,編譯時候報錯,[error]circular advice precedence: can't determine precedence between two or morepieces of advice that apply to the same join point: method-execution(voidtest.Test$TestDerived.testMethod())(你們能夠本身試試)。我猜想其中的緣由是around和after衝突了。around本質上表明瞭目標JPoint,好比此處的testMethod。而after是testMethod以後執行。那麼這個testMethod究竟是around仍是原testMethod呢?真是傻傻分不清楚!
(我以爲再加一些限制條件給after是能夠避免這個問題的,可是沒搞成功...)
advice講完了。如今回顧下3.2節從開始到如今咱們學到了哪些內容:
上面這些東西都有點像函數定義,在Java中,這些東西都是要放到一個class裏的。在AspectJ中,也有相似的數據結構,叫aspect。
public aspect 名字 {//aspect關鍵字和class的功能同樣,文件名以.aj結尾 pointcuts定義... advice定義... }
你看,經過這種方式,定義一個aspect類,就把相關的JPoint和advice包含起來,是否是造成了一個「關注面」?好比:
經過這種方式,咱們在原來的JPoint中,就不須要寫log打印的代碼,也不須要寫權限檢查的代碼了。全部這些關注點都挪到對應的Aspectj文件中來控制。恩,這就是AOP的精髓。
注意,讀者在把玩代碼時候,必定會碰到AspectJ語法不熟悉的問題。因此請讀者記得隨時參考官網的文檔。這裏有一個官方的語法大全:
http://www.eclipse.org/aspectj/doc/released/quick5.pdf 或者官方的另一個文檔也能夠:
http://www.eclipse.org/aspectj/doc/released/progguide/semantics.html
到此,AspectJ最基本的東西其實講差很少了,可是在實際使用AspectJ的時候,你會發現前面的內容還欠缺一點,尤爲是advice的地方:
l 前面介紹的advice都是沒有參數信息的,而JPoint確定是或多或少有參數的。並且advice既然是對JPoint的截獲或者hook也好,確定須要利用傳入給JPoint的參數乾點什麼事情。比方所around advice,我能夠對傳入的參數進行檢查,若是參數不合法,我就直接返回,根本就不須要調用proceed作處理。
往advice傳參數比較簡單,就是利用前面提到的this(),target(),args()等方法。另外,整個pointcuts和advice編寫的語法也有一些區別。具體方法以下:
先在pointcuts定義時候指定參數類型和名字
pointcut testAll(Test.TestDerived derived,int x):call(*Test.TestDerived.testMethod(..)) && target(derived)&& args(x)
注意上述pointcuts的寫法,首先在testAll中定義參數類型和參數名。這一點和定義一個函數徹底同樣
接着看target和args。此處的target和args括號中用得是參數名。而參數名則是在前面pointcuts中定義好的。這屬於target和args的另一種用法。
注意,增長參數並不會影響pointcuts對JPoint的匹配,上面的pointcuts選擇和
pointcut testAll():call(*Test.TestDerived.testMethod(..)) && target(Test.TestDerived) &&args(int)是同樣的
只不過咱們須要把參數傳入advice,才須要改造
接下來是修改advice:
Object around(Test.TestDerived derived,int x):testAll(derived,x){ System.out.println(" arg1=" + derived); System.out.println(" arg2=" + x); return proceed(derived,x); //注意,proceed就必須把全部參數傳進去。 }
advice的定義如今也和函數定義同樣,把參數類型和參數名傳進來。
接着把參數名傳給pointcuts,此處是testAll。注意,advice必須和使用的pointcuts在參數類型和名字上保持一致。
而後在advice的代碼中,你就能夠引用參數了,好比derived和x,均可以打印出來。
總結,參數傳遞其實並不複雜,關鍵是得記住語法:
咱們前面示例中都打印出了JPoint的信息,好比當前調用的是哪一個函數,JPoint位於哪一行代碼。這些都屬於JPoint的信息。AspectJ爲咱們提供以下信息:
關於thisJoinpoint,建議你們直接查看API文檔,很是簡單。其地址位於http://www.eclipse.org/aspectj/doc/released/runtime-api/index.html。
如今正式回到咱們的AndroidAopDemo這個例子來。咱們的目標是爲AopDemoActivity的幾個Activity生命週期函數加上log,另外爲checkPhoneState加上權限檢查。一切都用AOP來集中控制。
前面提到說AspectJ須要編寫aj文件,而後把AOP代碼放到aj文件中。可是在Android開發中,我建議不要使用aj文件。由於aj文件只有AspectJ編譯器才認識,而Android編譯器不認識這種文件。因此當更新了aj文件後,編譯器認爲源碼沒有發生變化,因此不會編譯它。
固然,這種問題在其餘不認識aj文件的java編譯環境中也存在。因此,AspectJ提供了一種基於註解的方法來把AOP實現到一個普通的Java文件中。這樣咱們就把AOP當作一個普通的Java文件來編寫、編譯就好。
立刻來看AopDemoActivity對應的DemoAspect.java文件吧。先看輸出日誌初版本:
[-->初版本]
package com.androidaop.demo; import android.util.Log; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.aspectj.lang.JoinPoint; @Aspect //必須使用@AspectJ標註,這樣class DemoAspect就等同於 aspect DemoAspect了 public class DemoAspect { staticfinal String TAG = "DemoAspect"; /* @Pointcut:pointcut也變成了一個註解,這個註解是針對一個函數的,好比此處的logForActivity() 其實它表明了這個pointcut的名字。若是是帶參數的pointcut,則把參數類型和名字放到 表明pointcut名字的logForActivity中,而後在@Pointcut註解中使用參數名。 基本和之前同樣,只是寫起來比較奇特一點。後面咱們會介紹帶參數的例子 */ @Pointcut("execution(* com.androidaop.demo.AopDemoActivity.onCreate(..)) ||" +"execution(* com.androidaop.demo.AopDemoActivity.onStart(..))") public void logForActivity(){}; //注意,這個函數必需要有實現,不然Java編譯器會報錯 /* @Before:這就是Before的advice,對於after,after -returning,和after-throwing。對於的註解格式爲 @After,@AfterReturning,@AfterThrowing。Before後面跟的是pointcut名字,而後其代碼塊由一個函數來實現。好比此處的log。 */ @Before("logForActivity()") public void log(JoinPoint joinPoint){ //對於使用Annotation的AspectJ而言,JoinPoint就不能直接在代碼裏獲得多了,而須要經過 //參數傳遞進來。 Log.e(TAG, joinPoint.toShortString()); } }
提示:若是開發者已經切到AndroidStudio的話,AspectJ註解是能夠被識別並能自動補齊。
上面的例子僅僅是列出了onCreate和onStart兩個函數的日誌,若是想在全部的onXXX這樣的函數里加上log,該怎麼改呢?
@Pointcut("execution(* *..AopDemoActivity.on*(..))") public void logForActivity(){};
圖8給出這個例子的執行結果:
圖8 AopDemoActivity執行結果
檢查權限這個功能的實現也能夠採用剛纔打印log那樣,可是這樣就沒有太多意思了。咱們玩點高級的。不過這個高級的玩法也是來源於現實需求:
若是我有10個API,10個不一樣的權限,那麼在10個函數的註釋裏都要寫,太麻煩了。怎麼辦?這個時候我想到了註解。註解的本質是源代碼的描述。權限聲明,從語義上來講,實際上是屬於API定義的一部分,兩者是一個統一體,而不是分離的。
Java提供了一些默認的註解,不過此處咱們要使用本身定義的註解:
package com.androidaop.demo; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; //第一個@Target表示這個註解只能給函數使用 //第二個@Retention表示註解內容須要包含的Class字節碼裏,屬於運行時須要的。 @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface SecurityCheckAnnotation {//@interface用於定義一個註解。 publicString declaredPermission(); //declarePermssion是一個函數,其實表明了註解裏的參數 } 怎麼使用註解呢?接着看代碼: //爲checkPhoneState使用SecurityCheckAnnotation註解,並指明調用該函數的人須要聲明的權限 @SecurityCheckAnnotation(declaredPermission="android.permission.READ_PHONE_STATE") private void checkPhoneState(){ //若是不使用AOP,就得本身來檢查權限 if(checkPermission("android.permission.READ_PHONE_STATE") ==false){ Log.e(TAG,"have no permission to read phone state"); return; } Log.e(TAG,"Read Phone State succeed"); return; }
下面,咱們來看看如何在AspectJ中,充分利用這注解信息來幫助咱們檢查權限。
/* 來看這個Pointcut,首先,它在選擇Jpoint的時候,把@SecurityCheckAnnotation使用上了,這代表全部那些public的,而且攜帶有這個註解的API都是目標JPoint 接着,因爲咱們但願在函數中獲取註解的信息,全部這裏的poincut函數有一個參數,參數類型是 SecurityCheckAnnotation,參數名爲ann 這個參數咱們須要在後面的advice裏用上,因此pointcut還使用了@annotation(ann)這種方法來告訴 AspectJ,這個ann是一個註解 */ @Pointcut("execution(@SecurityCheckAnnotation public * *..*.*(..)) && @annotation(ann)") publicvoid checkPermssion(SecurityCheckAnnotationann){}; /* 接下來是advice,advice的真正功能由check函數來實現,這個check函數第二個參數就是咱們想要 的註解。在實際運行過程當中,AspectJ會把這個信息從JPoint中提出出來並傳遞給check函數。 */ @Before("checkPermssion(securityCheckAnnotation)") publicvoid check(JoinPoint joinPoint,SecurityCheckAnnotationsecurityCheckAnnotation){ //從註解信息中獲取聲明的權限。 String neededPermission = securityCheckAnnotation.declaredPermission(); Log.e(TAG, joinPoint.toShortString()); Log.e(TAG, "\tneeded permission is " + neededPermission); return; }
如此這般,咱們在API源碼中使用的註解信息,如今就能夠在AspectJ中使用了。這樣,咱們在源碼中定義註釋,而後利用AspectJ來檢查。圖9展現了執行的結果
圖9 權限檢查的例子
事情這樣就完了?很明顯沒有。爲何?剛纔權限檢查只是簡單得打出了日誌,可是並無真正去作權限檢查。如何處理?這就涉及到AOP如何與一個程序中其餘模塊交互的問題了。初看起來容易,其實有難度。
好比,DemoAspect雖然是一個類,可是沒有構造函數。並且,咱們也沒有在代碼中主動去構造它。根據AsepctJ的說明,DemoAspect不須要咱們本身去構造,AspectJ在編譯的時候會把構造函數給你自動加上。具體在程序什麼位置加上,實際上是有規律的,可是咱們並不知道,也不要去知道。
這樣的話,DemoAspect豈不是除了打打log就沒什麼做用了?非也!以此例的權限檢查爲例,咱們須要:
恩,這實際上是Aspect的真正做用,它負責收集Jpoint,設置advice。一些簡單的功能可在Aspect中來完成,而一些複雜的功能,則只是有Aspect來統一收集信息,並交給專業模塊來處理。
最終代碼:
@Before("checkPermssion(securityCheckAnnotation)") publicvoid check(JoinPoint joinPoint,SecurityCheckAnnotation securityCheckAnnotation){ String neededPermission = securityCheckAnnotation.declaredPermission(); Log.e(TAG, "\tneeded permission is " + neededPermission); SecurityCheckManager manager =SecurityCheckManager.getInstanc(); if(manager.checkPermission(neededPermission) == false){ throw new SecurityException("Need to declare permission:" + neededPermission); } return; }
圖10所示爲最終的執行結果。
圖10 執行真正的權限檢查
注意,
最後咱們來說講其餘一些內容。首先是AspectJ的編譯。
在Android裏邊,咱們用得是第二種方法,即對class文件進行處理。來看看代碼:
//AndroidAopDemo.build.gradle //此處是編譯一個App,因此使用的applicationVariants變量,不然使用libraryVariants變量 //這是由Android插件引入的。因此,須要import com.android.build.gradle.AppPlugin; android.applicationVariants.all { variant -> /* 這段代碼之意是: 當app編譯個每一個variant以後,在javaCompile任務的最後添加一個action。此action 調用ajc函數,對上一步生成的class文件進行aspectj處理。 */ AppPluginplugin = project.plugins.getPlugin(AppPlugin) JavaCompile javaCompile = variant.javaCompile javaCompile.doLast{ String bootclasspath =plugin.project.android.bootClasspath.join(File.pathSeparator) //ajc是一個函數,位於utils.gradle中 ajc(bootclasspath,javaCompile) } }
ajc函數其實和咱們手動試玩aspectj-test目標同樣,只是咱們沒有直接調用ajc命令,而是利用AspectJ提供的API作了和ajc命令同樣的事情。
import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main def ajc(String androidbootClassFiles,JavaCompile javaCompile){ String[] args = ["-showWeaveInfo", "-1.8", //1.8是爲了兼容java 8。請根據本身java的版本合理設置它 "-inpath",javaCompile.destinationDir.toString(), "-aspectpath",javaCompile.classpath.asPath, "-d",javaCompile.destinationDir.toString(), "-classpath",javaCompile.classpath.asPath, "-bootclasspath", androidbootClassFiles] MessageHandlerhandler = new MessageHandler(true); new Main().run(args,handler) deflog = project.logger for(IMessage message : handler.getMessages(null, true)) { switch (message.getKind()) { case IMessage.ABORT: case IMessage.ERROR: case IMessage.FAIL: log.error message.message, message.thrown throw message.thrown break; case IMessage.WARNING: case IMessage.INFO: log.info message.message, message.thrown break; case IMessage.DEBUG: log.debug message.message, message.thrown break; } } }
主要利用了https://eclipse.org/aspectj/doc/released/devguide/ajc-ref.html中TheAspectJ compiler API一節的內容。因爲代碼已經在csdn git上,你們下載過來直接用便可。
除了hook以外,AspectJ還能夠爲目標類添加變量。另外,AspectJ也有抽象,繼承等各類更高級的玩法。根據本文前面的介紹,這些高級玩法必定要靠需求來驅動。AspectJ確定對原程序是有影響的,如若貿然使用高級用法,則可能帶來一些未知的後果。關於這些內容,讀者根據狀況自行閱讀文後所列的參考文獻。
最後再來看一個圖。
圖11 未使用AOP的狀況
圖11中,左邊是一個程序的三個基於OOP而劃分的模塊(也就是concern)。安全、業務邏輯、交易管理。這三個模塊在設計圖上必定是互相獨立,互不干擾的。
可是在右圖實現的時候,這三個模塊就攪在一塊兒了。這和咱們在AndroidAopDemo中檢查權限的例子中徹底同樣。在業務邏輯的時候,須要顯示調用安全檢查模塊。
自從有了AOP,咱們就能夠去掉業務邏輯中顯示調用安全檢查的內容,使得代碼歸於乾淨,各個模塊又能各司其職。而這之中千絲萬縷的聯繫,都由AOP來鏈接和管理,豈不美哉?!
[1] Manning.AspectJ.in.Action第二版
看書仍是要挑簡單易懂的,AOP概念並不複雜,而AspectJ也有不少書,可是真正寫得通俗易懂的就是這本,雖然它本意是介紹Spring中的AOP,但對AspectJ的解釋真得是很是到位,並且還有對@AspectJ註解的介紹。本文除第一個圖外,其餘參考用圖全是來自於此書。
[2] http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/
Android中如何使用AspectJ,最重要的是它教會咱們怎麼使用aspectj編譯工具API。
[1] 關於Groovy更多的故事,請閱讀《深刻理解Android之Gradle》http://blog.csdn.net/innost/article/details/48228651