AOP編程之使用aspectj對app中按鈕進(jìn)行全局處理

由于簡書近期對平臺內(nèi)容進(jìn)行審核,導(dǎo)致之前的這篇文章無法訪問,所以重新發(fā)一次

最近項目進(jìn)入緊鑼密鼓測試階段,昨天測試提了一個issue,app中按鈕都沒有做快速點(diǎn)擊校驗挫望。

這就涉及到aop面向切面編程了!后端開發(fā)Spring對aop應(yīng)該很熟悉,android開發(fā)中可能用到aop的情況沒有后端那么多,但是aop對android開發(fā)也是至關(guān)重要的!

哪些情況用到aop?

  • 比如針對某一功能進(jìn)行埋點(diǎn)
  • 全局日志處理
  • 全局異常處理
  • 全局動畫處理等

java aop大致有三種方式

1.jdk動態(tài)代理
2.cglib動態(tài)代理
3.aspectj

. jdk動態(tài)代理 cglib aspectj
作用對象的限制 只能操作實現(xiàn)了接口的類 不能操作被final修飾的類,因為cglib是針對類實現(xiàn)代理,主要是對指定的類生成一個子類白华,覆蓋其中的方法來實現(xiàn)代理. 貌似沒什么限制
基本原理 利用攔截器(攔截器必須實現(xiàn)InvocationHanlder)加上反射機(jī)制生成一個實現(xiàn)代理接口的匿名類走越,在調(diào)用具體方法前調(diào)用InvokeHandler來處理椭豫。 利用ASM開源包,對代理對象類的class文件加載進(jìn)來旨指,通過修改其字節(jié)碼生成子類來處理捻悯。 采用基于jvm的ajc(編譯器)和weaver(織入器),在class字節(jié)碼中織入aspectj的代碼

項目中我使用的是https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx 它是在aspectj基礎(chǔ)上做了些修改,支持AS的instant run,集成比aspectj更加方便

首先在project下的gradle文件中加入aspectjx插件

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        maven { url 'https://maven.aliyun.com/repository/jcenter/' }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
        maven { url 'https://dl.bintray.com/umsdk/release' }
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.1'
        classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-beta02"
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        maven { url 'https://maven.aliyun.com/repository/jcenter/' }
        maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
        google()
        jcenter()
        maven { url 'https://jitpack.io' }
        maven { url 'https://dl.bintray.com/umsdk/release' }
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}
ext {
    compileSdkVersion = 28
    minSdkVersion = 17
    targetSdkVersion = 27
    versionCode=5
    versionName="1.4.6"
    testRunner="1.1.1"
    espresso="3.1.1"
    junit="4.12"
    appcompat="1.1.0-alpha01"
    supportLibVersion = "28.0.0"
}

module下的gradle文件添加

apply plugin: 'android-aspectjx'

接下來就可以開始編寫被@AspectJ 修飾的切面類了

AspectHandler.java 用來對點(diǎn)擊事件相關(guān)的攔截處理

package com.mjt.pad.common.aspect;

import android.util.Log;

import com.mjt.common.utils.UIUtils;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

/**
 * Copyright:mjt_pad_android
 * Author: liyang <br>
 * Date:2019-05-05 15:41<br>
 * Desc: <br>
 */

@Aspect
public class AspectHandler {
    private static final String TAG = AspectHandler.class.getSimpleName();


    @Before("execution(void android.view.View.OnClickListener.onClick(..))")
    public void beforePoint(JoinPoint joinPoint) {
        Log.e(TAG, "before: " + joinPoint);
    }

  

    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void dealWithNormal() {

    }

    @Around("dealWithNormal()")
    public void onViewClicked(ProceedingJoinPoint proceedingJoinPoint) {
        boolean isFastClickPassed = !UIUtils.isFastClickOnlyInAspect();
        Log.e(TAG, "onViewClicked: 捕獲到了,isFastClick=" + !isFastClickPassed);
        if (isFastClickPassed) {
            Log.e(TAG, "onViewClicked: " + proceedingJoinPoint);
            try {
                proceedingJoinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                Log.e(TAG, "onViewClicked: ", throwable);
            }
        }
    }

}

簡單介紹下這個類里面的@Before @Pointcut @Around幾個注解吧,不然完全沒接觸過aspectj的同學(xué)會看的一頭霧水

首先說pointcut
@Pointcut相當(dāng)于你要攔截的某些執(zhí)行點(diǎn)或者調(diào)用點(diǎn),pointcut可以有call,execution,target,this,within,withincode等等操作符,這些操作符可以結(jié)合java的||,&&,!使用

call捕獲的joinpoint是簽名方法的調(diào)用點(diǎn),而execution捕獲的則是執(zhí)行點(diǎn)淤毛。
call和execution的語法

within()的參數(shù)是一個類,比如我們可以通過within(A.class)或者!within(A.class)來過濾想要攔截的點(diǎn)
withincode()和within()相似,只不過withincode()接收的參數(shù)是方法的signature

target()判斷目標(biāo)對象是否是某種類型,this()判斷當(dāng)前執(zhí)行對象是否是某種類型

call和execution的語法結(jié)構(gòu):
execution/call([注解] [修飾符] 返回值類型 [類型聲明]方法名(參數(shù)列表)[ 異常列表]),被[]括住的是非必須項.

舉例:

execution (* com.mjt..*.*(..))
 1今缚、execution(): 表達(dá)式主體。

 2低淡、第一個*號:表示返回類型姓言,*號表示所有的類型。

 3蔗蹋、包名:表示需要攔截的包名何荚,后面的兩個句點(diǎn)表示當(dāng)前包和當(dāng)前包的所有子包,com.mjtl包猪杭、子孫包下所有類的方法餐塘。

 4、第二個*號:表示類名皂吮,*號表示所有的類戒傻。

 5税手、*(..):最后這個星號表示方法名,*號表示所有的方法需纳,后面括弧里面表示方法的參數(shù)芦倒,兩個句點(diǎn)表示任何參數(shù)。

@Before在攔截點(diǎn)或者調(diào)用點(diǎn)之前調(diào)用
@After是在攔截點(diǎn)或者調(diào)用點(diǎn)之后調(diào)用
被@Around注解的方法,會被織入到攔截方法調(diào)用點(diǎn)或這行點(diǎn)之前,

接著我運(yùn)行項目,編譯通過后,快速的點(diǎn)擊了一個按鈕
log日志顯示

2019-05-08 11:43:41.717 14103-14103/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.adapter.ProductAdapter.1.onClick(View))
2019-05-08 11:43:41.718 14103-14103/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=false
2019-05-08 11:43:41.718 14103-14103/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.adapter.ProductAdapter.1.onClick(View))
2019-05-08 11:43:41.883 14103-14103/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.adapter.ProductAdapter.1.onClick(View))
2019-05-08 11:43:41.883 14103-14103/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=true

執(zhí)行了,before方法,然后進(jìn)入被@Around修飾的方法,看到第一次點(diǎn)擊判斷不是快速點(diǎn)擊
放過攔截,proceedingJoinPoint.proceed();原方法得到執(zhí)行!
第二次點(diǎn)擊,判斷是快速點(diǎn)擊,proceedingJoinPoint.proceed()沒有執(zhí)行,也就是原方法被攔截掉了!
但是這時候我發(fā)現(xiàn)如果點(diǎn)擊事件是使用lambda表達(dá)式是無法攔截的,因為這里的pointcut的execution是這樣

 @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void dealWithNormal() {

    }

這個正則的大致意思是,攔截返回類型為void, android.view.View.OnClickListener.onClick()方法,參數(shù)(..)表示參數(shù)可以是任意數(shù)量任意類型

那么接著寫pointcut攔截lambda表達(dá)式的點(diǎn)擊事件
于是AspectHandler 切面類被我改成了這樣

package com.mjt.pad.common.aspect;

import android.util.Log;

import com.mjt.common.utils.UIUtils;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

/**
 * Copyright:mjt_pad_android
 * Author: liyang <br>
 * Date:2019-05-05 15:41<br>
 * Desc: <br>
 */

@Aspect
public class AspectHandler {
    private static final String TAG = AspectHandler.class.getSimpleName();


    @Before("dealWithNormal()||dealWithLambda()")
    public void beforePoint(JoinPoint joinPoint) {
        Log.e(TAG, "before: " + joinPoint);
    }

    @Pointcut("execution(void com.mjt..lambda*(android.view.View))")
    public void dealWithLambda() {

    }

    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void dealWithNormal() {

    }

    @Around("dealWithNormal()||dealWithLambda()")
    public void onViewClicked(ProceedingJoinPoint proceedingJoinPoint) {
        boolean isFastClickPassed = !UIUtils.isFastClickOnlyInAspect();
        Log.e(TAG, "onViewClicked: 捕獲到了,isFastClick=" + !isFastClickPassed);
        if (isFastClickPassed) {
            Log.e(TAG, "onViewClicked: " + proceedingJoinPoint);
            try {
                proceedingJoinPoint.proceed();
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                Log.e(TAG, "onViewClicked: ", throwable);
            }
        }
    }

}

增加的pointcut對點(diǎn)擊事件采用lambda表達(dá)式的攔截
然后我快速的點(diǎn)擊了一個采用lambda表達(dá)式方式實現(xiàn)的點(diǎn)擊事件log日志如下

2019-05-08 11:55:05.506 15052-15052/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.fragment.print.PrintManagerFragment.lambda$initViews$1(View))
2019-05-08 11:55:05.506 15052-15052/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=false
2019-05-08 11:55:05.506 15052-15052/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.fragment.print.PrintManagerFragment.lambda$initViews$1(View))
2019-05-08 11:55:05.755 15052-15052/com.mjt.pad.test E/AspectHandler: before: execution(void com.mjt.pad.ui.fragment.print.PrintManagerFragment.lambda$initViews$1(View))
2019-05-08 11:55:05.755 15052-15052/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=true

嗯,lambda方式實現(xiàn)的點(diǎn)擊事件也被攔截到了


接下來,如果某個小伙伴或我這個按鈕不要攔截快速點(diǎn)擊,那怎么辦呢?

嗯,采用自定義注解,如果某個onClick方法請求放過快速點(diǎn)擊攔截,加上這個注解就好了

接著我們寫一個自定義注解就叫Ignore
作用于CLASS,修飾的目標(biāo)為方法和構(gòu)造函數(shù)

package com.mjt.pad.common.aspect;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Copyright:mjt_pad_android
 * Author: liyang <br>
 * Date:2019-05-05 16:35<br>
 * Desc: <br>
 */
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD,ElementType.CONSTRUCTOR})
public @interface Ignore {
}

接著改動AspectHandler切面類

package com.mjt.pad.common.aspect;

import android.util.Log;

import com.mjt.common.utils.UIUtils;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;

/**
 * Copyright:mjt_pad_android
 * Author: liyang <br>
 * Date:2019-05-05 15:41<br>
 * Desc: <br>
 */

@Aspect
public class AspectHandler {
    private static final String TAG = AspectHandler.class.getSimpleName();

    private volatile boolean isIgnored = false;

    @Before("execution(@com.mjt.pad.common.aspect.Ignore void com.mjt..*.onClick(..))")
    public void checkIgnore(JoinPoint joinPoint) {
        isIgnored = true;
        Log.e(TAG, "checkIgnore: " + joinPoint);
    }

    @Pointcut("execution(void com.mjt..lambda*(android.view.View))")
    public void dealWithLambda() {

    }

    @Pointcut("execution(void android.view.View.OnClickListener.onClick(..))")
    public void dealWithNormal() {

    }

    @Around("dealWithNormal()||dealWithLambda()")
    public void onViewClicked(ProceedingJoinPoint proceedingJoinPoint) {
        boolean isFastClickPassed = !UIUtils.isFastClickOnlyInAspect();
        Log.e(TAG, "onViewClicked: 捕獲到了,isFastClick=" + !isFastClickPassed+",isIgnored="+isIgnored);
        if (isIgnored||isFastClickPassed) {
            Log.e(TAG, "onViewClicked: " + proceedingJoinPoint);
            try {
                proceedingJoinPoint.proceed();
                isIgnored=false;
            } catch (Throwable throwable) {
                throwable.printStackTrace();
                Log.e(TAG, "onViewClicked: ", throwable);
            }
        }
    }

}

對@Before方法進(jìn)行了修改

 @Before("execution(@com.mjt.pad.common.aspect.Ignore void com.mjt..*.onClick(..))")
    public void checkIgnore(JoinPoint joinPoint) {
        isIgnored = true;
        Log.e(TAG, "checkIgnore: " + joinPoint);
    }

"execution(@com.mjt.pad.common.aspect.Ignore void com.mjt..*.onClick(..))"匹配的是被我們自定義注解@Ignore修飾的 com.mjt包及其子包下的所有onClick方法

然后我們找一個onClick方法加上@Ignore注解看看起作用沒

    @Ignore
    @Override
    public void onClick(View v) {
        switch (v.getId()){
            case R.id.llyPart:
            ...

接著找到這個被Ignore修飾的點(diǎn)擊事件,快速點(diǎn)擊兩下

log日志如下

2019-05-08 12:19:06.925 16912-16912/com.mjt.pad.test E/AspectHandler: checkIgnore: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))
2019-05-08 12:19:06.925 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=false,isIgnored=true
2019-05-08 12:19:06.926 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))
2019-05-08 12:19:07.086 16912-16912/com.mjt.pad.test E/AspectHandler: checkIgnore: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))
2019-05-08 12:19:07.087 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: 捕獲到了,isFastClick=true,isIgnored=true
2019-05-08 12:19:07.087 16912-16912/com.mjt.pad.test E/AspectHandler: onViewClicked: execution(void com.mjt.pad.ui.fragment.RemarkFragment.onClick(View))

可以看到,是快速點(diǎn)擊,但是原方法也得到了執(zhí)行

嗯 對項目的全局處理點(diǎn)擊事件大致就是這樣,aspectj相關(guān)的東西有很多

關(guān)于aspectj的具體使用請查看 aspectj官方文檔
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末不翩,一起剝皮案震驚了整個濱河市兵扬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌口蝠,老刑警劉巖器钟,帶你破解...
    沈念sama閱讀 221,820評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異妙蔗,居然都是意外死亡傲霸,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,648評論 3 399
  • 文/潘曉璐 我一進(jìn)店門灭必,熙熙樓的掌柜王于貴愁眉苦臉地迎上來狞谱,“玉大人,你說我怎么就攤上這事禁漓「疲” “怎么了?”我有些...
    開封第一講書人閱讀 168,324評論 0 360
  • 文/不壞的土叔 我叫張陵播歼,是天一觀的道長伶跷。 經(jīng)常有香客問我,道長秘狞,這世上最難降的妖魔是什么叭莫? 我笑而不...
    開封第一講書人閱讀 59,714評論 1 297
  • 正文 為了忘掉前任,我火速辦了婚禮烁试,結(jié)果婚禮上雇初,老公的妹妹穿的比我還像新娘。我一直安慰自己减响,他們只是感情好靖诗,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,724評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著支示,像睡著了一般刊橘。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上颂鸿,一...
    開封第一講書人閱讀 52,328評論 1 310
  • 那天促绵,我揣著相機(jī)與錄音,去河邊找鬼。 笑死败晴,一個胖子當(dāng)著我的面吹牛浓冒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播位衩,決...
    沈念sama閱讀 40,897評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼裆蒸,長吁一口氣:“原來是場噩夢啊……” “哼熔萧!你這毒婦竟也來了糖驴?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,804評論 0 276
  • 序言:老撾萬榮一對情侶失蹤佛致,失蹤者是張志新(化名)和其女友劉穎贮缕,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體俺榆,經(jīng)...
    沈念sama閱讀 46,345評論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡感昼,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,431評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了罐脊。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片定嗓。...
    茶點(diǎn)故事閱讀 40,561評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖萍桌,靈堂內(nèi)的尸體忽然破棺而出宵溅,到底是詐尸還是另有隱情,我是刑警寧澤上炎,帶...
    沈念sama閱讀 36,238評論 5 350
  • 正文 年R本政府宣布恃逻,位于F島的核電站,受9級特大地震影響藕施,放射性物質(zhì)發(fā)生泄漏寇损。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,928評論 3 334
  • 文/蒙蒙 一裳食、第九天 我趴在偏房一處隱蔽的房頂上張望矛市。 院中可真熱鬧,春花似錦诲祸、人聲如沸浊吏。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,417評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卿捎。三九已至,卻和暖如春径密,著一層夾襖步出監(jiān)牢的瞬間午阵,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,528評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留底桂,地道東北人植袍。 一個月前我還...
    沈念sama閱讀 48,983評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像籽懦,于是被迫代替她去往敵國和親于个。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,573評論 2 359

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