AOP学习笔记(Android)
文章目录
AOP:面向切面编程,通过预编译与运行期间的动态代理实现对程序功能的统一维护的一种技术,AOP是对于OOP(面向对象编程)的延续,利用AOP可以对业务逻辑的各个部分进行隔离,从而使业务逻辑各个部分之间的耦合度降低,提高程序的可重用性,提高开发效率。
目前主流的AOP实现方式有AspectJ
、JDK动态代理
AspectJ
-
使用Aspect注解切面类
-
在切面类里面指定增强方法
使用的注解有:
@Before(前置增强)
@After(后置增强)
@Around(环绕增强)
@AfterReturning(返回增强)
@AfterThrowing(异常增强)
在Android使用AspectJ需要使用一个插件:https://github.com/HujiangTechnology/gradle_plugin_android_aspectjx
dependencies{ classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.8' }
AspectJX目前存在的问题
该插件目前与Kotlin1.4有兼容问题,需要在build.gradle添加aspectjx闭包添加对versions/9的排除才可正常进行编译。
aspectjx {
exclude 'versions.9'
}
增强注解
@Before(前置增强)
前置增强在目标方法执行前执行。
@Before("execution(* com.skit.androidaop.*.on**(..))")//com.skit.androidaop包下所有类on开头的方法
fun preOnMethodBefore(jp: JoinPoint) {
Log.d(TAG, "onClickBefore: $jp ${jp.target} ${jp.target.javaClass.name}")
}
@After(后置增强)
后置增强在方法目标执行后执行。
@After("execution(* com.skit.androidaop.*.on**(..))")
fun preOnMethodAfter(jp:JoinPoint){
Log.d(TAG, "onClickAfter: $jp ${jp.target}")
}
@AfterReturning(返回增强)
返回增强执行在目标方法执行后,可以在返回增强中获取目标方法的返回值。
@AfterReturning("execution(@com.skit.androidaop.ReturnAfter * *(..))", returning = "i")
fun afterReturing(p: JoinPoint, i: Any) {
Log.d(TAG, "afterReturing: ${p.signature.name}: $i")
}
@AfterThrowing(异常增强)
异常增强发生在未处理的异常发生之后
@AfterThrowing("execution(* com.skit..*.*(..))", throwing = "e")
fun afterThrowing(jp: JoinPoint, e: Exception) {
Log.d(TAG, "afterThrowing: ${e.message}")
}
@Around(环绕增强)
会替换原来的代码,使用此注解的切口方法必须有ProceedingJoinPoint
参数,如果想执行原来的代码得调用ProceedingJoinPoint
对象的proceed
方法。
@Around("execution(@com.skit.androidaop.SingleClick * *(..))")
fun aroundSingleClick(p: ProceedingJoinPoint) {
val currentTimeMillis = System.currentTimeMillis()
if ((currentTimeMillis - time) < 1000) {
Log.d(TAG, "aroundSingleClick: 一秒之内点击")
} else {
p.proceed(p.args)
time = System.currentTimeMillis()
}
}
@Pointcut(命名切入点)
@Pointcut用于定义可重用的切入点
//置顶切入点
@Pointcut("execution(@com.skit.androidaop.AClick * *(..))")
fun preWithOnPointcut(){
}
//上面的切入点运用
@Before("preWithOnPointcut()")
fun before(){
}
@After("preWithOnPointcut()")
fun after(){
}
切点函数
在看切点函数之前得知道通配符
与逻辑运算符
通配符用于匹配制定的规则,逻辑运算符用于多个切入点的组合使用。
通配符
*
通配符,该通配符主要用于匹配单个单词,或者是以某个词为前缀或后缀的单词。..
通配符,该通配符匹配多个元素,在表示类时必须与*
一起使用+
通配符,该通配符表示按照类型匹配指定类的所有类,必须跟在类名后面,如com.skit.bean.Car+
,表示继承该类的所有子类包括本身
逻辑运算符
&&
与操作符,相当于切点的交集运算。||
或操作符,相当于切点的并集运算。!
非操作符,相当于切点的反集运算。
@Before("target(androidx.appcompat.app.AppCompatActivity) && execution(* on*(..))")
fun before(jp: JoinPoint) {
Log.d(TAG, "before: ${jp.sourceLocation.withinType.name} ${jp.signature.name}")
}
如上面表示匹配androidx.appcompat.app.AppCompatActivity
类与它的子类并且方法开头为on
的方法
运行结果下图所示:
call
call和execution的区别在于call是调用切入点的时候,execution是执行切入点的时候,也就是说call是在调用处进行织入增强代码,而对于execution是在连接点方法内部织入增强代码。
execution
execution(
访问修饰符?
方法返回类型
方法所在包全路径?方法(方法参数类型)
异常类型?
(PS:使用包全路径))
execution是最常用的切点函数,execution表达式切面粒度最小可以达到方法级,用来匹配方法。
例子:
execution(public * *(..))
匹配所有类的public方法
execution(* on*(..))
匹配所有类on开头的方法
execution(public java.lang.String *(..))
匹配所有类的访问修饰符为public并且返回类型为String的方法
execution(* com.skit.activity.*(..))
匹配com.skit.activity
包下所有类方法(PS:不包含子包)
execution(* com.skit.activity..*(..))
匹配com.skit.activity
包下所有类与方法(PS:包含子包)
execution(* com.skit..*.*Activity.on*(..))
匹配com.skit
包下以Activity结尾的类的on开头的方法
@annotation(不支持通配符)
@annotation
用于标注了某个注解的目标切点,该函数入参必须是注解并且包名写全。
@annotation(com.skit.androidaop.SingleClick)
表示增强任何标注了SingleClick这个注解的方法。
within
within
表示特定域下的所有方法。
within(com.skit.activity.*)
表示com.skit.activity
包中所有方法。
within(com.skit..*.*Activity.*)
表示com.skit
包中所有方法以Activity结尾的类的所有方法。
within()与execution()的功能类似,两者的区别是,within()定义的连接点的最小范围是类级别的(不是接口),而execution()定义的连接点的最小范围可以精确到方法的入参,因此可以认为execution()涵盖了within()的功能。
@within(不支持通配符)
@within
匹配标注了指定注解的类及其子孙类。
@within(com.skit.AllMethodTime)
匹配所有标注了AllMethodTime的类与其子孙类的所有方法。
target(不支持通配符)
target
表示目标类如果是入参的类型,那么目标类的所有方法都匹配切点。
target(com.skit.activity.BaseActivity)
匹配com.skit.activity.BaseActivity
所有实现类及其子孙类中的所有方法
@target(不支持通配符)
@target
表示目标类如果标注了入参的注解,那么该目标类的所有方法都匹配切点。
@target(com.skit.AllMethodTime)
匹配所有标注com.skit.AllMethodTime
注解的所有类的所有方法。
within与target区别
within(androidx.appcompat.app.AppCompatActivity) && execution(* on*(..))
target(androidx.appcompat.app.AppCompatActivity) && execution(* on*(..))
通过上图可以看出within只有指定的类,而target则包含子类甚至它的父类
用AOP方式实现按钮快速点击拦截
创建注解
@Target(AnnotationTarget.FUNCTION)//使用在方法
@Retention(AnnotationRetention.BINARY)//等同于Java注解的RetentionPolicy.CLASS
annotation class SingleClick {
}
创建增强类
@Aspect
class ClickAspect{
private var time = 0L
//环绕增强
@Around("execution(@com.skit.androidaop.SingleClick * *(..))")
fun aroundSingleClick(p: ProceedingJoinPoint) {
val currentTimeMillis = System.currentTimeMillis()//获取当前时间
if ((currentTimeMillis - time) < 1000) {//点击间隔小于1000毫秒
Log.d(TAG, "aroundSingleClick: 一秒之内点击被拦截")
} else {
p.proceed(p.args)//执行方法
time = System.currentTimeMillis()
}
}
}
用法
override fun onCreate(savedInstanceState: Bundle?) {
...
binding.singleClick.setOnClickListener {
singleClick()
}
...
}
@SingleClick
private fun singleClick() {
Log.d(TAG, "singleClick: ")
}
JDK动态代理
JDK动态代理主要在于实现
InvocationHandler
接口,并在invoke方法进行注入代码
先创建几个类:
IUser接口
public interface IUser {
String userName();
void setName(String name);
void apply();
}
VipUser类
public class VipUser implements IUser {
String name;
@Override
public String userName() {
System.out.println("执行userName");
return name;
}
@Override
public void setName(String name) {
System.out.println("执行setName");
this.name = name;
}
@Override
public void apply() {
System.out.println("执行apply");
}
}
AOPHandler类实现InvocationHandler方法
public class AOPHandler implements InvocationHandler {
VipUser iUser;
public AOPHandler(VipUser iUser) {
this.iUser = iUser;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println(method.getName() + "方法执行前");
Object invoke = method.invoke(iUser, args);
System.out.println(method.getName() + "方法执行后\n\n");
return invoke;
}
public IUser apply() {
Object o = Proxy.newProxyInstance(iUser.getClass().getClassLoader(), new Class[]{IUser.class}, this);
return (IUser) o;
}
}
运行:
public static void main(String[] args) {
IUser user = new AOPHandler(new VipUser()).apply();
user.setName("Shuike");
System.out.println(user.userName() + "\n");
user.apply();
}
Proxy.newProxyInstance方法第二个参数只能传递接口类,每一个注入点需要有一个对应的接口类操作繁杂,同时也没能做到增强代码与业务代码分离,同时JDK动态代理使用JDK的反射机制实现,而AspectJ使用预编译方式进行实现。
AspectJ在编译器生成class文件后会对这个class文件进行插桩实现代码的加入。