Android AOP编程——AspectJ语法&实战
Posted yubo_725
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android AOP编程——AspectJ语法&实战相关的知识,希望对你有一定的参考价值。
在上一篇Android AOP编程(一)——AspectJ基础知识中我记录了在android中使用AspectJ实现AOP编程的一些基础知识,但是AspectJ的使用其实最主要的是针对切面的语法,找切面并不难,难的是如何编写匹配这个切面的规则,本篇主要记录的就是AspectJ的语法,以及使用一个实例来解释AspectJ的应用。
AspectJ语法整理
以下关于AspectJ的语法整理全部出自网络收集,并未一一验证,若有错误请指出。
execution
使用execution(<匹配表达式>)方法执行
匹配模式 | 描述 |
---|---|
* public * *(..) | 任何公共方法的执行 |
* cn.javass..IPointcutService.*() | cn.javass包及所有子包下IPointcutService接口中的任何无参方法 |
* cn.javass..*.*(..) | cn.javass包及所有子包下任何类的任何方法 |
* cn.javass..IPointcutService.*(*) | cn.javass包及所有子包下IPointcutService接口的任何只有一个参数方法 |
* (!cn.javass..IPointcutService+).*(..) | 非“cn.javass包及所有子包下IPointcutService接口及子类型”的任何方法 |
* cn.javass..IPointcutService+.*() | cn.javass包及所有子包下IPointcutService接口及子类型的的任何无参方法 |
* cn.javass..IPointcut*.test*(java.util.Date) | cn.javass包及所有子包下IPointcut前缀类型的的以test开头的只有一个参数类型为java.util.Date的方法,注意该匹配是根据方法签名的参数类型进行匹配的,而不是根据执行时传入的参数类型决定的如定义方法:public void test(Object obj);即使执行时传入java.util.Date,也不会匹配的; |
* cn.javass..IPointcut*.test*(..) throws IllegalArgumentException, ArrayIndexOutOfBoundsException | cn.javass包及所有子包下IPointcut前缀类型的的任何方法,且抛出IllegalArgumentException和ArrayIndexOutOfBoundsException异常 |
* (cn.javass..IPointcutService+ && java.io.Serializable+).*(..) | 任何实现了cn.javass包及所有子包下IPointcutService接口和java.io.Serializable接口的类型的任何方法 |
@java.lang.Deprecated * *(..) | 任何持有@java.lang.Deprecated注解的方法 |
@java.lang.Deprecated @cn.javass..Secure * *(..) | 任何持有@java.lang.Deprecated和@cn.javass…Secure注解的方法 |
@(java.lang.Deprecated || cn.javass..Secure) * *(..) | 任何持有@java.lang.Deprecated或@ cn.javass…Secure注解的方法 |
(@cn.javass..Secure *) *(..) | 任何返回值类型持有@cn.javass…Secure的方法 |
* (@cn.javass..Secure *).*(..) | 任何定义方法的类型持有@cn.javass…Secure的方法 |
* *(@cn.javass..Secure (*) , @cn.javass..Secure (*)) | 任何签名带有两个参数的方法,且这个两个参数都被@ Secure标记了,如public void test(@Secure String str1, @Secure String str1); |
* *((@ cn.javass..Secure *))或* *(@ cn.javass..Secure *) | 任何带有一个参数的方法,且该参数类型持有@cn.javass…Secure;如public void test(Model model);且Model类上持有@Secure注解 |
* *(@cn.javass..Secure (@cn.javass..Secure *) ,@ cn.javass..Secure (@cn.javass..Secure *)) | 任何带有两个参数的方法,且这两个参数都被@ cn.javass…Secure标记了;且这两个参数的类型上都持有@ cn.javass…Secure; |
* *(java.util.Map<cn.javass..Model, cn.javass..Model>, ..) | 任何带有一个java.util.Map参数的方法,且该参数类型是以< cn.javass…Model, cn.javass…Model >为泛型参数;注意只匹配第一个参数为java.util.Map,不包括子类型;如public void test(HashMap<Model, Model> map, String str);将不匹配,必须使用“* *(java.util.HashMap<cn.javass…Model,cn.javass…Model>, …)”进行匹配;而public void test(Map map, int i);也将不匹配,因为泛型参数不匹配 |
* *(java.util.Collection<@cn.javass..Secure *>) | 任何带有一个参数(类型为java.util.Collection)的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.javass…Secure注解;如public void test(Collection collection);Model类型上持有@cn.javass…Secure |
* *(java.util.Set<? extends HashMap>) | 任何带有一个参数的方法,且传入的参数类型是有一个泛型参数,该泛型参数类型继承与HashMap; |
* *(java.util.List<? super HashMap>) | 任何带有一个参数的方法,且传入的参数类型是有一个泛型参数,该泛型参数类型是HashMap的基类型;如public voi test(Map map); |
* *(*<@cn.javass..Secure *>) | 任何带有一个参数的方法,且该参数类型是有一个泛型参数,该泛型参数类型上持有@cn.javass…Secure注解; |
within
使用within(<匹配表达式>)方法执行
匹配模式 | 描述 |
---|---|
within(cn.javass..*) | cn.javass包及子包下的任何方法执行 |
within(cn.javass..IPointcutService+) | cn.javass包或所有子包下IPointcutService类型及子类型的任何方法 |
within(@cn.javass..Secure *) | 持有cn.javass…Secure注解的任何类型的任何方法必须是在目标对象上声明这个注解,在接口上声明的对它不起作用 |
this
使用“this(类型全限定名)”匹配当前AOP代理对象类型的执行方法;注意是AOP代理对象的类型匹配,这样就可能包括引入接口方法也可以匹配;注意this中使用的表达式必须是类型全限定名,不支持通配符;
匹配模式 | 描述 |
---|---|
this(cn.javass.spring.chapter6.service.IPointcutService) | 当前AOP对象实现了 IPointcutService接口的任何方法 |
this(cn.javass.spring.chapter6.service.IIntroductionService) | 当前AOP对象实现了 IIntroductionService接口的任何方法也可能是引入接口 |
target
使用“target(类型全限定名)”匹配当前目标对象类型的执行方法;注意是目标对象的类型匹配,这样就不包括引入接口也类型匹配;注意target中使用的表达式必须是类型全限定名,不支持通配符;
匹配模式 | 描述 |
---|---|
target(cn.javass.spring.chapter6.service.IPointcutService) | 当前目标对象(非AOP对象)实现了 IPointcutService接口的任何方法 |
target(cn.javass.spring.chapter6.service.IIntroductionService) | 当前目标对象(非AOP对象) 实现了IIntroductionService 接口的任何方法不可能是引入接口 |
args
使用“args(参数类型列表)”匹配当前执行的方法传入的参数为指定类型的执行方法;注意是匹配传入的参数类型,不是匹配方法签名的参数类型;参数类型列表中的参数必须是类型全限定名,通配符不支持;args属于动态切入点,这种切入点开销非常大,非特殊情况最好不要使用;
匹配模式 | 描述 |
---|---|
args (java.io.Serializable,..) | 任何一个以接受“传入参数类型为 java.io.Serializable” 开头,且其后可跟任意个任意类型的参数的方法执行,args指定的参数类型是在运行时动态匹配的 |
@within
使用“@within(注解类型)”匹配所以持有指定注解类型内的方法;注解类型也必须是全限定类型名;
匹配模式 | 描述 |
---|---|
@within cn.javass.spring.chapter6.Secure) | 任何目标对象对应的类型持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用 |
@target
使用“@target(注解类型)”匹配当前目标对象类型的执行方法,其中目标对象持有指定的注解;注解类型也必须是全限定类型名;
匹配模式 | 描述 |
---|---|
@target (cn.javass.spring.chapter6.Secure) | 任何目标对象持有Secure注解的类方法;必须是在目标对象上声明这个注解,在接口上声明的对它不起作用 |
@args
使用“@args(注解列表)”匹配当前执行的方法传入的参数持有指定注解的执行;注解类型也必须是全限定类型名;
匹配模式 | 描述 |
---|---|
@args (cn.javass.spring.chapter6.Secure) | 任何一个只接受一个参数的方法,且方法运行时传入的参数持有注解 cn.javass.spring.chapter6.Secure;动态切入点,类似于arg指示符; |
@annotation
使用“@annotation(注解类型)”匹配当前执行方法持有指定注解的方法;注解类型也必须是全限定类型名;
匹配模式 | 描述 |
---|---|
@annotation(cn.javass.spring.chapter6.Secure ) | 当前执行方法上持有注解 cn.javass.spring.chapter6.Secure将被匹配 |
AspectJ在Android中的应用
处理Android快速点击时重复触发点击事件问题
Android中我们经常有这样的场景:点击按钮从当前页面A跳转到另一个页面B,如果点击速度特别快,会发现可能页面B被打开了多个,比如下面的代码在MainActivity页面中央,通过点击按钮打开OtherActivity:
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, OtherActivity.class));
}
});
}
}
如果你在真机上测试,会发现快速点击按钮时,可能打开OtherActivity两次甚至三次,为了处理这种快速点击导致的问题,一般会这么做:
编写一个工具类,提供一个方法用于判断当前点击时间和上次点击时间的差,如果时间差比较短(比如500毫秒),认为是快速点击,则不处理这次点击事件,比如下面的代码:
public class ClickUtil {
private static long lastClickTime = 0L;
// 是否是快速点击
public static boolean isFastClick() {
long curTime = System.currentTimeMillis();
if (curTime - lastClickTime >= 500L) {
lastClickTime = curTime;
return true;
}
return false;
}
}
在点击事件相关的逻辑中调用上面的代码:
if (!ClickUtil.isFastClick()) {
startActivity(new Intent(MainActivity.this, OtherActivity.class));
}
这种方式可以防止快速点击,但是有一些问题:
- 代码具有侵入性,需要在原有的代码逻辑上做修改
- 比较复杂,如果有很多地方都需要处理点击事件过快,则需要加很多代码
我们可以使用AspectJ面向切面编程来处理这种问题,比如编写如下切入点代码:
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;
@Aspect
public class MethodAspect {
// 点击事件触发的时间间隔为600毫秒,即600毫秒内只允许一次点击
private static final long CLICK_INTERVAL = 600L;
// 上次点击的时间
private static long lastClickTime = 0L;
// 是否允许点击事件
private boolean isClickEnabled() {
long curTime = System.currentTimeMillis();
if (curTime - lastClickTime > CLICK_INTERVAL) {
lastClickTime = curTime;
return true;
}
return false;
}
// 该方法会匹配android.view包及所有子包下的OnClickListener接口中的onClick方法,且方法参数为android.view.View
// 注意使用的@Around而不是@Before,且方法参数为ProceedingJoinPoint
@Around("execution(* android.view..OnClickListener.onClick(android.view.View))")
public void clickableDetect(ProceedingJoinPoint joinPoint) {
if (isClickEnabled()) {
// 如果点击事件被允许,则执行原来的逻辑
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
}
在MainActivity中不用做任何更改,上面的切入点代码会自动匹配按钮的点击事件,并修改编译后的字节码,在原有的点击逻辑周围加入点击是否被允许的逻辑判断,我们可以在app/build/intermediates/javac/debug/classed目录下查看MainActivity.class反编译后的代码,代码如下:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
public MainActivity() {
}
protected void onCreate(Bundle savedInstanceState) {
MethodAspect.aspectOf().beforeOnCreate();
super.onCreate(savedInstanceState);
this.setContentView(2131427356);
this.findViewById(2131230807).setOnClickListener(new OnClickListener() {
public void onClick(View v) {
JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, v);
onClick_aroundBody1$advice(this, v, var3, MethodAspect.aspectOf(), (ProceedingJoinPoint)var3);
}
static {
ajc$preClinit();
}
});
}
}
可以看到onClick方法体中已经多了一些被AspectJ处理过的逻辑,在真机上测试快速点击,已经不会出现多次触发点击事件的问题了。
这种使用AspectJ AOP编程处理快速点击多次触发的问题较上面直接编码的优势在于:代码无侵入性,不需要修改原来的点击事件逻辑,通过AspectJ框架可自动在点击事件的逻辑前后插入代码,完成点击是否过快的逻辑判断,但是这种方式完美吗?
并不完美!
我们知道在Android中处理点击事件的方式有很多种:
- 可以直接给某个View设置setOnClickListener然后传入匿名的View.OnClickListener
- 可以在类上实现View.OnClickListener接口然后设置某个View的点击事件为this
- 可以在类中定义一个成员变量listener,其类型为View.OnClickListener,然后指定某个View的点击事件为listener
- …
还有很多其他方式能为View添加点击事件,另外,不是所有的View的点击事件内部都是做的页面跳转,某些点击事件可能就算重复触发多次也没有什么问题,如果统一用上面AspectJ匹配onClick方法来处理,可能会有误伤;另外,如果某个项目中有很多onClick相关的点击事件方法,使用AspectJ去匹配并修改这些方法,将会给编译项目增加很大负担(编译时间会变长),所以这种方式并不完美,下面用另一种基于AspectJ的方法来更优雅的处理点击事件重复触发多次的问题,用到了自定义注解。
首先我们定义如下注解:
// 作用在方法上
@Target(ElementType.METHOD)
// 注解保留到字节码阶段
@Retention(RetentionPolicy.CLASS)
public @interface ClickOnce {
}
我们要实现的功能是,使用@ClickOnce
注解的方法,在600毫秒内只触发一次;
然后编写切入点方法如下代码:
@Aspect
public class MethodAspect {
// 点击事件触发的时间间隔为600毫秒,即600毫秒内只允许一次点击
private static final long CLICK_INTERVAL = 600L;
// 上次点击的时间
private static long lastClickTime = 0L;
// 是否允许点击事件
private boolean isClickEnabled() {
long curTime = System.currentTimeMillis();
if (curTime - lastClickTime > CLICK_INTERVAL) {
lastClickTime = curTime;
return true;
}
return false;
}
// 匹配的是使用@ClickOnce注解的任意方法
@Around("execution(@ClickOnce * *(..))")
public void clickOnce(ProceedingJoinPoint joinPoint) {
if (isClickEnabled()) {
try {
joinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
}
下面测试一下@ClickOnce
注解能否正常工作,在MainActivity中增加两个按钮,分别用两种不同的方式为这两个按钮添加点击事件,点击还是跳转到OtherActivity:
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
// 直接设置点击事件处理器
findViewById(R.id.btn2).setOnClickListener(new View.OnClickListener() {
@Override
@ClickOnce
public void onClick(View v) {
startActivity(new Intent(MainActivity.this, OtherActivity.class));
}
});
// 使用类上实现View.OnClickListener接口这种方式为按钮绑定点击事件处理器
findViewById(R.id.btn3).setOnClickListener(this);
}
@ClickOnce
private void toOtherActivity() {
startActivity(new Intent(MainActivity.this, OtherActivity.class));
}
@Override
public void onClick(View v) {
if (v.getId() == R.id.btn3) {
toOtherActivity();
}
}
}
然后我们编译项目,再次查看生成的MainActivity.class文件反编译后的代码:
public class MainActivity extends AppCompatActivity implements OnClickListener {
public MainActivity() {
}
protected void onCreate(Bundle savedInstanceState) {
MethodAspect.aspectOf().beforeOnCreate();
super.onCreate(savedInstanceState);
this.setContentView(2131427356);
this.findViewById(2131230808).setOnClickListener(new OnClickListener() {
@ClickOnce
public void onClick(View v) {
JoinPoint var3 = Factory.makeJP(ajc$tjp_0, this, this, v);
onClick_aroundBody1$advice(this, v, var3, MethodAspect.aspectOf(), (ProceedingJoinPoint)var3);
}
static {
ajc$preClinit();
}
});
this.findViewById(2131230809).setOnClickListener(this);
}
@ClickOnce
private void toOtherActivity() {
JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
toOtherActivity_aroundBody1$advice(this, var1, MethodAspect.aspectOf(), (ProceedingJoinPoint)var1);
}
public void onClick(View v) {
if (v.getId() == 2131230809) {
this.toOtherActivity();
}
}
static {
ajc$preClinit();
}
}
可以看到被@ClickOnce
注解的方法体内都被AspectJ插入的新的代码,我们将项目运行到真机上,测试可以看到快速点击重复触发点击事件的问题没有了,证明以上代码能正常工作。
这种使用自定义注解的方式,相较直接用AspectJ匹配onClick方法的方式,又更进了一步,不仅不会侵入原有代码逻辑,而且更灵活:需要防止重复点击时就使用自定义注解,否则就不用。
总结
AspectJ是一个Java AOP框架,它可以作用于Java编译后的字节码文件,从而实现某些功能,跟面向对象OOP编程相比,AOP在处理某些切面问题时更灵活且优雅,但是AspectJ的使用一定要小心,针对切面的匹配规则一定要详细测试,不当的匹配规则可能会导致代码编译时间变长,且可能处理了我们并不需要处理的逻辑从而导致某些错误。
参考
https://blog.csdn.net/sunlihuo/article/details/52701548
源码
本篇的源码可以在这里下载:https://github.com/yubo725/android-aspectj-demo/tree/v0.2
以上是关于Android AOP编程——AspectJ语法&实战的主要内容,如果未能解决你的问题,请参考以下文章