看AspectJ在Android中的強(qiáng)勢(shì)插入
什么是AOP
AOP是Aspect Oriented Programming的縮寫,即『面向切面編程』。它和我們平時(shí)接觸到的OOP都是編程的不同思想,OOP,即『面向?qū)ο缶幊獭?,它提倡的是將功能模塊化,對(duì)象化,而AOP的思想,則不太一樣,它提倡的是針對(duì)同一類問題的統(tǒng)一處理,當(dāng)然,我們?cè)趯?shí)際編程過程中,不可能單純的安裝AOP或者OOP的思想來編程,很多時(shí)候,可能會(huì)混合多種編程思想,大家也不必要糾結(jié)該使用哪種思想,取百家之長,才是正道。
那么AOP這種編程思想有什么用呢,一般來說,主要用于不想侵入原有代碼的場(chǎng)景中,例如SDK需要無侵入的在宿主中插入一些代碼,做日志埋點(diǎn)、性能監(jiān)控、動(dòng)態(tài)權(quán)限控制、甚至是代碼調(diào)試等等。
AspectJ
AspectJ實(shí)際上是對(duì)AOP編程思想的一個(gè)實(shí)踐,當(dāng)然,除了AspectJ以外,還有很多其它的AOP實(shí)現(xiàn),例如ASMDex,但目前***、最方便的,依然是AspectJ。
在Android項(xiàng)目中使用AspectJ
AOP的用處非常廣,從Spring到Android,各個(gè)地方都有使用,特別是在后端,Spring中已經(jīng)使用的非常方便了,而且功能非常強(qiáng)大,但是在Android中,AspectJ的實(shí)現(xiàn)是略閹割的版本,并不是所有功能都支持,但對(duì)于一般的客戶端開發(fā)來說,已經(jīng)完全足夠用了。
在Android上集成AspectJ實(shí)際上是比較復(fù)雜的,不是一句話就能compile,但是,鄙司已經(jīng)給大家把這個(gè)問題解決了,大家現(xiàn)在直接使用這個(gè)SDK就可以很方便的在Android Studio中使用AspectJ了。Github地址如下:
https://github.com/HujiangTec...
另外一個(gè)比較成功的使用AOP的庫是Jake大神的Hugo:
https://github.com/JakeWharto...
接入說明
首先,需要在項(xiàng)目根目錄的build.gradle中增加依賴:
- classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'
完整代碼如下:
- buildscript {
- repositories {
- jcenter()
- }
- dependencies {
- classpath 'com.android.tools.build:gradle:2.3.0-beta2'
- classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:1.0.8'
- // NOTE: Do not place your application dependencies here; they belong
- // in the individual module build.gradle files
- }
- }
然后再主項(xiàng)目或者庫的build.gradle中增加AspectJ的依賴:
- compile 'org.aspectj:aspectjrt:1.8.9'
同時(shí)在build.gradle中加入AspectJX模塊:
- apply plugin: 'android-aspectjx'
這樣就把整個(gè)Android Studio中的AspectJ的環(huán)境配置完畢了,如果在編譯的時(shí)候,遇到一些『can't determine superclass of missing type xxxxx』這樣的錯(cuò)誤,請(qǐng)參考項(xiàng)目README中關(guān)于excludeJarFilter的使用。
- aspectjx {
- //includes the libs that you want to weave
- includeJarFilter 'universal-image-loader', 'AspectJX-Demo/library'
- //excludes the libs that you don't want to weave
- excludeJarFilter 'universal-image-loader'
- }
AspectJ入門
我們通過一段簡單的代碼來了解下基本的使用方法和功能,新建一個(gè)AspectTest類文件,代碼如下:
- @Aspect
- public class AspectTest {
- private static final String TAG = "xuyisheng";
- @Before("execution(* android.app.Activity.on**(..))")
- public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
- String key = joinPoint.getSignature().toString();
- Log.d(TAG, "onActivityMethodBefore: " + key);
- }
- }
在類的最開始,我們使用@Aspect注解來定義這樣一個(gè)AspectJ文件,編譯器在編譯的時(shí)候,就會(huì)自動(dòng)去解析,并不需要主動(dòng)去調(diào)用AspectJ類里面的代碼。
我的原始代碼很簡單:
- public class MainActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- }
- }
通過這種方式編譯后,我們來看下生成的代碼是怎樣的。AspectJ的原理實(shí)際上是在編譯的時(shí)候,根據(jù)一定的規(guī)則解析,然后插入一些代碼,通過aspectjx生成的代碼,會(huì)在Build目錄下:
通過反編譯工具查看下生成內(nèi)容:
我們可以發(fā)現(xiàn),在onCreate的最前面,插入了一行AspectJ的代碼。這個(gè)就是AspectJ的主要功能,拋開AOP的思想來說,我們想做的,實(shí)際上就是『在不侵入原有代碼的基礎(chǔ)上,增加新的代碼』。
AspectJ之Join Points
Join Points,簡稱JPoints,是AspectJ的核心思想之一,它就像一把刀,把程序的整個(gè)執(zhí)行過程切成了一段段不同的部分。例如,構(gòu)造方法調(diào)用、調(diào)用方法、方法執(zhí)行、異常等等,這些都是Join Points,實(shí)際上,也就是你想把新的代碼插在程序的哪個(gè)地方,是插在構(gòu)造方法中,還是插在某個(gè)方法調(diào)用前,或者是插在某個(gè)方法中,這個(gè)地方就是Join Points,當(dāng)然,不是所有地方都能給你插的,只有能插的地方,才叫Join Points。
AspectJ之Pointcuts
Join Points和Pointcuts的區(qū)別實(shí)際上很難說,我也不敢說我理解的一定對(duì),但這些都是概念上的內(nèi)容,并不影響我們?nèi)ナ褂谩?/p>
Pointcuts,在我理解,實(shí)際上就是在Join Points中通過一定條件選擇出我們所需要的Join Points,所以說,Pointcuts,也就是帶條件的Join Points,作為我們需要的代碼切入點(diǎn)。
AspectJ之Advice
又來一個(gè)Advice,Advice其實(shí)是***理解的,也就是我們具體插入的代碼,以及如何插入這些代碼。我們最開始舉的那個(gè)例子,里面就是使用的最簡單的Advice——Before。類似的還有After、Around,我們后面來講講他們的區(qū)別。
AspectJ之切點(diǎn)語法
我們以前面的Demo來看下最簡單的AspectJ語法:
- @Before("execution(* android.app.Activity.on**(..))")
- public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
- }
這里會(huì)分成幾個(gè)部分,我們依次來看:
- @Before:Advice,也就是具體的插入點(diǎn)
- execution:處理Join Point的類型,例如call、execution
- ( android.app.Activity.on*(..)):這個(gè)是最重要的表達(dá)式,***個(gè)『*』表示返回值,『*』表示返回值為任意類型,后面這個(gè)就是典型的包名路徑,其中可以包含『*』來進(jìn)行通配,幾個(gè)『*』沒區(qū)別。同時(shí),這里可以通過『&&、||、!』來進(jìn)行條件組合。()代表這個(gè)方法的參數(shù),你可以指定類型,例如android.os.Bundle,或者(..)這樣來代表任意類型、任意個(gè)數(shù)的參數(shù)。
- public void onActivityMethodBefore:實(shí)際切入的代碼。
這里還有一些匹配規(guī)則,可以作為示例來進(jìn)行講解:
表達(dá)式 | 含義 |
---|---|
java.lang.String | 匹配String類型 |
java.*.String | 匹配java包下的任何“一級(jí)子包”下的String類型,如匹配java.lang.String,但不匹配java.lang.ss.String |
java..* | 匹配java包及任何子包下的任何類型,如匹配java.lang.String、java.lang.annotation.Annotation |
java.lang.*ing | 匹配任何java.lang包下的以ing結(jié)尾的類型 |
java.lang.Number+ | 匹配java.lang包下的任何Number的自類型,如匹配java.lang.Integer,也匹配java.math.BigInteger |
參數(shù) | 含義 |
---|---|
() | 表示方法沒有任何參數(shù) |
(..) | 表示匹配接受任意個(gè)參數(shù)的方法 |
(..,java.lang.String) | 表示匹配接受java.lang.String類型的參數(shù)結(jié)束,且其前邊可以接受有任意個(gè)參數(shù)的方法 |
(java.lang.String,..) | 表示匹配接受java.lang.String類型的參數(shù)開始,且其后邊可以接受任意個(gè)參數(shù)的方法 |
(*,java.lang.String) | 表示匹配接受java.lang.String類型的參數(shù)結(jié)束,且其前邊接受有一個(gè)任意類型參數(shù)的方法 |
AspectJ實(shí)例
Before、After
這兩個(gè)Advice應(yīng)該是使用的最多的,所以,我們先來看下這兩個(gè)Advice的實(shí)例,首先看下Before和After。
- @Before("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))")
- public void onActivityMethodBefore(JoinPoint joinPoint) throws Throwable {
- String key = joinPoint.getSignature().toString();
- Log.d(TAG, "onActivityMethodBefore: " + key);
- }
- @After("execution(* com.xys.aspectjxdemo.MainActivity.on*(android.os.Bundle))")
- public void onActivityMethodAfter(JoinPoint joinPoint) throws Throwable {
- String key = joinPoint.getSignature().toString();
- Log.d(TAG, "onActivityMethodAfter: " + key);
- }
經(jīng)過上面的語法解釋,現(xiàn)在看這個(gè)應(yīng)該很好理解了,我們來看下編譯后的類:
我們可以看見,在原始代碼的基礎(chǔ)上,增加了Before和After的代碼,Log也能被正確的插入并打印出來。
Around
Before和After其實(shí)還是很好理解的,也就是在Pointcuts之前和之后,插入代碼,那么Around呢,從字面含義上來講,也就是在方法前后各插入代碼,是的,他包含了Before和After的全部功能,代碼如下:
- @Around("execution(* com.xys.aspectjxdemo.MainActivity.testAOP())")
- public void onActivityMethodAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
- String key = proceedingJoinPoint.getSignature().toString();
- Log.d(TAG, "onActivityMethodAroundFirst: " + key);
- proceedingJoinPoint.proceed();
- Log.d(TAG, "onActivityMethodAroundSecond: " + key);
- }
其中,proceedingJoinPoint.proceed()代表執(zhí)行原始的方法,在這之前、之后,都可以進(jìn)行各種邏輯處理。
原始代碼:
- public class MainActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- testAOP();
- }
- public void testAOP() {
- Log.d("xuyisheng", "testAOP");
- }
- }
我們先來看下編譯后的代碼:
我們可以發(fā)現(xiàn),Around確實(shí)實(shí)現(xiàn)了Before和After的功能,但是要注意的是,Around和After是不能同時(shí)作用在同一個(gè)方法上的,會(huì)產(chǎn)生重復(fù)切入的問題。
自定義Pointcuts
自定義Pointcuts可以讓我們更加精確的切入一個(gè)或多個(gè)指定的切入點(diǎn)。
首先,我們需要自定義一個(gè)注解類,例如——DebugTool.java:
- /**
- * 自定義AOP注解
- * <p>
- * Created by xuyisheng on 17/1/12.
- */
- @Retention(RetentionPolicy.CLASS)
- @Target({ElementType.CONSTRUCTOR, ElementType.METHOD})
- public @interface DebugTool {
- }
然后在需要插入代碼的地方使用這個(gè)注解:
- public class MainActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- testAOP();
- }
- @DebugTool
- public void testAOP() {
- Log.d("xuyisheng", "testAOP");
- }
- }
***,我們來創(chuàng)建自己的切入文件。
- @Pointcut("execution(@com.xys.aspectjxdemo.DebugTool * *(..))")
- public void DebugToolMethod() {
- }
- @Before("DebugToolMethod()")
- public void onDebugToolMethodBefore(JoinPoint joinPoint) throws Throwable {
- String key = joinPoint.getSignature().toString();
- Log.d(TAG, "onDebugToolMethodBefore: " + key);
- }
先定義Pointcut,并申明要監(jiān)控的方法名,***,在Before或者其它Advice里面添加切入代碼,即可完成切入。
編譯好的代碼如下:
通過這種方式,我們可以非常方便的監(jiān)控指定的Pointcut,從而增加監(jiān)控的粒度。
call和execution
在AspectJ的切入點(diǎn)表達(dá)式中,我們前面都是使用的execution,實(shí)際上,還有一種類型——call,那么這兩種語法有什么區(qū)別呢,我們來試驗(yàn)下就知道了。
被切代碼依然很簡單:
- public class MainActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- testAOP();
- }
- public void testAOP() {
- Log.d("xuyisheng", "testAOP");
- }
- }
先來看execution,代碼如下:
- @Before("execution(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")
- public void methodAOPTest(JoinPoint joinPoint) throws Throwable {
- String key = joinPoint.getSignature().toString();
- Log.d(TAG, "methodAOPTest: " + key);
- }
編譯之后的代碼如下所示:
再來看下call,代碼如下:
- @Before("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")
- public void methodAOPTest(JoinPoint joinPoint) throws Throwable {
- String key = joinPoint.getSignature().toString();
- Log.d(TAG, "methodAOPTest: " + key);
- }
編譯之后的代碼如下所示:
其實(shí)對(duì)照起來看就一目了然了,execution是在被切入的方法中,call是在調(diào)用被切入的方法前或者后。
對(duì)于Call來說:
- Call(Before)
- Pointcut{
- Pointcut Method
- }
- Call(After)
對(duì)于Execution來說:
- Pointcut{
- execution(Before)
- Pointcut Method
- execution(After)
- }
切入點(diǎn)過濾與withincode
除了前面提到的call和execution,比較常用的還有一個(gè)withincode。這個(gè)語法通常來進(jìn)行一些切入點(diǎn)條件的過濾,作更加精確的切入控制。我們可以參考下面這個(gè)例子:
- public class MainActivity extends AppCompatActivity {
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
- setContentView(R.layout.activity_main);
- testAOP1();
- testAOP2();
- }
- public void testAOP() {
- Log.d("xuyisheng", "testAOP");
- }
- public void testAOP1() {
- testAOP();
- }
- public void testAOP2() {
- testAOP();
- }
- }
testAOP1()和testAOP2()都調(diào)用了testAOP()方法,但是,現(xiàn)在想在testAOP2()方法調(diào)用testAOP()方法的時(shí)候,才切入代碼,那么這個(gè)時(shí)候,就需要使用到Pointcut和withincode組合的方式,來精確定位切入點(diǎn)。
- // 在testAOP2()方法內(nèi)
- @Pointcut("withincode(* com.xys.aspectjxdemo.MainActivity.testAOP2(..))")
- public void invokeAOP2() {
- }
- // 調(diào)用testAOP()方法的時(shí)候
- @Pointcut("call(* com.xys.aspectjxdemo.MainActivity.testAOP(..))")
- public void invokeAOP() {
- }
- // 同時(shí)滿足前面的條件,即在testAOP2()方法內(nèi)調(diào)用testAOP()方法的時(shí)候才切入
- @Pointcut("invokeAOP() && invokeAOP2()")
- public void invokeAOPOnlyInAOP2() {
- }
- @Before("invokeAOPOnlyInAOP2()")
- public void beforeInvokeAOPOnlyInAOP2(JoinPoint joinPoint) {
- String key = joinPoint.getSignature().toString();
- Log.d(TAG, "onDebugToolMethodBefore: " + key);
- }
我們?cè)賮砜聪戮幾g后的代碼:
我們可以看見,只有在testAOP2()方法中被插入了代碼,這就做到了精確條件的插入。
異常處理AfterThrowing
AfterThrowing是一個(gè)比較少見的Advice,他用于處理程序中未處理的異常,記住,這點(diǎn)很重要,是未處理的異常,具體原因,我們等會(huì)看反編譯出來的代碼就知道了。我們隨手寫一個(gè)異常,代碼如下:
- public void testAOP() {
- View view = null;
- view.animate();
- }
然后使用AfterThrowing來進(jìn)行AOP代碼的編寫:
- @AfterThrowing(pointcut = "execution(* com.xys.aspectjxdemo.*.*(..))", throwing = "exception")
- public void catchExceptionMethod(Exception exception) {
- String message = exception.toString();
- Log.d(TAG, "catchExceptionMethod: " + message);
- }
這段代碼很簡單,同樣是使用我們前面類似的表達(dá)式,但是這里是為了處理異常,所以,使用了*.*來進(jìn)行通配,在異常中,我們執(zhí)行一行日志,編譯好的代碼如下:
我們可以看見com.xys.aspectjxdemo包下的所有方法都被加上了try catch,同時(shí),在catch中,被插入了我們切入的代碼,但是***,他依然會(huì)throw e,也就是說,這個(gè)異常已經(jīng)會(huì)被拋出去,崩潰依舊是會(huì)發(fā)生的。同時(shí),如果你的原始代碼中已經(jīng)try catch了,那么同樣也無法處理,具體原因,我們看一個(gè)反編譯的代碼:
可以看見,實(shí)際上,原始代碼的catch中,又被套了一層try catch,所以,e.printStackTrace()被try catch,也就不會(huì)再有異常發(fā)生了,也就無法切入了。
AspectJX使用案例
目前鄙司的很多項(xiàng)目都已經(jīng)使用了這套AOP方案,例如基于AOP的動(dòng)態(tài)權(quán)限管理、基于AOP的業(yè)務(wù)數(shù)據(jù)埋點(diǎn)、基于AOP的性能監(jiān)測(cè)系統(tǒng)等等。
現(xiàn)在已經(jīng)開源了一部分基于AOP的動(dòng)態(tài)權(quán)限管理的源碼,但由于需要?jiǎng)冸x業(yè)務(wù)代碼,所以后面會(huì)更加完善這功能代碼,大家可以繼續(xù)關(guān)注,github地址如下所示:
https://github.com/firefly112...
其它的AOP項(xiàng)目陸續(xù)開源中,大家可以持續(xù)關(guān)注~