轉自:https://www.jianshu.com/p/aa1112dbebc7javascript
若是你用java作事後臺開發,那麼你必定知道AOP這個概念。若是不知道也無妨,套用百度百科的介紹,也能讓你明白這玩意是幹什麼的:java
AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,經過預編譯方式和運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP能夠對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度下降,提升程序的可重用性,同時提升了開發的效率。android
項目開發過程當中,可能會有這樣的需求,須要咱們在方法執行完成後,記錄日誌(後臺開發中比較常見~),或是計算這個方法的執行時間,在不使用AOP的狀況下,咱們能夠在方法最後調用另外一個專門記錄日誌的方法,或是在方法體的首尾分別獲取時間,而後經過計算時間差來計算整個方法執行所消耗的時間,這樣也能夠完成需求。那若是不僅一個方法要這麼玩怎麼辦?每一個方法都寫上一段相同的代碼嗎?後期處理邏輯變了要怎麼辦?最後老闆說這功能不要了咱們還得一個個刪除?git
很明顯,這是不可能的,咱們不只僅是代碼的搬運工,咱們仍是有思考能力的軟件開發工程師。這麼low的作法絕對不幹,這種問題咱們徹底能夠用AOP來解決,不就是在方法前和方法後插入一段代碼嗎?AOP分分鐘搞定。github
要注意了,AOP僅僅只是個概念,實現它的方式(工具和庫)有如下幾種:spring
本篇的主角就是AspectJ,下面就來看看AspectJ方式的AOP如何在Android開發中進行使用吧。編程
對於eclipse與Android Studio的引入是不同的,本篇只介紹Android Studio如何引入AspectJ,eclipse請自行百度。Android Studio須要在app模塊的build.gradle文件中引入,總共分爲3個步驟:bash
dependencies {
...
compile 'org.aspectj:aspectjrt:1.8.9' }
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath 'org.aspectj:aspectjtools:1.8.9' classpath 'org.aspectj:aspectjweaver:1.8.9' } }
AspectJ須要依賴maven倉庫。app
dependencies {
...
}
// 貼上面那段沒用的代碼是爲了說明:下面的任務代碼與dependencies同級 import org.aspectj.bridge.IMessage import org.aspectj.bridge.MessageHandler import org.aspectj.tools.ajc.Main final def log = project.logger final def variants = project.android.applicationVariants variants.all { variant -> if (!variant.buildType.isDebuggable()) { log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.") return; } JavaCompile javaCompile = variant.javaCompile javaCompile.doLast { String[] args = ["-showWeaveInfo", "-1.8", "-inpath", javaCompile.destinationDir.toString(), "-aspectpath", javaCompile.classpath.asPath, "-d", javaCompile.destinationDir.toString(), "-classpath", javaCompile.classpath.asPath, "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)] log.debug "ajc args: " + Arrays.toString(args) MessageHandler handler = new MessageHandler(true); new Main().run(args, handler); 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 break; case IMessage.WARNING: log.warn message.message, message.thrown break; case IMessage.INFO: log.info message.message, message.thrown break; case IMessage.DEBUG: log.debug message.message, message.thrown break; } } } }
直接粘貼到build.gradle文件的末尾便可,不要嵌套在別的指令中。框架
在使用AspectJ以前,仍是須要先介紹下AOP的基本知識,熟悉的看官能夠跳過這部分。
上述術語的解釋引用自《AOP中的概念通知、切點、切面》這篇文章,做者的描述很是直白,很容易理解,點個贊。
@Pointcut、@Before、@Around、@After、@AfterReturning、@AfterThrowing須要在切面類中使用,即在使用@Aspect的類中。
這就是切點表達式:execution (* com.lqr..*.*(..))。切點表達式的組成以下:
execution(<修飾符模式>? <返回類型模式> <方法名模式>(<參數模式>) <異常模式>?)
除了返回類型模式、方法名模式和參數模式外,其它項都是可選的。
修飾符模式指的是public、private、protected,異常模式指的是NullPointException等。
對於切點表達式的理解不是本篇重點,下面列出幾個例子說明一下就行了:
@Before("execution(public * *(..))") public void before(JoinPoint point) { System.out.println("CSDN_LQR"); }
匹配全部public方法,在方法執行以前打印"CSDN_LQR"。
@Around("execution(* *to(..))") public void around(ProceedingJoinPoint joinPoint) { System.out.println("CSDN"); joinPoint.proceed(); System.out.println("LQR"); }
匹配全部以"to"結尾的方法,在方法執行以前打印"CSDN",在方法執行以後打印"LQR"。
@After("execution(* com.lqr..*to(..))") public void after(JoinPoint point) { System.out.println("CSDN_LQR"); }
匹配com.lqr包下及其子包中以"to"結尾的方法,在方法執行以後打印"CSDN_LQR"。
@AfterReturning("execution(int com.lqr.*(..))") public void afterReturning(JoinPoint point, Object returnValue) { System.out.println("CSDN_LQR"); }
匹配com.lqr包下全部返回類型是int的方法,在方法返回結果以後打印"CSDN_LQR"。
@AfterThrowing(value = "execution(* com.lqr..*(..))", throwing = "ex") public void afterThrowing(Throwable ex) { System.out.println("ex = " + ex.getMessage()); }
匹配com.lqr包及其子包中的全部方法,當方法拋出異常時,打印"ex = 報錯信息"。
@Pointcut是專門用來定義切點的,讓切點表達式能夠複用。
你可能須要在切點執行以前和切點報出異常時作些動做(如:出錯時記錄日誌),能夠這麼作:
@Before("execution(* com.lqr..*(..))") public void before(JoinPoint point) { System.out.println("CSDN_LQR"); } @AfterThrowing(value = "execution(* com.lqr..*(..))", throwing = "ex") public void afterThrowing(Throwable ex) { System.out.println("記錄日誌"); }
能夠看到,表達式是同樣的,那要怎麼重用這個表達式呢?這就須要用到@Pointcut註解了,@Pointcut註解是註解在一個空方法上的,如:
@Pointcut("execution(* com.lqr..*(..))") public void pointcut() {}
這時,"pointcut()"就等價於"execution(* com.lqr..*(..))",那麼上面的代碼就能夠這麼改了:
@Before("pointcut()") public void before(JoinPoint point) { System.out.println("CSDN_LQR"); } @AfterThrowing(value = "pointcut()", throwing = "ex") public void afterThrowing(Throwable ex) { System.out.println("記錄日誌"); }
通過上面的學習,下面是時候實戰一下了,這裏咱們來一個簡單的例子。
這是界面上一個按鈕的點擊事件,就是一個簡單的方法而已,咱們拿它來試刀。
public void test(View view) { System.out.println("Hello, I am CSDN_LQR"); }
要織入一段代碼到目標類方法的前先後後,必需要有一個切面類,下面就是切面類的代碼:
@Aspect public class TestAnnoAspect { @Pointcut("execution(* com.lqr.androidaopdemo.MainActivity.test(..))") public void pointcut() { } @Before("pointcut()") public void before(JoinPoint point) { System.out.println("@Before"); } @Around("pointcut()") public void around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("@Around"); } @After("pointcut()") public void after(JoinPoint point) { System.out.println("@After"); } @AfterReturning("pointcut()") public void afterReturning(JoinPoint point, Object returnValue) { System.out.println("@AfterReturning"); } @AfterThrowing(value = "pointcut()", throwing = "ex") public void afterThrowing(Throwable ex) { System.out.println("@afterThrowing"); System.out.println("ex = " + ex.getMessage()); } }
先來試試看,這幾個註解的執行結果如何。
不對啊,按鈕的點擊事件中有打印"Hello, I am CSDN_LQR"的,這裏沒有,怎麼肥事?
這裏由於@Around環繞通知會攔截原方法內容的執行,咱們須要手動放行才能夠。代碼修改以下:
@Around("pointcut()") public void around(ProceedingJoinPoint joinPoint) throws Throwable { System.out.println("@Around"); joinPoint.proceed();// 目標方法執行完畢 }
也不對啊,少了一個@AfterThrowing通知。這個通知只有在切點拋出異常時纔會執行,咱們可讓代碼出現一個簡單的運行時異常:
public void test(View view) { System.out.println("Hello, I am CSDN_LQR"); int a = 1 / 0; }
這下@AfterThrowing通知確實被調用了,並且也打印出了錯誤信息(divide by zero)。但@AfterReturning通知反而不執行了,緣由很簡單,都拋出異常了,切點確定是不能返回結果的。也就是說:@AfterThrowing通知與@AfterReturning通知是衝突的,在同個切點上不可能同時出現。
由於@Around是環繞通知,能夠在切點的先後分別執行一些操做,AspectJ爲了能確定操做是在切點前仍是在切點後,因此在@Around通知中須要手動執行joinPoint.proceed()來肯定切點已經執行,故在joinPoint.proceed()以前的代碼會在切點執行前執行,在joinPoint.proceed()以後的代碼會切點執行後執行。因而,方法耗時計算的實現就是這麼簡單:
@Around("pointcut()") public void around(ProceedingJoinPoint joinPoint) throws Throwable { long beginTime = SystemClock.currentThreadTimeMillis(); joinPoint.proceed(); long endTime = SystemClock.currentThreadTimeMillis(); long dx = endTime - beginTime; System.out.println("耗時:" + dx + "ms"); }
發現沒有,上面全部的通知都會至少攜帶一個JointPoint參數,這個參數包含了切點的全部信息,下面就結合按鈕的點擊事件方法test()來解釋joinPoint能獲取到的方法信息有哪些:
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName(); // 方法名:test Method method = signature.getMethod(); // 方法:public void com.lqr.androidaopdemo.MainActivity.test(android.view.View) Class returnType = signature.getReturnType(); // 返回值類型:void Class declaringType = signature.getDeclaringType(); // 方法所在類名:MainActivity String[] parameterNames = signature.getParameterNames(); // 參數名:view Class[] parameterTypes = signature.getParameterTypes(); // 參數類型:View
前面的切點表達式結構是這樣的:
execution(<修飾符模式>? <返回類型模式> <方法名模式>(<參數模式>) <異常模式>?)
但實際上,上面的切點表達式結構並不完整,應該是這樣的:
execution(<@註解類型模式>? <修飾符模式>? <返回類型模式> <方法名模式>(<參數模式>) <異常模式>?)
這就意味着,切點能夠用註解來標記了。
若是用註解來標記切點,通常會使用自定義註解,方便咱們拓展。
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface TestAnnoTrace { String value(); int type(); }
其中的value和type是本身拓展的屬性,方便存儲一些額外的信息。
這個自定義註解只能註解在方法上(構造方法除外,構造方法也叫構造器,須要使用ElementType.CONSTRUCTOR),像日常使用其它註解同樣使用它便可:
@TestAnnoTrace(value = "lqr_test", type = 1) public void test(View view) { System.out.println("Hello, I am CSDN_LQR"); }
既然用註解來標記切點,那麼切點表達式確定是有所不一樣的,要這麼寫:
@Pointcut("execution(@com.lqr.androidaopdemo.TestAnnoTrace * *(..))") public void pointcut() {}
切點表達式使用註解,必定是@+註解全路徑,如:@com.lqr.androidaopdemo.TestAnnoTrace。
親測可用 ,不貼圖了。
上面在編寫自定義註解時就聲明瞭兩個屬性,分別是value和type,並且在使用該註解時也都爲之賦值了,那怎麼在通知中獲取這兩個屬性值呢?還記得JoinPoint這個參數吧,它就能夠獲取到註解中的屬性值,以下所示:
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 經過Method對象獲得切點上的註解 TestAnnoTrace annotation = method.getAnnotation(TestAnnoTrace.class); String value = annotation.value(); int type = annotation.type();