Android中的AOP

在上一篇
使用自定義注解實現(xiàn)MVP中Model和View的注入
中,使用了自定義的方式進行依賴注入這一篇我們將繼續(xù)對注解進行深入了解。在日常的開發(fā)過程中,我們經(jīng)常會在同一個地方使用到相同的代碼,以往我們的處理方式是可以將其進行一個封裝,然后在
不同的地方進行調(diào)用這樣確實也很方便,但是還有另外的方式,就是自定義注解實現(xiàn)AOP。

需求:在開發(fā)過程中有很多頁面需要判斷登錄,實現(xiàn)這樣一個功能,能夠在不同需要實現(xiàn)的地方進行登錄的校驗!

AOP

AOPAspect Oriented Program的首字母縮寫AOP,其意是面向切面編程),其實很多前端的開發(fā)可能都沒有聽說過這個,但是對于
后端的小伙伴來說這個是在是太熟悉了,因為很多時候他們就靠這個來進行Log的打印。

那么AOP到底是什么呢?

AOP定義

先看定義:運行時,動態(tài)地將代碼切入到類的指定方法、指定位置上的編程思想

在解釋AOP之前,首先得說說和面向切面編程相對的另一個編程思想:面向?qū)ο缶幊蹋?code>OOP。在面向?qū)ο蟮乃枷胫?,我們以“一切皆對象”為原則,為不同的對象賦予不同的
功能,在需要使用到的時候,我們就對實例化對象,然后調(diào)用其功能,這樣降低了代碼的復雜度,使類可重用。

但是在使用的過程中,會出現(xiàn)這么一種情況,類A和類B,都需要進行實現(xiàn)一個功能(比如:是否登錄的判斷),以往我們的做法很簡單,
將這個登錄判斷的功能寫在一個類中(這里命名為C),然后在各自的引用的地方調(diào)用這個類的方法,確實這樣是解決了這個問題,但是
這樣卻使A,B 兩個類與C類之間就會有耦合。有沒有什么辦法,能讓我們在需要的時候,隨意地加入代碼呢?
為了解決這樣的問題就出現(xiàn)了面向切面編程的思想,即是:這種在運行時,動態(tài)地將代碼切入到類的指定方法、指定位置上的編程思想就是面向切面的編程

AOP和OOP之間的關(guān)系

AOP的實際操作是將幾個類之間共有的功能單獨出來,然后在這幾個需要的時候進行切入,改變其本來的運行方式。這樣分析下來,我們可以
得出一個結(jié)論,即是:面向切面編程(AOP)其實是面向?qū)ο缶幊蹋?code>OOP)的一個補充。

加入AspectJ

AspectJ AspectJ實際上是對AOP編程思想的一個實現(xiàn)。

  • 在項目的gradle文件下加入:

     dependencies {
             classpath 'com.android.tools.build:gradle:3.0.0'
             classpath 'org.aspectj:aspectjtools:1.8.9'
             classpath 'org.aspectj:aspectjweaver:1.8.9'
    
             // NOTE: Do not place your application dependencies here; they belong
             // in the individual module build.gradle files
         }
    
  • 在app的gradle文件下加入:

    1. 引入aspectjtools

      import org.aspectj.bridge.IMessage
      import org.aspectj.bridge.MessageHandler
      import org.aspectj.tools.ajc.Main
      
    2. 導入第三方包

      compile 'org.aspectj:aspectjrt:1.8.9'
      
  1. 使用AspectJ編譯器ajc

使用ajc會對所有受 aspect 影響的類進行織入,這樣才能使我們的Aspect

//獲取 log實例
final def log = project.logger
//獲取variants
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;
            }
        }
    }
}      

至此,我們就將AspectJ的準備工作做好了,那么接下來就是使用了

在Android中使用AOP

先來介紹幾個概念:

  • Pointcut:切入點,就是在程序運行過程中,在何處注入我們想運行的特定代碼。
    注意:這里的何處,并不是真正意義上的具體位置,而是可切入的范圍,比如整個包下面所有類及所有方法,或者某個類下面的所有方法。
  • Joint point:連接點,程序中可能作為代碼注入目標的特定的點,所以此處才是執(zhí)行注入的具體的位置。
  • Advice: 通知,即是在程序運行過程中,當執(zhí)行到切點位置時,執(zhí)行注入到class文件中什么樣的代碼,
    比較常用的類型是before,around,after。從字面上面我們就可以看出其意,
    就是在目標方法執(zhí)行之前,執(zhí)行之時替代目標方法,執(zhí)行之后的代碼。
  • Aspect: 切面,其實就是PointcutAdvice的組合,所以如上可以總結(jié)為在何處做什么。

創(chuàng)建@CheckLogin注解

可能有人會問:為什么是創(chuàng)建注解呢?不能是其的什么類或者對象么?
AOP本來就是為了解決耦合才進行使用的,如果使用其他的,或讓AspectJ與其耦合,那我們使用AOP干什么呢?


@Retention(RetentionPolicy.RUNTIME) //保留到源碼中,同時也保留到class中,最后加載到虛擬機中
@Target({ElementType.METHOD,ElementType.CONSTRUCTOR}) //可以注解在方法或構(gòu)造上
public @interface CheckLogin {
}

在上次的講解中已經(jīng)提到元注解@Retention,表示注解的表示方式,這里再回顧一下:

  • SOURCE:只保留在源碼中,不保留在class中,同時也不加載到虛擬機中
  • CLASS:保留在源碼中,同時也保留到class中,但是不加載到虛擬機中
  • RUNTIME:保留到源碼中,同時也保留到class中,最后加載到虛擬機中

@Target 這個注解表示注解的作用范圍,主要有如下:

  • ElementType.FIELD 注解作用于變量
  • ElementType.METHOD 注解作用于方法
  • ElementType.PARAMETER 注解作用于參數(shù)
  • ElementType.CONSTRUCTOR 注解作用于構(gòu)造方法
  • ElementType.LOCAL_VARIABLE 注解作用于局部變量
  • ElementType.PACKAGE 注解作用于包

所以如上的CheckLogin表示將注解可以注入到構(gòu)造方法和其他方法上,并且保留到源碼中,同時也保留到class中,最后加載到虛擬機中。

創(chuàng)建Aspect類

到此,才是我們這章的重點,就是怎么構(gòu)建一個Aspect類,這里以CheckLoginAspectJ為例。

@Aspect
public class CheckLoginAspectJ {
    private static final String TAG = "CheckLogin";

    /**
     * 找到處理的切點
     * * *(..)  可以處理CheckLogin這個類所有的方法
     */
    @Pointcut("execution(@com.yw.android.aoptest.aop.CheckLogin  * *(..))")
    public void executionCheckLogin() {

    }

    /**
     * 處理切面
     *
     * @param joinPoint
     * @return
     */
    @Around("executionCheckLogin()")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.i(TAG, "checkLogin: ");
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        CheckLogin checkLogin = signature.getMethod().getAnnotation(CheckLogin.class);
        if (checkLogin != null) {
            Context context = (Context) joinPoint.getThis();
            if (BaseApplication.isLogin) {
                Log.i(TAG, "checkLogin: 登錄成功 ");
                return joinPoint.proceed();
            } else {
                Log.i(TAG, "checkLogin: 請登錄");
                Toast.makeText(context, "請登錄", Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(context, LoginActivity.class);
                context.startActivity(intent);
                return null;
            }
        }
        return joinPoint.proceed();
    }

@Pointcut說明

在上方代碼Pointcut之后緊跟了一個execution的表達式,這個就代表切入點的位置,也就是我們上述的何處

解釋一下execution的用法:

execution僅僅是AOP中pointcut expression表達式中的一種。其他還有如下這幾種:

  • args():用于匹配當前執(zhí)行的方法傳入的參數(shù)為指定類型的執(zhí)行方法
  • @args():用于匹配當前執(zhí)行的方法傳入的參數(shù)持有指定注解的執(zhí)行
  • execution():用于匹配方法執(zhí)行的連接點
  • this():用于匹配當前AOP代理對象類型的執(zhí)行方法;注意是AOP代理對象的類型匹配,這樣就可能包括引入接口也類型匹配
  • target():用于匹配當前目標對象類型的執(zhí)行方法;注意是目標對象的類型匹配,這樣就不包括引入接口也類型匹配
  • @target():用于匹配當前目標對象類型的執(zhí)行方法,其中目標對象持有指定的注解;
  • within():用于匹配指定類型內(nèi)的方法執(zhí)行
  • @within():用于匹配所有持有指定注解類型內(nèi)的方法;
  • @annotation:用于匹配當前執(zhí)行方法持有指定注解的方法

這里重點解釋一下execution,因為在我們的日常使用中,execution是最多的。

類型匹配語法

  • *:匹配任何數(shù)量字符,即是全部;
  • ..:匹配任何數(shù)量字符的重復,如在類型模式中匹配任何數(shù)量子包;而在方法參數(shù)模式中匹配任何數(shù)量參數(shù)。
  • +:匹配指定類型的子類型;僅能作為后綴放在類型模式后邊。
  • ():表示方法沒有任何參數(shù)
  • (..):表示匹配接受任意個參數(shù)的方法
//匹配String類型
java.lang.String
//匹配java包下任何子包的String類型
java.*.String
//匹配java包及任何子包下的任何類型
java..*

execution表達式

execution的表達式如下:
execution(modifiers-pattern? ret-type-pattern declaring-type-pattern? name-pattern(param-pattern)throws-pattern?)

  • modifiers-pattern:修飾符匹配,如publicprivate、protect,可選。
  • ret-type-pattern:返回類型匹配,必填。
  • declaring-type-pattern:聲明類型匹配,可選。
  • name-pattern(param-pattern):
    • name-pattern:方法名匹配,必填
    • param-pattern:方法參數(shù)匹配,必填
  • throws-pattern:異常匹配,可選。

至此,我們可以知道,上述中代碼代表的匹配意思了

"execution(@com.yw.android.aoptest.aop.CheckLogin  * *(..))"

返回類型:com.yw.android.aoptest.aop.CheckLogin;
聲明類型: * ,表示任何
方法名: *,任何方法
參數(shù):(..),任意個參數(shù)

即是:匹配com.yw.android.aoptest.aop.CheckLogin類下的所有聲明和所以任意參數(shù)方法。

@Advice說明

@Around("executionCheckLogin()")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        ...
    }

在上述代碼中我們使用的是@Around,這個也是很常用的。

@Around("executionCheckLogin()")將切面表達式與通知進行綁定,使用我們的代碼注入在使用@CheckLogin的地方生效
,其中參數(shù)是上面切面的方法名。

而在方法中參數(shù)就是JoinPoint,常用的也就是這個ProceedingJoinPoint。

JoinPoint

public interface JoinPoint {
    String toString();         //連接點所在位置的相關(guān)信息
    String toShortString();     //連接點所在位置的簡短相關(guān)信息
    String toLongString();     //連接點所在位置的全部相關(guān)信息
    Object getThis();         //返回AOP代理對象
    Object getTarget();       //返回目標對象
    Object[] getArgs();       //返回被通知方法參數(shù)列表
    Signature getSignature();  //返回當前連接點簽名
    SourceLocation getSourceLocation();//返回連接點方法所在類文件中的位置
    String getKind();        //連接點類型
    StaticPart getStaticPart(); //返回連接點靜態(tài)部分
}

ProceedingJoinPoint

ProceedingJoinPoint繼承了JoinPoint

public interface ProceedingJoinPoint extends JoinPoint {
    public Object proceed() throws Throwable;
    public Object proceed(Object[] args) throws Throwable;
}

使用proceed()方法來執(zhí)行目標方法,即是被@CheckLogin注解的方法,我們再來看看我們的方法

@Around("executionCheckLogin()")
    public Object checkLogin(ProceedingJoinPoint joinPoint) throws Throwable {
        Log.i(TAG, "checkLogin: ");
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        CheckLogin checkLogin = signature.getMethod().getAnnotation(CheckLogin.class);
        if (checkLogin != null) {
            Context context = (Context) joinPoint.getThis();
            if (BaseApplication.isLogin) {
                Log.i(TAG, "checkLogin: 登錄成功 ");
                return joinPoint.proceed();
            } else {
                Log.i(TAG, "checkLogin: 請登錄");
                Toast.makeText(context, "請登錄", Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(context, LoginActivity.class);
                context.startActivity(intent);
                return null;
            }
        }
        return joinPoint.proceed();
    }

  1. 先獲取一個方法前面對象MethodSignature,這個對象有兩個方法:
public interface MethodSignature extends CodeSignature {
    Class getReturnType();      /* name is consistent with reflection API */
    Method getMethod();
}

一個是獲取目標方法的返回類型,一個是目標方法的Methond對象。
然后通過:

signature.getMethod().getAnnotation(CheckLogin.class);

就可以獲取目標方法的注解,如果注解實例不為空,說明加了CheckLogin注解。

Context context = (Context) joinPoint.getThis();

通過上述方法,可以獲取目標方法所在類的對象,但是這里強轉(zhuǎn)成了Context,也就是說,改注解只能在有上下文的類里使用。
然后通過登錄的標志進行判斷,是讓目標方法繼續(xù)執(zhí)行,還是跳轉(zhuǎn)至登錄。

簡單測試

private Button btnAop;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    btnAop = (Button) findViewById(R.id.btn_aop);
    btnAop.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
             onAop();
        }
    });
}

@CheckLogin
public void onAop(){
    Log.d("tag","執(zhí)行方法參數(shù)");
}
  1. 設(shè)置登錄標志為未登錄:
I/CheckLogin: checkLogin:
I/CheckLogin: checkLogin: 請登錄

檢測出未登錄,跳轉(zhuǎn)到了登錄界面

  1. 設(shè)置登錄標志為已登錄:
I/CheckLogin: checkLogin:
I/CheckLogin: checkLogin: 登錄成功
D/tag: 執(zhí)行方法參數(shù)

檢測出已登錄,執(zhí)行目標方法。

總結(jié)

AOP的使用不光在檢測登錄,還有其他的一些用處:

  • 打印日志,在需要打印日志的地方加上這樣的方式,就可以打印日志,是不是比寫一個打印方法簡單多了
  • 緩存,假設(shè)目標方法是個數(shù)據(jù)請求,那么是不是可以在目標方法執(zhí)行之后,進行緩存
  • 數(shù)據(jù)校驗,我們的代碼中很多地方都會去校驗數(shù)據(jù),那么自定義一個AOP,然后傳入你需要注解的對象進行校驗。

這樣的方式應(yīng)該還有很多,只是現(xiàn)在還沒有用到,希望大家可以多多提出自己的想法。

查看項目,請戳這里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 來源:知乎 欲眼熊貓 面向切面編程(AOP是Aspect Oriented Program的首字母縮寫) ,我...
    wenld_閱讀 3,548評論 4 13
  • Android 中的 AOP 編程 原文鏈接 : Aspect Oriented Programming in A...
    mao眼閱讀 18,590評論 19 82
  • 引言 之前有個做Java Web的師兄就跟我提過,我一直以為這是Java Web的特產(chǎn),也就是一個叫做Spring...
    Android開發(fā)哥閱讀 1,645評論 1 7
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,699評論 19 139
  • AOP實現(xiàn)可分為兩類(按AOP框架修改源代碼的時機): 靜態(tài)AOP實現(xiàn):AOP框架在編譯階段對程序進行修改,即實現(xiàn)...
    數(shù)獨題閱讀 2,408評論 0 22

友情鏈接更多精彩內(nèi)容