AspectJ 使用及原理
AspectJ 使用及原理
- 一.簡介
- 二.原理
- 三.使用
- (一)基本概念
- 1.連接點(JoinPoint)
- 2.切點(PointCut)
- 3.插入邏輯(Advice)
- 4.切面(Aspect)
- (二)類型匹配表達式
- 1.注解
- 2.修飾符
- 3.返回值
- 4.類
- 5.方法名
- 6.方法參數
- 7.組合使用
- (三)切入點組合表達式
- 1.call/execution
- 2.this
- 3.target
- 4.within
- 5.args
- 6.注解支持
- 7.組合使用
- 8.定義切點
- (四)插入邏輯
- 1.@Before
- 2.@After
- 3.@Around
- 4.@AfterReturning
- 5.@AfterThrowing
- 6.JoinPoint
- 7.運行時參數
- 8.@DeclareParents
- (五)定義切面
- 四.注意事項
一.簡介
Aspect Oriented Programming(AOP)面向切面編程是目前比較流行的一種編程方式,切面是指從不同的角度來看待同一個事物,比如我們做一個Android app需求時候,是從業務邏輯角度考慮的,需要實現一個Activity,實現一個model,實現一個View,再在Activity里完成對View和model控制等等;再比如我們需要對整個app的所有Activity做一些性能監控,這時候就需要從整個項目的角度來考慮,需要實現的就是統一對每個Activity或基類做處理,而不是某個業務的Activity了。
AOP就是可以通過預編譯方式和運行期動態代理實現在不修改源代碼的情況下給程序動態統一添加功能的一種編程思想,也就是不需要侵入代碼(比如修改基類等)就可以實現功能模塊,而AspectJ就是該思想的一種具體實現方式,它是一個面向切面的框架,它擴展了Java語言。
二.原理
AspectJ定義了AOP語法,所以它有一個專門的編譯器用來生成遵守Java字節編碼規范的Class文件。
AspectJ定義了一組注解,對應著一組概念,還有一套匹配表達式,我們通過書寫匹配表達式,告知AspectJ我們想在哪些地方添加代碼,然后再通過不同的注解,決定我們想以什么樣的方式在源代碼處做什么樣的事,最后在編譯時,AspectJ的編譯器就會按照我們的想法,在源代碼里注入新的代碼,也可以理解為AspectJ hook住了編譯過程,添加了一些代碼。
先舉個簡單的小例子來驗證下AspectJ的實現效果:
假如我們想要在所有的Activity的onCreate執行時,先輸出一個log,那么我們定義自己的切面
@Aspect class InheritAspect {private companion object {private const val TAG = "InheritAspect"private const val ON_CREATE_EXECUTION = "execution(void *..*Activity.onCreate(..))"}@Pointcut(ON_CREATE_EXECUTION)fun onCreateExecution() {}@Before("onCreateExecution()")fun beforeOnCreateExecution(joinPoint: JoinPoint) {Log.i(TAG,"onCreate start")} }代碼先不用管,意思就是定義了一個切面,攔截的是所有Activity的onCreate方法的執行,并且在執行時,輸出一個log
下面我們來看看,運行后其中一個Activity的文件,反編譯過后的結果:
//源代碼 protected void onCreate(@Nullable Bundle var1) {super.onCreate(var1); } //運行后 protected void onCreate(@Nullable Bundle var1) {JoinPoint var2 = Factory.makeJP(ajc$tjp_7, this, this, var1);//拿到InheritAspect的單例對象InheritAspect.aspectOf().beforeOnCreateExecution(var2);super.onCreate(var1); }我們可以看到,在這個Activity的onCreate方法中,先執行的就是InheritAspect類的beforeOnCreateExecution方法(JoinPoint對象就是包裝了一些切點的信息),該方法就是我們上面定義的那個方法,輸出了log,可見,AspectJ的編譯器按照我們的想法,在源碼上添加了代碼。
知道了大概原理,我們就來看看怎么使用吧!
三.使用
(一)基本概念
1.連接點(JoinPoint)
連接點是程序中可以插入代碼的地方,比如調用一個方法、一個方法執行中、調用一個構造器、一個構造器執行中等等,被AspectJ支持的連接點如下:
2.切點(PointCut)
切點其實就是想要在哪些地方插入一段代碼,也就是一組連接點集合的邏輯組合關系,比如一個切點可以定義為:調用A類的a方法時||調用B類的b方法時;這樣當調用A類的a方法這個連接點或者調用B類的b方法這個連接點時都會被該切點切入,從而插入代碼
3.插入邏輯(Advice)
上面說的是定義的切點,即靜態點,有了靜態點后,就需要插入代碼了,插入代碼的方式AspectJ也定義了幾種:
-
Before
在連接點之前插入代碼
-
After
在連接點之后插入代碼
-
Around
代理連接點,可以自定義是否執行連接點或返回何種結果等
4.切面(Aspect)
切面就是包裝了連接點、切點、插入邏輯的單一模塊,也是AspectJ掃描的單元,一個切面定義了一組AOP功能
以上是AspectJ的基本概念,下面帶著這些概念,進入到具體的使用學習中吧!
(二)類型匹配表達式
對于AOP來說,最重要的就是要準確的找到想要切入的點了,AspectJ提供了一套匹配表達式來完成,主要的結構如下({}為可選項):
{注解}{修飾符}<返回值>{類}<方法名><方法參數>下面我們來結合例子來學習,如何使用匹配表達式定位到具體方法
1.注解
此處的注解為標注在方法上的注解
@java.lang.Deprecated * *(..)表示標有@java.lang.Deprecated注解的所有方法
2.修飾符
public * *(..)表示所有public的方法,這個不用多說
3.返回值
* *(..)*表示任意類型,所以該表達式表示任何方法
void *(..)表示返回值為void的所有方法
java.lang.String *(..)表示返會String類型的所有方法
java.lang.String+ *(..)+表示子類,所以該表達式表示返回String及其子類類型的所有方法
java.lang.* *(..)表示返回java.lang包下所有類型的方法
java.lang.String* *(..)*也可以作為任意字符個數的匹配(前綴后綴),該表達式表示返回java.lang包下,以String為前綴的所有類型,的所有方法
java..* *(..)…表示的是當前包及其任意子包下(包括子包的子包等),該表達式表示返回java包及其底下所有包下的類型,的所有方法
(@java.lang.Deprecated *) *(..)此處注解聲明在*上(括號包住),表示該返回值的類型上有@java.lang.Deprecated注解的所有方法
4.類
類的匹配規則大多與返回值類似
* com..*Activity+.*(..)表示com包及其子包下,以Activity為后綴的所有類及其子類,的所有方法,此處+、*與…和返回值處一樣
* (@java.lang.Deprecated *).*(..)表示所有標有@java.lang.Deprecated注解的類的所有方法
5.方法名
方法名就相對簡單了,只有前綴后綴
* on*(..)表示所有以on為前綴的方法
6.方法參數
方法參數與上述匹配符略有不同
* *(..)表示所有方法,此處的…指的是不限參數個數和類型
* *()表示無參的所有方法
* *(*)此處的*表示只有一個參數,類型任意,該表達式表示所有只有一個參數的方法
* *(java..String*+)參數的類型表達式與上述相同,該表達式表示只有一個參數,且參數類型為java及其子包下以String為前綴的類型及其子類型,的所有方法
* *(@java.lang.Deprecated (@java.lang.Deprecated *))外層的注解,表示方法的這個參數上聲明這該注解(也可以寫為* (@java.lang.Deprecated ())),而內層的注解標注在*上,表示該參數的類型上標注著該注解;所以該表達式表示,有一個標有@java.lang.Deprecated注解的參數,且該參數類型上也標有@java.lang.Deprecated注解的所有方法,如:
public void a(@Deprecated Cat cat){ }@Deprecated class Cat{ }7.組合使用
以上就是類型匹配的表達式規則,下面我們組合起來寫一個復雜點的表達式來加深理解:
@java.lang.Deprecated public java.lang.String com..*Activity+.test*(*,int,..)該表達式表示:
在com及其所有子包下,以Activity為后綴的所有類及其子類中,以test為前綴的,且第一個參數是任意類型,第二個參數是int,后面的參數個數類型不限的方法,且方法是public的還帶有@java.lang.Deprecated注解
(三)切入點組合表達式
上面我們學會了如何找到指定的方法,但作為一個切點,還需要有其他更加靈活的限制條件來幫助我們插入代碼,因為我們往往不止要找到方法,還有滿足其他一些條件才行,并且這些條件我們可以通過邏輯關系進行組合,即使用&&、||、!操作符
1.call/execution
上面說過,AspectJ認可的連接點有很多,對于方法來說,主要有兩個,就是call和execution,這兩個連接點一定要弄清楚區別
call是指該方法在被外部調用的時候,而execution指該方法正在被調用的時候,即在方法體內的時候,這么說可能不清楚,舉個栗子:
call(* test(..))execution(* test(..)) public void a(){//call starttest()//call end } public void test(){//execution start...//execution end }由例子可知,如果我們的切點是基于call的,則我們的代碼是插入到a方法里調用test方法前后的,而不能管理test方法內部的執行過程;
如果我們的切點是基于execution的,則我們的代碼是插入到test方法里面的具體執行邏輯的前后的,可以影響test方法的內部執行
----------華麗的分割線----------
以上的內容其實都是編譯時靜態決定的,比如* *(android.content.Context+),我們攔截的是所有帶有一個Context或其子類型參數的方法,這在編譯期通過方法形參類型聲明就已經確定了,但是如果我們想準確截獲的是這些方法內,實際參數類型是Activity的呢?這就需要插入代碼進行運行時判斷了,也就是需要一些運行時的篩選條件幫我們定位,下面我們就來看看AspectJ為我們提供了哪些!
需要注意的就是以下的這些動態篩選條件,都不支持表達式匹配,而只能使用全路徑(因為是在業務代碼里添加的代碼)
2.this
連接點JoinPoint提供了this這個屬性,這個屬性代表的是當前的AOP代理對象,也可以理解為上下文環境,舉個栗子就明白了:
class AspectJActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_aspectj2)val animal: Animal = Cat()//callanimal.action()} }class AspectJActivity2 : AspectJActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)} }abstract class Animal {abstract fun action() }class Cat : Animal() {override fun action() {//execution} }(1)call(* action(..))&&this(AspectJActivity2)
對于call來說,連接點執行的上下文this就是AspectJActivity2的對象,也就是當前Activity的實際類型,因為AspectJ會在call處添加動態判斷代碼:
if(xxx instanceof AspectJActivity2){//執行插入代碼段}(2)execution(* action(..))&&this(Cat)
對于execution來說,連接點的上下文已經轉移到了方法內部,即該方法的對象內部了,此時this就是Cat對象,原理與上述相同,也是加入動態判斷代碼決定是否執行
3.target
連接點JoinPoint提供了target這個屬性,這個屬性代表的是真正執行該方法的對象,也就是該方法的對象了
還舉上述例子的話
call(* action(..))&&target(Cat)execution(* action(..))&&target(Cat)對于call和execution來說,target都是Cat對象,因為真正執行action方法的是Cat對象
其原理其實也是在需要的地方,加入動態代碼段進行判斷:
if(xxx instanceof Cat){//執行插入代碼段}4.within
連接點JoinPoint提供了within這個屬性,這個屬性是個靜態屬性,即該連接點的執行聲明在哪個類型中,withinType就是哪個類型
還舉上述例子
call(* action(..))&&within(AspectJActivity)execution(* action(..))&&within(Cat)當運行AspectJActivity2時,在基類里也會執行action,但此時的withinType仍然是AspectJActivity
對于execution,withinType就是定義方法的類,即action方法的類本身
5.args
連接點JoinPoint提供了args,用來約束實參的類型,舉個栗子:
class AspectJActivity2 : AspectJActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)a(this)} } fun a(context:Context){ } call(* a(Context+))&&args(Activity)execution(* a(Context+))&&args(Activity)對于方法a,接受的參數類型是Context,而我們使用args約束為Activity,則實際調用時,只有實參類型為Activity時才會執行插入代碼
也是通過插入動態判斷代碼實現的:
if(xxx instanceof Activity){//執行插入代碼段}6.注解支持
對于以上的動態篩選條件,都是匹配類的,還有一種情況就是注解,比如想要指定在運行中的“this”持有某個注解,此時就不能直接寫在this()里了,因為this()里只支持限定類
對此AspectJ提供了相應的匹配符來實現,其實就是在原有的匹配符前面加上@符號,括號里仍然寫類名(注解類)罷了,并且向源代碼中插入動態判斷代碼:使用xxx.getClass().isAnnotationPresent(xxx.class)
(1)@this/@target/@within
@this(java.lang.Deprecated)/@target(java.lang.Deprecated)/@within(java.lang.Deprecated)表示this/target/withinType對象帶有java.lang.Deprecated注解
(2)@args
@args(java.lang.Deprecated,java.lang.SafeVarags)表示第一個實際參數對象帶有java.lang.Deprecated注解,第二個實際參數帶有java.lang.SafeVarags注解
(3)@annotation
@annotation(java.lang.Deprecated)表示執行的方法帶有java.lang.Deprecated注解,感覺和類型匹配表達式里的意思一致
7.組合使用
以上就是動態條件的定義方式,下面通過一個組合例子加深一下理解:
call(* *(..))&&this(com.aspect.AspectJActivity2)&&target(com.model.Cat)&&within(com.aspect.AspectJActivity)&&args(com.model.Animal)&&@args(java.lang.Deprecated)該表達式表示的連接點為:當前上下文環境(this)是AspectJActivity2對象,且方法的執行對象(target)是Cat對象,且調用聲明在AspectJActivity類中,且第一個實際參數是Animal類型,且該實際參數類型上標有Deprecated注解,的所有方法,被外部調用時
8.定義切點
現在我們學會了如何書寫切點表達式,那么接下來我們看看如何定義一個切點吧
AspectJ提供了@PointCut注解來實現切點的定義,我們只需要寫一個方法,定義該注解,并且將注解的value設置為我們的切點表達式即可—代表該方法就是一個切點,具體切點就是我們定義的表達式,如下:
@Pointcut("call(* *(..))") fun onPointCutCall() { }該切點表示在所有方法被外部調用時
(四)插入邏輯
現在,我們有了切點,找到了具體的插入代碼位置,接下來就可以插入代碼了
上面說過,AspectJ提供了插入代碼的幾種方式:之前、之后、整體代理,相應的也是提供了幾種注解來實現,我們也只需要定義一個方法,標注相應的注解,將其value設置為想要攔截的切點(即我們上面說的PointCut),方法內部的代碼就會被插入到切點位置了,并且AspectJ還可以自動提供JoinPoint類型的參數,包裝基本信息供我們使用
1.@Before
該注解就是指在切入點代碼執行前插入一段代碼,如下:
@Before("onPointCutCall()") fun beforePointCutCall(joinPoint: JoinPoint) {//輸出logLog.i(TAG,"doing before") }表示在所有方法被外部調用前,加入一段代碼:輸出log
2.@After
該注解就是指在切入點代碼執行后插入一段代碼,默認情況下,包括切入點代碼正常return后、或throw異常退出后,都會執行,如下:
@After("onPointCutCall()") fun afterPointCutCall(joinPoint: JoinPoint) {//輸出logLog.i(TAG,"doing after") }表示在所有方法被外部調用后,加入一段代碼:輸出log
3.@Around
該注解就是指,將切入點代碼完全hook住,放到一個閉包里,整個替換為我們定義的方法體,并且我們可以拿到這個閉包內容,決定是否執行切入點源代碼以及返回什么內容等等,如下:
@Around("onPointCutCall()") fun aroundPointCutCall(joinPoint: ProceedingJoinPoint):Any? {Log.i(TAG,"doing before")//執行切入點源代碼val res = joinPoint.proceed()Log.i(TAG,"doing after")return res }表示在所有方法執行前,輸出log,然后執行方法,最后再輸出log,而返回值還是取自源代碼的結果
此時參數是ProceedingJoinPoint,是JoinPoint的子類,事實上,每個連接點返回的都是該類型對象,該對象可以調用proceed()方法執行源代碼
4.@AfterReturning
該注解是指在方法return(不管有沒有顯示的return)之后執行的hook,并且可以得到方法return的返回值
@AfterReturning(value = "xxx", returning = "result") public void afterReturnPointCut(boolean result) {System.out.println("result is "+result); }注解的returning參數指的是返回值的參數名,要與方法參數列表中的對應參數名稱相同
如果方法沒有返回值,那么AfterReturning捕獲的result就是null
5.@AfterThrowing
該注解是指在方法拋出異常后(不包括內部try-catch)執行的hook,并且可以得到拋出的異常對象
@AfterThrowing(value = "xxx", throwing = "ex") public void afterReturnPointCut(RuntimeException ex) {throw ex; }直接的throwing參數指的是所捕獲的異常的參數名,要與方法參數列表中的對應參數名稱相同
6.JoinPoint
該接口時接入點的基類接口,提供了一些編譯時信息和運行時信息,默認會傳遞給插入方法,它在每個插入方法調用前都會構建相應的對象,下面來看看它提供的一些參數意義:
/*** this:AOP代理對象* target:目標對象* args:參數類型列表* signature.methodName:連接點的方法名* signature.declaringTypeName:連接點的方法屬于的類型(編譯時類型)* sourceLocation.withinType:連接點聲明類(編譯時類型)* sourceLocation.fileName:調用連接點方法的源碼文件* sourceLocation.line:調用連接點方法的源碼的行數*/ fun showJoinPoint(joinPoint: JoinPoint, tag: String = "JoinPoint") {val thisObj = "this:${joinPoint.`this`?.javaClass?.name ?: "no"}\n"val targetObj = "target:${joinPoint.target?.javaClass?.name ?: "no"}\n"val args = "args:${joinPoint.args?.map { it?.javaClass?.name ?: "no" } ?: "no args"}\n"val methodName = "methodName:${joinPoint.signature.name}\n"val declareType = "declareType:${joinPoint.signature.declaringTypeName}\n"val withinType = "withinType:${joinPoint.sourceLocation.withinType?.name ?: "no within type"}\n"val sourceLocation = "sourceLocation:${joinPoint.sourceLocation.let { "${it.fileName}-${it.line}" }}\n" }這里要說明的是declareType,該類型指的是連接點方法屬于的對象,其編譯時的類型,比如call(* *(…))有兩處調用:
val animal:Animal = Cat(); animal.action()val cat:Cat = Cat(); cat.action()對應的declareType分別是Animal和Cat
ProceedingJoinPoint
該類型是JoinPoint的子類,多出來的是proceed()方法,用于執行切入點的源代碼
該類型也是運行中實際創建的JoinPoint類型,before和after執行proceed沒有意義,所以一般不用;而around一般用這個類型,用于執行源代碼
7.運行時參數
以上是插入代碼方式,但是這還不夠,因為我們很有可能在插入的代碼中,去操作實際參數,甚至是this、target對象等,而我們現在的參數只有JoinPoint,這顯然不夠,下面就來說說如何拿到運行時的一些對象
先來看例子:
@Pointcut(value = "call(* *(..))&&this(activity)&&target(cat)&&args(value)", argNames = "activity,cat,value") fun onPointCutCall(activity: AspectJActivity, cat: Cat, value: String) { }@Before(value = "onPointCutCall(activity,cat,value)", argNames = "activity,cat,value") fun beforePointCutCall(joinPoint: JoinPoint, activity: AspectJActivity, cat: Cat, value: String) {//do with params }上面的例子做到了拿到實際的運行時參數,怎么做到的呢?
AspectJ切點和插入邏輯的注解,不僅有value參數聲明切點,還提供了argNames參數,用來聲明參數名,而這些參數名就是定義在切入點表達式里的this/target/args里的類型,只不過和前面說的直接聲明類型不一樣,這里用參數名來代替了,那他們的類型又是什么呢?就是我們聲明在插入方法的參數列表的同名參數的類型:
比如上面的this(activity),名字為activity,對應的插入方法的參數就是activity: AspectJActivity參數,即類型是AspectJActivity,所以就相當于this(AspectJActivity),并且調用時把this的實際對象傳入到方法中,就是這么簡單,只要參數名對應上就行;而第一個參數JoinPoint我們可以不用聲明,因為它默認會被傳入到第一個參數中
該功能只適用于動態篩選條件:this/target/args
8.@DeclareParents
該注解是指,為指的切入點的類,實現一些接口,即添加一些公共功能
@DeclareParents(value = "com.test.Poi+", defaultImpl = PoiClaimCounter.class) public Counter counter;該例子為:對Poi及其子類實現了一個Counter接口,默認實現為PoiClaimCounter類,為其添加了一個計數器功能
這是,Poi類及其子類就可以轉換為Counter類型了,如:
@After(value = "xxx(counter)", argNames = "counter") public void afterPoiClaimCounter(Counter counter) {counter.count();//可以使用Poi的Counter的功能 }(五)定義切面
以上,我們學會了如何定義切點,如何插入代碼,他們都是寫在一個類里的,剩下的,就是上面說過的,把他們包裝在一起形成一個模塊,供AspectJ的編譯器進行掃描
AspectJ提供了@Aspect注解,我們只需將上述的類標有該注解即可,非常簡單
@Aspect class TestAspect {private companion object {private const val POINT_CUT = "call(* *(..))"}@Pointcut(POINT_CUT)fun onPointCut() {}@Before("onPointCut()")fun beforePointCut(joinPoint: JoinPoint) {...} }編譯時,編譯器就會掃描到標有@Aspect注解的該類,進行應用
四.注意事項
在實際使用的時候,發現了一些細節的點,總結了一些以供參考:
在類型匹配時,Number+,代表的是Number及其子類型,比如Double,Integer,但是不包括double,int,這些原始類型需要單獨聲明,比如int *(…)
對于同一個切點,如果同時聲明了@After和@Before,則不能聲明@Around,會報錯-編譯器不能決定插入順序;但是聲明其中之一和@Around是可以的
在匹配表達式中,如果父類的方法作為了切點,那么其子類重寫的相應方法也會被作為切點,比如:
execution(void com.aspect.AspectJActivity.test())-
AspectJActivity的test方法,以及AspectJActivity2重寫的test方法,都會被切入,即使表達式沒有寫成com.aspect.AspectJActivity+
-
然而對于AspectJActivity的父類test方法,則不會被切入
而對于call(void com.aspect.AspectJActivity.test())
-
在AspectJActivity中,調用this.test()方法時,會被切入
-
在AspectJActivity2中,直接調用test()方法(即this.test())也會被切入,但如果調用super.test(),則不會被切入(即使實質上調用的是AspectJActivity的test()方法),這點很神奇
關于動態篩選條件,this/target/args等,都是插入的動態判斷代碼,如instanceof、getClass().isAnnotationPresent()等,所以用的都是實際運行時類型,且包括了對子類的檢測,即使我們沒有書寫成this(AspectJActivity+)的形式(事實上并不能這么寫,因為動態篩選條件不支持匹配表達式)
對于動態篩選條件使用的注解,因為要在運行時判斷,所以需要其保持在運行時,即需要
@Retention(RetentionPolicy.RUNTIME)總結
以上是生活随笔為你收集整理的AspectJ 使用及原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: c语言字母分别代表的意思,C语言中%c,
- 下一篇: 二维码是什么