非侵入式AOP——AspectJ使用

AspectJAndroid平臺上一種比較高效和簡單的實現編譯時AOP技術的方案。java

what is AOP?

  • 百度百科定義:在軟件業,AOP爲Aspect Oriented Programming的縮寫,意爲:面向切面編程,經過預編譯方式運行期動態代理實現程序功能的統一維護的一種技術。AOP是OOP的延續,是軟件開發中的一個熱點,也是Spring框架中的一個重要內容,是函數式編程的一種衍生範型。利用AOP能夠對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度下降,提升程序的可重用性,同時提升了開發的效率。android

  • 簡單的來說,AOP是一種:能夠在不改變原來代碼的基礎上,經過「動態注入」代碼,來改變原來執行結果的技術。git

what can Aspectj do?

  • 性能監控: 在方法調用先後記錄調用時間,方法執行太長或超時報警。
  • 無痕埋點: 在須要埋點的地方添加對應統計代碼。
  • 緩存代理: 緩存某方法的返回值,下次執行該方法時,直接從緩存裏獲取。
  • 記錄日誌: 在方法執行先後記錄系統日誌。
  • 權限驗證: 方法執行前驗證是否有權限執行當前方法,沒有則拋出沒有權限執行異常,由業務代碼捕捉。
  • 其餘(結合業務擴展)

Aspectj術語

  • Join Points
    • 簡稱JPoints,是AspectJ的核心思想之一,它就像一把刀,把程序的整個執行過程切成了一段段不一樣的部分。例如,構造方法調用、調用方法、方法執行、異常等等,這些都是Join Points,實際上,也就是你想把新的代碼插在程序的哪一個地方,是插在構造方法中,仍是插在某個方法調用前,或者是插在某個方法中,這個地方就是Join Points,固然,不是全部地方都能給你插的,只有能插的地方,才叫Join Points
  • Pointcuts
    • Pointcuts,實際上就是在Join Points中經過必定條件選擇出咱們所須要的Join Points,因此說,Pointcuts,也就是帶條件的Join Points,做爲咱們須要的代碼切入點。
  • Advice
    • Advice是指具體插入的代碼,以及如何插入這些代碼。例如Before、After、Around等。

引入Aspectj

app/build.grade加入如下配置項:github

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.1'
    }
}

repositories {
    mavenCentral()
}

android {
    ...
}

dependencies {
    ...
    compile 'org.aspectj:aspectjrt:1.8.1'
}

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.5",
                     "-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;
            }
        }
    }
}複製代碼

AspectJ 語法

JPoint的分類以及對應的Pointcut以下所示:
編程

Pointcut中的Signature以下所示:
緩存

Pointcut中的Signature由一段表達式組成,每一個關鍵詞之間都有空格,如下是對關鍵詞的說明:
bash

如下是Advice用法說明:
markdown

間接JPoint高級語法,以下所示:
併發

使用

首先定義兩個Model類,驗證結果用,代碼以下:app

StuModel:

package com.yy.live.aoptraining.model;
import android.util.Log;

/**
 * @className: StuModel
 * @classDescription: stu model for aspectj
 * @author: leibing
 * @email: leibing@yy.com
 * @createTime:2017/11/3
 */
public class StuModel {
// TAG
private final static String TAG = "AOP StuModel";
// stu name
private String stuName;

static {
Log.e(TAG, " StuModel static block");
}

/**
 * construction
 *
 * @param stuName
 */
public StuModel(String stuName) {
this.stuName = stuName;
Log.e(TAG, " StuModel Construction");
}

/**
 * get stu name
 *
 * @return
 */
public String getStuName() {
Log.e(TAG, " get stu name");
return stuName;
}

/**
 * set stu name
 *
 * @param stuName
 */
public void setStuName(String stuName) {
this.stuName = stuName;
Log.e(TAG, " set stu name");
}

/**
 * create throws
 */
public void createThrows(){
Log.e(TAG, " createThrows");
try {
String a = null;
a.toString();
}catch (Exception ex){
ex.printStackTrace();
}
}
}複製代碼

UserModel:

package com.yy.live.aoptraining.model;
import android.util.Log;

/**
 * @className: UserModel
 * @classDescription: user model for aspectj
 * @author: leibing
 * @email: leibing@yy.com
 * @createTime:2017/11/3
 */
public class UserModel {
// TAG
private final static String TAG = "AOP UserModel";
// user name
private String userName;

static {
Log.e(TAG, " UserModel static block");
}

/**
 * construction
 *
 * @param userName
 */
public UserModel(String userName) {
this.userName = userName;
Log.e(TAG, " UserModel Construction");
}

/**
 * get user name
 *
 * @return
 */
public String getUserName() {
Log.e(TAG, " get user name");
return this.userName;
}

/**
 * set user name
 *
 * @param userName
 */
public void setUserName(String userName) {
this.userName = userName;
Log.e(TAG, " set user name");
}

/**
 * work
 */
public void work() {
Log.e(TAG, " work");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}

/**
 * create throws
 */
public void createThrows(){
Log.e(TAG, " createThrows");
try {
Integer.parseInt("abc");
}catch (Exception ex){
ex.printStackTrace();
}
}
}複製代碼

構造函數

構造函數被調用

/**
 * 構造函數被調用
 */
@Pointcut("call(com.yy.live.aoptraining.model..*.new(..))")
public void callConstructor() {
}

/**
 * 執行(構造函數被調用)JPoint以前
 *
 * @param joinPoint
 */
@Before("callConstructor()")
public void beforeConstructorCall(JoinPoint joinPoint) {
Log.e(TAG, " before->" + joinPoint.getThis().toString() + "#" + joinPoint.getSignature().getName());
}

/**
 * 執行(構造函數被調用)JPoint以後
 *
 * @param joinPoint
 */
@After("callConstructor()")
public void afterConstructorCall(JoinPoint joinPoint) {
Log.e(TAG, " after->" + joinPoint.getThis().toString() + "#" + joinPoint.getSignature().getName());
}複製代碼

結果

11-09 11:26:39.840 18747-18747/com.yy.live.aoptraining E/AOP ConstructorAspect:  before->com.yy.live.aoptraining.constructor.ConstructorActivity@54b5f45#<init>
11-09 11:26:39.842 18747-18747/com.yy.live.aoptraining E/AOP UserModel:  UserModel static block
11-09 11:26:39.842 18747-18747/com.yy.live.aoptraining E/AOP UserModel:  UserModel Construction
11-09 11:26:39.842 18747-18747/com.yy.live.aoptraining E/AOP ConstructorAspect:  after->com.yy.live.aoptraining.constructor.ConstructorActivity@54b5f45#<init>複製代碼

從上面結果能夠看到在UserModel構造函數以前後分別插入了相關的日誌,從而實現了對構造函數被調用AOP處理。

構造函數執行內部

/**
 * 構造函數執行內部
 */
@Pointcut("execution(com.yy.live.aoptraining.model..*.new(..))")
public void executionConstructor() {}

/**
 * (構造函數執行內部)替換原來的代碼,若是要執行原來的代碼,需使用joinPoint.proceed(),不能和Before、After一塊兒使用
 * @param joinPoint
 * @throws Throwable
 */
@Around("executionConstructor()")
public void aroundConstructorExecution(ProceedingJoinPoint joinPoint) throws Throwable {
Log.e(TAG, " around->" + joinPoint.getThis().toString() + "#" + joinPoint.getSignature().getName());
// 執行原代碼
joinPoint.proceed();
}複製代碼

結果

11-09 11:32:38.677 24213-24213/com.yy.live.aoptraining E/AOP UserModel:  UserModel static block
11-09 11:32:38.678 24213-24213/com.yy.live.aoptraining E/AOP ConstructorAspect:  around->com.yy.live.aoptraining.model.UserModel@379a83e#<init>
11-09 11:32:38.678 24213-24213/com.yy.live.aoptraining E/AOP UserModel:  UserModel Construction
11-09 11:32:38.679 24213-24213/com.yy.live.aoptraining E/AOP StuModel:  StuModel static block
11-09 11:32:38.679 24213-24213/com.yy.live.aoptraining E/AOP ConstructorAspect:  around->com.yy.live.aoptraining.model.StuModel@a03b69f#<init>
11-09 11:32:38.679 24213-24213/com.yy.live.aoptraining E/AOP StuModel:  StuModel Construction複製代碼

從上面結果能夠看到在UserModel、StuModel構造函數以前分別插入了相關的日誌,從而實現了對構造函數執行內部AOP處理,@Around實現了和@Before、@Afrer同樣的效果,可是與其不能共用。

屬性

此處粗略說下讀取屬性,代碼以下:

/**
 * 讀變量
 */
@Pointcut("get(String com.yy.live.aoptraining.model.UserModel.userName)")
public void getField() {
}

/**
 * (讀變量)替換原來的代碼,若是要執行原來的代碼,需使用joinPoint.proceed(),不能和Before、After一塊兒使用
 *
 * @param joinPoint
 * @return
 * @throws Throwable
 */
@Around("getField()")
public String aroundFieldGet(ProceedingJoinPoint joinPoint) throws Throwable {
    // 執行原代碼
    Object obj = joinPoint.proceed();
    String userName = obj.toString();
    Log.e(TAG, " around->userName = " + userName);
    // 可在此處偷天換日更改類原有屬性的值
    return "李四";
}複製代碼

結果

11-09 11:46:03.788 3942-3942/com.yy.live.aoptraining E/AOP UserModel:  UserModel static block
11-09 11:46:03.788 3942-3942/com.yy.live.aoptraining E/AOP UserModel:  UserModel Construction
11-09 11:46:03.788 3942-3942/com.yy.live.aoptraining E/AOP UserModel:  get user name
11-09 11:46:03.789 3942-3942/com.yy.live.aoptraining E/AOP FieldAspect:  around->userName = 張三
11-09 11:46:03.789 3942-3942/com.yy.live.aoptraining E/AOP FieldActivity:  userName = 李四複製代碼

結果顯示已完美動態更改了屬性值。

方法

話很少說,show code:

/**
 * 函數被調用
 */
@Pointcut("call(* com.yy.live.aoptraining.model.UserModel.**(..))")
public void callMethod() {
}

/**
 * 執行(函數被調用)JPoint以前
 *
 * @param joinPoint
 */
@Before("callMethod()")
public void beforeMethodCall(JoinPoint joinPoint) {
    Log.e(TAG, " before->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
    beforeTarget =  joinPoint.getTarget().toString();
    beforeSignatureName = joinPoint.getSignature().getName();
    beforeTime  = System.currentTimeMillis();
}

/**
 * 執行(函數被調用)JPoint以後
 *
 * @param joinPoint
 */
@After("callMethod()")
public void afterMethodCall(JoinPoint joinPoint) {
    Log.e(TAG, " after->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName());
    afterTarget =  joinPoint.getTarget().toString();
    afterSignatureName = joinPoint.getSignature().getName();
    afterTime = System.currentTimeMillis();
    if (afterTarget != null
            && afterSignatureName != null
            && afterTarget.equals(beforeTarget)
            && afterSignatureName.equals(beforeSignatureName)) {
        long castTime = afterTime - beforeTime;
        Log.e(TAG, " monitor->" + joinPoint.getTarget().toString() + "#" + joinPoint.getSignature().getName() + "#cost " + castTime + " ms");
    }
}

/**
 * 替換原方法返回值
 * 注:@Pointcut能夠不單獨定義方法,直接使用,以下:
 *
 * @param joinPoint
 * @return
 * @throws Throwable
 * @Around("execution(* com.yy.live.aoptraining.model.UserModel.getUserName(..))")
 */
@Around("execution(* com.yy.live.aoptraining.model.UserModel.getUserName(..))")
public String aroundGetUserNameMethodExecution(ProceedingJoinPoint joinPoint) throws Throwable {
    String originUserName = joinPoint.proceed().toString();
    Log.e(TAG, " origin userName = " + originUserName);
    // 此處可對原方法作偷天換日處理
    return "王五";
}複製代碼

結果

11-09 12:02:50.681 18513-18513/com.yy.live.aoptraining E/AOP MethodAspect:  before->com.yy.live.aoptraining.model.UserModel@33363ec#work
11-09 12:02:50.681 18513-18513/com.yy.live.aoptraining E/AOP UserModel:  work
11-09 12:02:52.684 18513-18513/com.yy.live.aoptraining E/AOP MethodAspect:  after->com.yy.live.aoptraining.model.UserModel@33363ec#work
11-09 12:02:52.684 18513-18513/com.yy.live.aoptraining E/AOP MethodAspect:  monitor->com.yy.live.aoptraining.model.UserModel@33363ec#work#cost 2003 ms複製代碼

結果顯示在方法執行先後作了AOP日誌插入並統計了該方法在主線程耗時時間。

異常

AOP的使用場景包括異常處理、統計異常,代碼以下:

/**
 * 異常處理,用於統計全部出現Exception的點
 * 不支持@After、@Around
 */
@Before("handler(java.lang.Exception)")
public void handler() {
Log.e(TAG, " handler");
}

/**
 * 異常退出,用於收集拋出異常的方法信息
 * @AfterThrowing
 * @param throwable
 */
@AfterThrowing(pointcut = "call(* *..*(..))", throwing = "throwable")
public void anyFuncThrows(Throwable throwable) {
Log.e(TAG, " Throwable: ", throwable);
}複製代碼

結果

11-09 12:10:55.419 25880-25880/com.yy.live.aoptraining E/AOP UserModel:  createThrows
11-09 12:10:55.420 25880-25880/com.yy.live.aoptraining E/AOP MethodAspect:  Throwable: 
   java.lang.NumberFormatException: For input string: "abc"
   at java.lang.Integer.parseInt(Integer.java:521)
   at java.lang.Integer.parseInt(Integer.java:556)
   at com.yy.live.aoptraining.model.UserModel.createThrows(UserModel.java:79)
   at com.yy.live.aoptraining.method.MethodActivity.onCreate(MethodActivity.java:28)
   at android.app.Activity.performCreate(Activity.java:6910)
   at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1123)
   at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2746)
   at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2864)
   at android.app.ActivityThread.-wrap12(ActivityThread.java)
   at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1567)
   at android.os.Handler.dispatchMessage(Handler.java:105)
   at android.os.Looper.loop(Looper.java:156)
   at android.app.ActivityThread.main(ActivityThread.java:6577)
   at java.lang.reflect.Method.invoke(Native Method)
   at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:942)
   at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:832)
11-09 12:10:55.420 25880-25880/com.yy.live.aoptraining E/AOP MethodAspect:  handler複製代碼

經過AOP能夠統計對應的異常狀況而且將對應的異常放到一個統一的地方集中處理。

權限驗證

此處用一個6.0版本以上動態權限申請做爲示例,首先寫一個動態權限註解接口,代碼以下:

package com.yy.live.aoptraining.permission;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @className: YPermission
 * @classDescription: 動態權限申請註解
 * @author: leibing
 * @email: leibing@yy.com
 * @createTime:2017/11/3
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface YPermission {
String value();
}複製代碼

接着寫對應的Aspect,代碼以下:

package com.yy.live.aoptraining.permission;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.util.Log;
import com.yy.live.aoptraining.AppManager;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;

/**
 * @className: PermissionAspect
 * @classDescription: permission aspectj
 * @author: leibing
 * @email: leibing@yy.com
 * @createTime:2017/11/3
 */
@Aspect
public class PermissionAspect {
// TAG
private final static String TAG = "AOP PermissionAspect";

/**
 * 函數執行內部(採用註解動態權限處理)
 *
 * @param permission
 */
@Pointcut("execution(@com.yy.live.aoptraining.permission.YPermission * *(..)) && @annotation(permission)")
public void methodAnnotatedWithMPermission(YPermission permission) {
}

/**
 * (函數執行內部(採用註解動態權限處理))替換原來的代碼,若是要執行原來的代碼,需使用joinPoint.proceed(),不能和Before、After一塊兒使用
 *
 * @param joinPoint
 * @param permission
 * @throws Throwable
 */
@Around("methodAnnotatedWithMPermission(permission)")
public void checkPermission(final ProceedingJoinPoint joinPoint, YPermission permission) throws Throwable {
Log.e(TAG, " checkPermission");
// 權限
String permissionStr = permission.value();
// 模擬權限申請
if (AppManager.getInstance().currentActivity() != null) {
new AlertDialog.Builder(AppManager.getInstance().currentActivity()).setTitle("提示")
.setMessage(permissionStr)
.setNegativeButton("取消", null)
.setPositiveButton("容許", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
Log.e(TAG, " checkPermission allow");
try {
// 繼續執行原方法
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}

}
}).create().show();
}
}
}複製代碼

代碼註釋也比較詳細,不細講了。

總結

本文粗略的描述了Aspectj一些基本的用法、應用場景並作了示例分析。一提及AOP,你們想到的就是埋點、埋點、仍是埋點,埋點只是AOP最基礎的功能罷了,還有不少更高級的用法:性能監控、權限驗證、數據校驗、緩存、其餘(項目中特別的一些需求)。目前我就遇到兩個問題,就是接收到廣播出現屢次重複的問題,因而就想辦法去過濾,因而就想到了用Handler作延遲處理,效果不太理想,雖然解決了一時的問題,而且還有個問題就是每一個廣播接收器處都要寫一遍,代碼有點冗餘,若是此處採用AOP就很是簡單了,寫一個註解,採用相似動態權限申請的方式去作一個統一處理就較完美的解決了這個問題,之後維護起來也很方便;另一個是點擊按鈕的時候,有時會屢次觸發事件,這種狀況會引起併發執行的相關bug,因而就是在點擊的時候設置按鈕爲不可點擊,邏輯處理完再設置爲可點擊,而後每一個這樣的事件都要寫一遍,若採用AOP,則所有集中處理了。相似的問題,應該還有挺多的。也許你們在擔憂採用Aspectj會帶來相關問題:性能問題?這個不用擔憂,Aspectj是屬於編譯時的,不會對app性能形成影響;增長apk包大小?
反編譯任意主流apk去看,apk包中代碼永遠是佔據小部分大小,資源 + so包等纔是重心,去查看了Aspectj編譯時插入的代碼(4KB)佔apk大小(1.5MB),幾乎微乎其微,基本沒影響;插件不支持multiple dex,插件方法數超65535?反編譯查看代碼發現使用Aspectj切入的頁面,只生成了一個ajc$preClinit初始化切點的方法,這對插件方法數的影響微乎其微。綜上述得之,使用Aspectj會帶來更多的便捷,提升工做效率,下降維護成本。

AspectJ Demo 源碼連接

相關文章
相關標籤/搜索