編輯:關於Android編程
大家都知道OOP,即ObjectOriented Programming,面向對象編程。而本文要介紹的是AOP。AOP是Aspect Oriented Programming的縮寫,中譯文為面向切向編程。OOP和AOP是什麼關系呢?首先:
l OOP和AOP都是方法論。我記得在剛學習C++的時候,最難學的並不是C++的語法,而是C++所代表的那種看問題的方法,即OOP。同樣,今天在AOP中,我發現其難度並不在利用AOP干活,而是從AOP的角度來看待問題,設計解決方法。這就是為什麼我特意強調AOP是一種方法論的原因!l 在OOP的世界中,問題或者功能都被劃分到一個一個的模塊裡邊。每個模塊專心干自己的事情,模塊之間通過設計好的接口交互。從圖示來看,OOP世界中,最常見的表示比如:
圖1 Android Framework中的模塊
圖1中所示為AndroidFramework中的模塊。OOP世界中,大家畫的模塊圖基本上是這樣的,每個功能都放在一個模塊裡。非常好理解,而且確實簡化了我們所處理問題的難度。
OOP的精髓是把功能或問題模塊化,每個模塊處理自己的家務事。但在現實世界中,並不是所有問題都能完美得劃分到模塊中。舉個最簡單而又常見的例子:現在想為每個模塊加上日志功能,要求模塊運行時候能輸出日志。在不知道AOP的情況下,一般的處理都是:先設計一個日志輸出模塊,這個模塊提供日志輸出API,比如Android中的Log類。然後,其他模塊需要輸出日志的時候調用Log類的幾個函數,比如e(TAG,...),w(TAG,...),d(TAG,...),i(TAG,...)等。
在沒有接觸AOP之前,包括我在內,想到的解決方案就是上面這樣的。但是,從OOP角度看,除了日志模塊本身,其他模塊的家務事絕大部分情況下應該都不會包含日志輸出功能。什麼意思?以ActivityManagerService為例,你能說它的家務事裡包含日志輸出嗎?顯然,ActivityManagerService的功能點中不包含輸出日志這一項。但實際上,軟件中的眾多模塊確實又需要打印日志。這個日志輸出功能,從整體來看,都是一個面上的。而這個面的范圍,就不局限在單個模塊裡了,而是橫跨多個模塊。
l 在沒有AOP之前,各個模塊要打印日志,就是自己處理。反正日志模塊的那幾個API都已經寫好了,你在其他模塊的任何地方,任何時候都可以調用。功能是得到了滿足,但是好像沒有Oriented的感覺了。是的,隨意加日志輸出功能,使得其他模塊的代碼和日志模塊耦合非常緊密。而且,將來要是日志模塊修改了API,則使用它們的地方都得改。這種搞法,一點也不酷。
AOP的目標就是解決上面提到的不cool的問題。在AOP中:
l 第一,我們要認識到OOP世界中,有些功能是橫跨並嵌入眾多模塊裡的,比如打印日志,比如統計某個模塊中某些函數的執行時間等。這些功能在各個模塊裡分散得很厲害,可能到處都能見到。l 第二,AOP的目標是把這些功能集中起來,放到一個統一的地方來控制和管理。如果說,OOP如果是把問題劃分到單個模塊的話,那麼AOP就是把涉及到眾多模塊的某一類問題進行統一管理。比如我們可以設計兩個Aspects,一個是管理某個軟件中所有模塊的日志輸出的功能,另外一個是管理該軟件中一些特殊函數調用的權限檢查。
講了這麼多,還是先來看個例子。在這個例子中,我們要:
l Activity的生命周期的幾個函數運行時,要輸出日志。l 幾個重要函數調用的時候,要檢查有沒有權限。
先來看沒有AOP的情況下,代碼怎麼寫。主要代碼都在AopDemoActivity中
[-->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項目中,官方網站是:
l http://www.eclipse.org/aspectj/ <=AspectJ官方網站l http://www.eclipse.org/aspectj/doc/released/runtime-api/index.html <=AspectJ類庫參考文檔,內容非常少l http://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/index.html <=@AspectJ文檔,以後我們用Annotation的方式最多。
題外話:AspectJ東西比較多,但是AOP做為方法論,它的學習和體會是需要一步一步,並且一定要結合實際來的。如果一下子講太多,反而會疲倦。更可怕的是,有些膽肥的同學要是一股腦把所有高級玩法全弄上去,反而得不償失。這就是是方法論學習和其他知識學習不一樣的地方。請大家切記。
Join Points(以後簡稱JPoints)是AspectJ中最關鍵的一個概念。什麼是JPoints呢?JPoints就是程序運行時的一些執行點。那麼,一個程序中,哪些執行點是JPoints呢?比如:
l 一個函數的調用可以是一個JPoint。比如Log.e()這個函數。e的執行可以是一個JPoint,而調用e的函數也可以認為是一個JPoint。l 設置一個變量,或者讀取一個變量,也可以是一個JPoint。比如Demo類中有一個debug的boolean變量。設置它的地方或者讀取它的地方都可以看做是JPoints。l for循環可以看做是JPoint。
理論上說,一個程序中很多地方都可以被看做是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中的輸出為從左到右,我們來解釋紅框中的內容。先來看左圖的第一個紅框:
l staticinitialization(test.Test.
再來看左圖第二個紅框,它表示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:
l testMethod的call類型JPointl testMethod的execution類型JPonintl 以及對異常捕獲的Handler類型JPoint
好了。JPoint的介紹就先到此。現在大家對JoinPoint應該有了一個很直觀的體會,簡單直白粗暴點說,JoinPoint就是一個程序中的關鍵函數(包括構造函數)和代碼段(staticblock)。
為什麼AspectJ首先要定義好JoinPoint呢?大家仔細想想就能明白,以打印log的AopDemo為例,log在哪裡打印?自然是在一些關鍵點去打印。而誰是關鍵點?AspectJ定義的這些類型的JPoint就能滿足我們絕大部分需求。
注意,要是想在一個for循環中打印一些日志,而AspectJ沒有這樣的JPoint,所以這個需求我們是無法利用AspectJ來實現了。另外,不同的軟件框架對表1中的JPoint類型支持也不同。比如Spring中,不是所有AspectJ支持的JPoint都有。
圖2的示例代碼我也放到了https://code.csdn.net/Innost/androidaopdemo上。請小伙伴們自己下載玩耍。具體的操作方法是:
l 下載得到androidaopdemo中,有一個aspectj-test目錄。l aspectj-test目錄中有一個libs目錄,裡邊有一個文件aspectj-1.8.7.jar文件。執行這個文件(java -jar aspectj-1.8.7.jar,安裝aspectj)。安裝完後,按照圖示要求將aspectj的安裝路徑加到PATH中,然後export。這樣,就可以在命令行中執行aspectj的命令了。比如編譯工具ajc。l 另外,libs下還有一個aspectjrt.jar,這個是aspectjt運行時的依賴庫。使用AspectJ的程序都要包含該jar包。l 執行create-jar.sh。它會編譯幾個文件,然後生成test.jar。l 執行test.jar(java -jar test.jar),就會打印出圖3的log。
我已經編譯並提交了一個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則是到實踐中,確實有需求了,才回過頭來,重新查閱文檔來實施的。恩,那就一步一步來吧。
直接來看一個例子,現在我想把圖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合起來就是:
l 選擇那些調用println(而且不考慮println函數的參數是什麼)的Joinpoint。l 另外,調用者的類型不要是TestAspect的。
圖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。
上面這些東西,建議讀者:
l 進入androidaopdemo/aspectj-test目錄。l 修改test/TestAspect.aj文件。主要是其中的pointcuts:testAll()這一行。按照圖2中的解釋說明,隨便改改試試。l 執行./create-jar.sh,得到一個test.jar包,然後java -jar test.jar得到執行結果
注意: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中:
l 第一個紅框是修改後的testMethod,在這個testMethod中,肯定會拋出一個空指針異常。l 第二個紅框是我們配置的advice,除了before以外,還加了一個around。我們重點來看around,它的返回值是Object。雖然匹配的JPoint是testMethod,其定義的返回值是void。但是AspectJ考慮的很周到。在around裡,可以設置返回值類型為Object來表示返回任意類型的返回值。AspectJ在真正返回參數的時候,會自動進行轉換。比如,假設inttestMethod定義了int作為返回值類型,我們在around裡可以返回一個Integer,AspectJ會自動轉換成int作為返回值。l 再看around中的//proceed()這句話。這代表調用真正的JPoint函數,即testMethod。由於這裡我們屏蔽了proceed,所以testMethod真正的內容並未執行,故運行的時候空指針異常就不會拋出來。也就是說,我們完全截獲了testMethod的運行,甚至可以任意修改它,讓它執行別的函數都沒有問題。。
注意:從技術上說,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節從開始到現在我們學到了哪些內容:
l AspectJ中各種類型的JoinPoint,JPoint是一個程序的關鍵執行點,也是我們關注的重點。l pointcuts:提供了一種方法來選擇目標JPoint。程序有很多JPoint,但是需要一種方法來讓我們選擇我們關注的JPoint。這個方法就是利用pointcuts來完成的。l 通過pointcuts選擇了目標JPoint後,我們總得干點什麼吧?這就用上了advice。advice包括好幾種類型,一般情況下都夠我們用了。
上面這些東西都有點像函數定義,在Java中,這些東西都是要放到一個class裡的。在AspectJ中,也有類似的數據結構,叫aspect。
public aspect 名字 {//aspect關鍵字和class的功能一樣,文件名以.aj結尾 pointcuts定義... advice定義... }
你看,通過這種方式,定義一個aspect類,就把相關的JPoint和advice包含起來,是不是形成了一個“關注面”?比如:
l 我們定義一個LogAspect,在LogAspect中,我們在關鍵JPoint上設置advice,這些advice就是打印日志l 再定義一個SecurityCheckAspect,在這個Aspect中,我們在關鍵JPoint上設置advice,這些advice將檢查調用app是否有權限。
通過這種方式,我們在原來的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,都可以打印出來。
總結,參數傳遞其實並不復雜,關鍵是得記住語法:
l pointcuts修改:像定義函數一樣定義pointcuts,然後在this,target或args中綁定參數名(注意,不再是參數類型,而是參數名)。l advice修改:也像定義函數一樣定義advice,然後在冒號後面的pointcuts中綁定參數名(注意是參數名)l 在advice的代碼中使用參數名。
我們前面示例中都打印出了JPoint的信息,比如當前調用的是哪個函數,JPoint位於哪一行代碼。這些都屬於JPoint的信息。AspectJ為我們提供如下信息:
l thisJoinpoint對象:在advice代碼中可直接使用。代表JPoint每次被觸發時的一些動態信息,比如參數啊之類的、l thisJoinpointStatic對象:在advice代碼中可直接使用,代表JPoint中那些不變的東西。比如這個JPoint的類型,JPoint所處的代碼位置等。l thisEnclosingJoinPointStaticPart對象:在advice代碼中可直接使用。也代表JPoint中不可變的部分,但是它包含的東西和JPoint的類型有關,比如對一個call類型JPoint而言,thisEnclosingJoinPointStaticPart代表包含調用這個JPoint的函數的信息。對一個handler類型的JPoint而言,它代表包含這個try/catch的函數的信息。
關於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那樣,但是這樣就沒有太多意思了。我們玩點高級的。不過這個高級的玩法也是來源於現實需求:
l 權限檢查一般是針對API的,比如調用者是否有權限調用某個函數。l API往往是通過SDK發布的。一般而言,我們會在這個函數的注釋裡說明需要調用者聲明哪些權限。l 然後我們在API檢查調用者是不是申明了文檔中列出的權限。
如果我有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, needed permission is + neededPermission); return; }
如此這般,我們在API源碼中使用的注解信息,現在就可以在AspectJ中使用了。這樣,我們在源碼中定義注釋,然後利用AspectJ來檢查。圖9展示了執行的結果
圖9 權限檢查的例子
事情這樣就完了?很明顯沒有。為什麼?剛才權限檢查只是簡單得打出了日志,但是並沒有真正去做權限檢查。如何處理?這就涉及到AOP如何與一個程序中其他模塊交互的問題了。初看起來容易,其實有難度。
比如,DemoAspect雖然是一個類,但是沒有構造函數。而且,我們也沒有在代碼中主動去構造它。根據AsepctJ的說明,DemoAspect不需要我們自己去構造,AspectJ在編譯的時候會把構造函數給你自動加上。具體在程序什麼位置加上,其實是有規律的,但是我們並不知道,也不要去知道。
這樣的話,DemoAspect豈不是除了打打log就沒什麼作用了?非也!以此例的權限檢查為例,我們需要:
l 把真正進行權限檢查的地方封裝到一個模塊裡,比如SecurityCheck中。l SecurityCheck往往在一個程序中只會有一個實例。所以可以為它提供一個函數,比如getInstance以獲取SecurityCheck實例對象。l 我們就可以在DemoAspect中獲取這個對象,然後調用它的check函數,把最終的工作由SecurityCheck來檢查了。
恩,這其實是Aspect的真正作用,它負責收集Jpoint,設置advice。一些簡單的功能可在Aspect中來完成,而一些復雜的功能,則只是有Aspect來統一收集信息,並交給專業模塊來處理。
最終代碼:
@Before(checkPermssion(securityCheckAnnotation)) publicvoid check(JoinPoint joinPoint,SecurityCheckAnnotation securityCheckAnnotation){ String neededPermission = securityCheckAnnotation.declaredPermission(); Log.e(TAG, needed permission is + neededPermission); SecurityCheckManager manager =SecurityCheckManager.getInstanc(); if(manager.checkPermission(neededPermission) == false){ throw new SecurityException(Need to declare permission: + neededPermission); } return; }
圖10所示為最終的執行結果。
圖10 執行真正的權限檢查
注意,
1 所有代碼都放到https://code.csdn.net/Innost/androidaopdemo上了....2 編譯:請在ubuntu下使用gradle assemble。編譯結果放在out/apps/目錄下。關於gradle的使用,請大家參考我的另外一篇重磅文章http://blog.csdn.net/innost/article/details/48228651
最後我們來講講其他一些內容。首先是AspectJ的編譯。
l AspectJ比較強大,除了支持對source文件(即aj文件、或@AspectJ注解的Java文件,或普通java文件)直接進行編譯外,l 還能對Java字節碼(即對class文件)進行處理。有感興趣的同學可以對aspectj-test小例子的class文件進行反編譯,你會發現AspectJ無非是在被選中的JPoint的地方加一些hook函數。當然Before就是在調用JPoint之前加,After就是在JPoint返回之前加。l 更高級的做法是當class文件被加載到虛擬機後,由虛擬機根據AOP的規則進行hook。
在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; } } }
除了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。
本文實例為大家分享了Android分類側滑菜單的制作方法,供大家參考,具體內容如下classificmenuActivity.java代碼:package com.sis
MVP模式 ListView中嵌入checkBox的使用本文寫的是一個小demo,如何在ListView中嵌入checkBox配合使用,本篇文章與前面的嵌入Button類
在《Android基於IIS的APK下載(三)用JSON傳輸更新數據》一文中已經從服務器中拿到了更新數據,並且呈現到了UI中,結合前面的文章及效果圖(參見下圖),可以看到
安卓系統的刷機包分線刷包和卡刷包兩種,一般後綴為ROM的是線刷包,卡刷包的後綴名是ZIP壓縮文件,本身ROM的格式上就有區別,那麼用刷機精靈的rom可以卡刷