AOP是什么东东?
AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统 一维护的一种技术。AOP是OOP的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的 一种衍生范型。
(利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率)
AOP实现方式
可以使用以下4种方式,完成不同阶段的切面编程
APT : 编译期,静态织入
AspectJ:编译期,静态织入
ASM:编译期和运行期,可以动态也可以静态织入
-
Xposed:运行期,动态织入
Aop思想可以说成插桩,在类的编译期间中干一些东西,下图看一个图就明白了,主要关注一下AspectJ插入时机:image.png
1. APT
介绍
APT(Annotation Processing Tool)即注解处理器,是一种处理注解的工具,确切的说它是javac的一个工具,它用来在编译时扫描和处理注解。注解处理器以Java代码(或者编译过的字节码)作为输入,生成.java文件作为输出。简单来说就是在编译期,通过注解生成.java文件。
用过ButterKnife、Dagger、EventBus等注解框架的同学就能感受到,利用这些框架可以少些很多代码,只要写一些注解就可以了。
其实,他们不过是通过注解,生成了一些代码。通过对APT的学习,你就会发现,他们很强~~~
实现
- apt-annotation:自定义注解,存放@BindView
- apt-processor:注解处理器,根据apt-annotation中的注解,在编译期生成xxxActivity_ViewBinding.java代码
-
apt-library:工具类,调用xxxActivity_ViewBinding.java中的方法,实现View的绑定。
关系如图所示
image.png
2. AspectJ
介绍
AspectJ意思就是Java的Aspect,Java的AOP。它其实不是一个新的语言,它在代码的编译期间扫描目标程序,根据切点(PointCut)匹配,将开发者编写的Aspect程序编织(Weave)到目标程序的.class文件中,对目标程序作了重构(重构单位是JoinPoint),目的就是建立目标程序与Aspect程序的连接(获得执行的对象、方法、参数等上下文信息),从而达到AOP的目的,它的核心就是ajc(编译器)\weaver(织入)。
基本概念
ajc(编译器):
基于Java编译器之上的,它是用来编译.aj文件,aspectj在Java编译器的基础上增加了一些它自己的关键字和方法。因此,ajc也可以编译Java代码。weaver(织入器):
为了在java编译器上使用AspectJ而不依赖于Ajc编译器,aspectJ 5出现了@AspectJ,使用注释的方式编写AspectJ代码,可以在任何Java编译器上使用。Aspect(切面):
实现了cross�cutting功能,是针对切面的模块。最常见的是logging模块、方法执行耗时模块,这样,程序按功能被分为好几层,如果按传统的继承的话,商业模型继承日志模块的话需要插入修改的地方太多,而通过创建一个切面就可以使用AOP来实现相同的功能了,我们可以针对不同的需求做出不同的切面。JoinPoint(连接点):
连接点是切面插入应用程序的地方,该点能被方法调用,而且也会被抛出意外。连接点是应用程序提供给切面插入的地方,可以添加新的方法。比如:我们的切点可以认为是findInfo(String)方法。
AspectJ将面向对象的程序执行流程看成是JoinPoint的执行链,每一个JoinPoint是一个单独的闭包,在执行的时候将上下文环境赋予闭包执行方法体逻辑。PointCut(切点):
切点的声明决定需要切割的JoinPoint的集合,就结果上来说,它是JoinPoint的一个实际子集合。
pointcut可以控制你把哪些advice应用于jointpoint(连接点)上去,通常通过正则表达式来进行匹配应用,决定了那个jointpoint(连接点)会获得通知。分为call、execution、target、this、within等关键字,含义下面会附图。
实现
说了一大堆概念,感觉还是不知道在说些什么,我们这个AspectJ到底能做啥?我们现在就举个例子:我们在程序中是不是很多功能需要申请相应的权限才能使用,每次我们要调用此方法的时候是不是需要先看权限是否获取,没获取需要先获取了再执行,这里就会出现每个地方都需要做判断,是不是很麻烦,如果我们可以做到在需要申请权限的方法之前统一获取权限,判断只需要写一次统一处理,代码也好管理,修改也容易,那不是一劳永逸,好,我们的AspectJ就帮我们完成了这个工作。
实现流程
- 自定义注解:我们需要自定义一个注解标记需要获取权限的方法,这个注解定义一个数组,你需要申请的权限集合和请求码(为了让回调做不同操作设置)
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {
/**
* 请求权限
*/
String[] value();
/**
* 请求码
*/
int requestCode();
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PermissionDenied {
/**
* 请求码
*/
int requestCode();
}
- 方法添加注解标记JoinPoint(连接点):
@Permission(value = "android.permission.READ_EXTERNAL_STORAGE",requestCode = 1)
public void getSd() {
//去读取存储卡里面的内容
}
@PermissionFailed(requestCode = 1)
private void requestPermissionFailed() {
Toast.makeText(this, "用户拒绝了权限", Toast.LENGTH_SHORT).show();
}
@PermissionDenied(requestCode = 1)
private void requestPermissionDenied() {
Toast.makeText(this, "权限申请失败,不再询问", Toast.LENGTH_SHORT).show();
//开发者可以根据自己的需求看是否需要跳转到设置页面去
PermissionUtil.startAndroidSettings(this);
}
- 获取所有连接点实现代码织入:获取所有的 JoinPoint连接点然后实现 PointCut(切点)的代码织入
@Aspect
public class PermissionAspect {
/**
* 声明切入点是所有的Permission注解标记了的方法
* @param permissions
*/
@Pointcut("execution(@com.maniu.mn_vip_aspectj.permission.annotation.Permission * *(..)) && @annotation(permissions)")
public void getPermission(Permission permissions){}
/**
* 根据切入点 织入代码
* @param proceedingJoinPoint
* @param permissions
* @throws Throwable
*/
@Around("getPermission(permissions)")
public void getPermissionPoint(final ProceedingJoinPoint proceedingJoinPoint, Permission permissions) throws Throwable {
//获取到上下文
Context context = null;
//获取到注解所在的类对象
final Object aThis = proceedingJoinPoint.getThis();
//判断aThis是否是Context的子类 如果是 就进行赋值
if(aThis instanceof Context){
context = (Context) aThis;
}else if(aThis instanceof Fragment){
context = ((Fragment) aThis).getActivity();
}
//获取到要申请的权限
if(context==null || permissions == null || permissions.value()==null || permissions.value().length==0){
return;
}
//获取到注解携带的权限数据
String[] value = permissions.value();
//由于权限回调的方法 只有Activity才有 因此 在这里我们创建一个透明的Activity来进行权限申请
ApplyPermissionActivity.launchActivity(context,value,permissions.requestCode(),
new PermissionRequestCallback() {
@Override
public void permissionSuccess() {
Log.e("-------->","权限申请结果:成功");
//权限获取成功 继续执行
try {
proceedingJoinPoint.proceed();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
@Override
public void permissionCanceled() {
// 权限申请被拒绝怎么办
//反射执行permissionCanceled()方法
PermissionUtil.invokeAnnotation(aThis,PermissionFailed.class);
}
@Override
public void permissionDenied() {
//权限申请被拒绝并且永久不提示怎么办
Log.e("-------->","权限申请结果:被永久拒绝");
//反射执行permissionDenied()方法
PermissionUtil.invokeAnnotation(aThis,PermissionDenied.class);
}
});
}
}
补充知识
在@Pointcut后面的括号中会有一个匹配连接点的表达式,这里给大家说下匹配的规则如下:
@Pointcut(execution(<><><><>) )
“修饰符”和“异常”可以省略。
- 修饰符(public ,private,注解(需要指定自定注释的绝对路径))
- 返回类型(使用*代表匹配所有返回类型)
- 包.类.方法:(支持模糊匹配)
- 参数 :方法传入的参数类型 (可以多个参数匹配,使用空格隔开)
支持三种通配符:
- 匹配任意字符,只匹配一个元素
- 匹配任意字符,可以匹配多个元素 ,在表示类时,必须和 联合使用
- 表示按照类型匹配指定类的所有类,必须跟在类名后面,如 ,表示继承该类的所有子类包括本身
支持逻辑运算符:
&&:与操作符。相当于切点的交集运算。xml配置文件中使用切点表达式,&是特殊字符,所以需要转义字符&;来表示。
:或操作符。相当于切点的并集运算。
:非操作符,相当于切点的反集运算
不同搭配请看如下示范:
execution(public * *(..)):任意public方法
execution(* set*(..)):方法名以set开头的任意方法
execution(* com.xyz.service.AccountService.*(..)):AccountService 接口下的任意方法
execution(* com.xyz.service..(..)):service包下的任意方法
execution(* com.xyz.service...(..)):service包或子包下的任意方法
execution(public void MyClass.myMethod(String)) :MyClass 类的myMethod方法,方法public访问权限,void返回值,形参只有一个并为String类型
execution(void MyClass.myMethod(..)):MyClass 类的myMethod方法,任意访问权限,返回值void,任意形参
execution(* MyClass.myMethod(..)):MyClass 类的myMethod方法,任意返回值,任意形参
execution(* MyClass.myMethod*(..)):MyClass 类的以myMethod开头的方法,任意返回值,任意形参
execution(* MyClass.myMethod*(String,..)):MyClass 类的以myMethod开头的方法,任意返回值,第一个形参类型是String
execution(* *.myMethod(..)):任意类下myMethod方法 execution(MyClass.new()):任意MyClass类的无参构造器
execution(MyClass.new(..)):任意MyClass类的任意有参构造器 execution(MyClass+.new(..)):任意MyClass或其子类构造器
execution(public * com.mycompany..*.*(..)):com.mycompany包下任意子包的所有类的所有public 方法
//Spring AOP只能切方法(也就是任意连接点中的方法),AspectJ可以切任意成员(任意连接点,包括类/对象初始化块,field,方法,构造器)
within(com.xyz.service.*):service包下任意连接点
within(com.xyz.service..*):service包或子包下任意连接点
this(com.xyz.service.AccountService):AccountService接口的代理实现里的任意连接点
target(com.xyz.service.AccountService):目标对象实现了AccountService接口的任意连接点
args(java.io.Serializable):只有一个参数且参数在运行时是Serializable类型的任意连接点
@target(org.springframework.transaction.annotation.Transactional):目标对象有一个@Transactional注解任意连接点
@within(org.springframework.transaction.annotation.Transactional):目标对象的声明类型有一个@Transactional注解任意连接点
@annotation(org.springframework.transaction.annotation.Transactional):执行方法有一个@Transactional注解的任意连接点
@args(com.xyz.security.Classified):只有一个参数且参数在运行时参数有@Classified注解的任意连接点
bean(tradeService):Spring bean 名称是tradeService的任意连接点
bean(*Service):Spring bean的名称以Service结尾的任意连接点
//表达式可以使用|| && !进行组合 在XML下就是 or and not
execution(* com.xyz.myapp.service..(..)) and this(service)//xml
execution(* com.xyz.myapp.service..(..)) && this(service)//java