Android中的AOP

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

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

AOP

AOPAspect Oriented Program的首字母縮寫AOP,其意是面向切面編程),其實(shí)很多前端的開發(fā)可能都沒有聽說過這個(gè),但是對(duì)于
后端的小伙伴來說這個(gè)是在是太熟悉了,因?yàn)楹芏鄷r(shí)候他們就靠這個(gè)來進(jìn)行Log的打印。

那么AOP到底是什么呢?

AOP定義

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

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

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

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

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

加入AspectJ

AspectJ AspectJ實(shí)際上是對(duì)AOP編程思想的一個(gè)實(shí)現(xiàn)。

  • 在項(xiàng)目的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. 導(dǎo)入第三方包

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

使用ajc會(huì)對(duì)所有受 aspect 影響的類進(jìn)行織入,這樣才能使我們的Aspect

//獲取 log實(shí)例
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;
    }

    //編譯時(shí)做如下處理
    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的準(zhǔn)備工作做好了,那么接下來就是使用了

在Android中使用AOP

先來介紹幾個(gè)概念:

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

創(chuàng)建@CheckLogin注解

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


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

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

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

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

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

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

創(chuàng)建Aspect類

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

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

    /**
     * 找到處理的切點(diǎn)
     * * *(..)  可以處理CheckLogin這個(gè)類所有的方法
     */
    @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: 請(qǐng)登錄");
                Toast.makeText(context, "請(qǐng)登錄", Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(context, LoginActivity.class);
                context.startActivity(intent);
                return null;
            }
        }
        return joinPoint.proceed();
    }

@Pointcut說明

在上方代碼Pointcut之后緊跟了一個(gè)execution的表達(dá)式,這個(gè)就代表切入點(diǎn)的位置,也就是我們上述的何處

解釋一下execution的用法:

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

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

這里重點(diǎn)解釋一下execution,因?yàn)樵谖覀兊娜粘J褂弥校?code>execution是最多的。

類型匹配語法

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

execution表達(dá)式

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

  • modifiers-pattern:修飾符匹配,如publicprivateprotect,可選。
  • 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ù):(..),任意個(gè)參數(shù)

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

@Advice說明

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

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

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

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

JoinPoint

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

ProceedingJoinPoint

ProceedingJoinPoint繼承了JoinPoint

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

使用proceed()方法來執(zhí)行目標(biāo)方法,即是被@CheckLogin注解的方法,我們?cè)賮砜纯次覀兊姆椒?/p>

@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: 請(qǐng)登錄");
                Toast.makeText(context, "請(qǐng)登錄", Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(context, LoginActivity.class);
                context.startActivity(intent);
                return null;
            }
        }
        return joinPoint.proceed();
    }

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

一個(gè)是獲取目標(biāo)方法的返回類型,一個(gè)是目標(biāo)方法的Methond對(duì)象。
然后通過:

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

就可以獲取目標(biāo)方法的注解,如果注解實(shí)例不為空,說明加了CheckLogin注解。

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

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

簡(jiǎn)單測(cè)試

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è)置登錄標(biāo)志為未登錄:
I/CheckLogin: checkLogin:
I/CheckLogin: checkLogin: 請(qǐng)登錄

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

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

檢測(cè)出已登錄,執(zhí)行目標(biāo)方法。

總結(jié)

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

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

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

查看項(xiàng)目,請(qǐng)戳這里

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

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