AOP编程之使用aspectj对app中按钮进行全局处理

由于简书近期对平台内容进行审核,导致之前的这篇文章无法访问,所以重新发一次

最近项目进入紧锣密鼓测试阶段,昨天测试提了一个issue,app中按钮都没有做快速点击校验。

这就涉及到aop面向切面编程了!后端开发Spring对aop应该很熟悉,android开发中可能用到aop的情况没有后端那么多,但是aop对android开发也是至关重要的!

哪些情况用到aop?

  • 比如针对某一功能进行埋点
  • 全局日志处理
  • 全局异常处理
  • 全局动画处理等

java aop大致有三种方式

1.jdk动态代理
2.cglib动态代理
3.aspectj

. jdk动态代理 cglib aspectj
作用对象的限制 只能操作实现了接口的类 不能操作被final修饰的类,因为cglib是针对类实现代理,主要是对指定的类生成一个子类,覆盖其中的方法来实现代理. 貌似没什么限制
基本原理 利用拦截器(拦截器必须实现InvocationHanlder)加上反射机制生成一个实现代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理。 利用ASM开源包,对代理对象类的class文件加载进来,通过修改其字节码生成子类来处理。 采用基于jvm的ajc(编译器)和weaver(织入器),在class字节码中织入aspectj的代码

项目中我使用的是https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx 它是在aspectj基础上做了些修改,支持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 用来对点击事件相关的拦截处理

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的同学会看的一头雾水

首先说pointcut
@Pointcut相当于你要拦截的某些执行点或者调用点,pointcut可以有call,execution,target,this,within,withincode等等操作符,这些操作符可以结合java的||,&&,!使用

call捕获的joinpoint是签名方法的调用点,而execution捕获的则是执行点。
call和execution的语法

within()的参数是一个类,比如我们可以通过within(A.class)或者!within(A.class)来过滤想要拦截的点
withincode()和within()相似,只不过withincode()接收的参数是方法的signature

target()判断目标对象是否是某种类型,this()判断当前执行对象是否是某种类型

call和execution的语法结构:
execution/call([注解] [修饰符] 返回值类型 [类型声明]方法名(参数列表)[ 异常列表]),被[]括住的是非必须项.

举例:

execution (* com.mjt..*.*(..))
 1、execution(): 表达式主体。

 2、第一个*号:表示返回类型,*号表示所有的类型。

 3、包名:表示需要拦截的包名,后面的两个句点表示当前包和当前包的所有子包,com.mjtl包、子孙包下所有类的方法。

 4、第二个*号:表示类名,*号表示所有的类。

 5、*(..):最后这个星号表示方法名,*号表示所有的方法,后面括弧里面表示方法的参数,两个句点表示任何参数。

@Before在拦截点或者调用点之前调用
@After是在拦截点或者调用点之后调用
被@Around注解的方法,会被织入到拦截方法调用点或这行点之前,

接着我运行项目,编译通过后,快速的点击了一个按钮
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

执行了,before方法,然后进入被@Around修饰的方法,看到第一次点击判断不是快速点击
放过拦截,proceedingJoinPoint.proceed();原方法得到执行!
第二次点击,判断是快速点击,proceedingJoinPoint.proceed()没有执行,也就是原方法被拦截掉了!
但是这时候我发现如果点击事件是使用lambda表达式是无法拦截的,因为这里的pointcut的execution是这样

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

    }

这个正则的大致意思是,拦截返回类型为void, android.view.View.OnClickListener.onClick()方法,参数(..)表示参数可以是任意数量任意类型

那么接着写pointcut拦截lambda表达式的点击事件
于是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对点击事件采用lambda表达式的拦截
然后我快速的点击了一个采用lambda表达式方式实现的点击事件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方式实现的点击事件也被拦截到了


接下来,如果某个小伙伴或我这个按钮不要拦截快速点击,那怎么办呢?

嗯,采用自定义注解,如果某个onClick方法请求放过快速点击拦截,加上这个注解就好了

接着我们写一个自定义注解就叫Ignore
作用于CLASS,修饰的目标为方法和构造函数

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方法进行了修改

 @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修饰的点击事件,快速点击两下

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))

可以看到,是快速点击,但是原方法也得到了执行

嗯 对项目的全局处理点击事件大致就是这样,aspectj相关的东西有很多

关于aspectj的具体使用请查看 aspectj官方文档
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,076评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,658评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,732评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,493评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,591评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,598评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,601评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,348评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,797评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,114评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,278评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,953评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,585评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,202评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,180评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,139评论 2 352

推荐阅读更多精彩内容