所谓切面编程,就算将一个完整的流程切分成若干个,然后主流程只关心调用的方法,而不关心具体的实现逻辑,而子流程在完成业务逻辑后会把通知主流程方法继续向下执行或者通知失败;说白了就是方法的拦截操作,可以在方法执行前后进行一定的处理,然后根据需求判断是否是真正进入执行该方法或是直接跳过该方法抛出异常之类的,可以类比成OkHttp的拦截器
Aspect是一个实现切面编程的框架,本篇介绍一下Aspect常用的一种方法
配置信息
应用build配置里添加依赖
implementation 'org.aspectj:aspectjrt:1.9.2'
配置脚本信息,这里是固定格式
import org.aspectj.tools.ajc.Main
project.android.applicationVariants.all { variant ->
JavaCompile javaCompile = variant.javaCompile
javaCompile.doLast {
String[] args = [
"-1.7",
"-inpath", javaCompile.destinationDir.toString(),
"-d", javaCompile.destinationDir.toString(),
"-aspectpath", javaCompile.classpath.asPath,
"-classpath", javaCompile.classpath.asPath,
"-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
new Main().runMain(args, false)
}
}
root配置
classpath 'org.aspectj:aspectjtools:1.9.2'
代码信息
1.声明一个类,用@Aspect标记,表示这个是个AOP的编译入口,里面定义的方法会被检测编译
@Aspect
public class AspectTest{
...
}
2.定义两个注解入口
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestEntrance {
String[] params();
int code();
}
这是方法入口,就是我们主流程调用的子流程方法会用这个进行标记;然后编译后会插入我们拦截的部分插桩方法,当该方法被调用时候,会优先进入我们的插入的逻辑,在里面判断最终是否执行这个方法或者绕过这个方法,走额外定义的失败方法
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequestFail {
}
这是方法失败的标记,上面判断逻辑认为是不符合的我们会手动绕过该方法,反射调用失败的方法
3.定义切入点
@Pointcut("execution(@com.example.aop.annoation.RequestEntrance void *(..)) && @annotation(request)")
public void pointcutSolve(RequestEntrance request) {
}
这里的格式说明一下
execution 是定义的切入点执行入口,会根据该括号内的表达式匹配相应的方法;
@com.example.aop.annoation.RequestEntrance 这个上面定义的注解的全路径格式;
void 是匹配的方法的返回值类型;
* 是通配符,表示匹配任意名称的方法,也可以写固定名称或者 xx*等表示xx开头的方法;
这句表达式的意思就是匹配 RequestEntrance标记的所有void的方法;
这是常用的注解标记的方法匹配;
如果不使用注解,则可以定义成 execution(* xx.xx.methodName()) 第一个*表示任意返回参数,后面是方法名的全路径;
后面的 && 符号表示添加一个参数传递,传递的是annotation类型的名字为request的参数,这个名字得和定义的方法中的参数名字保持一致;annotation不能写错;这里的annotation主要是为了获取该注解的参数,比如定义权限申请的场合,会在参数中声明需要授权的权限信息
4.定义拦截方法,就是调用上面的PointCut方法,这里参数名也要保持一致
@Around("pointcutSolve(request)")
public void mainEntrance(ProceedingJoinPoint joinPoint, RequestEntrance request) {
String name = joinPoint.getSignature().getName();
Log.e("1234", "开始执行代码插入逻辑 -> "+name);
Object thisObj = joinPoint.getThis();
Object[] args = joinPoint.getArgs();
String params[] = request.params();
int code = request.code();
boolean value = true;
if (value) {
try {
if (args != null && args.length > 0){
String[] first = (String[]) args[0];
first[0] = "这是替换的文字";
}
joinPoint.proceed(args);
Log.e("1234", "插入逻辑执行结束 -> "+name);
} catch (Throwable throwable) {
throwable.printStackTrace();
}
} else {
Log.e("1234", "执行插入失败方法 -> "+name);
solveFail(thisObj, code, params);
}
}
拦截的入口使用Around标记;除此之外,Aspect还提供了@Before,@After , @AfterThrowing, @AfterReturning 方法,分别表示在方法执行前,方法执行后,方法抛出异常后,方法返回值返回后执行;而相对而已,Around能实现上面的所有方法
这里我定义了一个拦截方法,并定义了一个开关可以控制继续方法的执行或直接走失败,如果有参数,会把第一个参数替换掉再去调用原方法
private void solveFail(Object target, int code, String... params) {
try {
Method[] methods = target.getClass().getDeclaredMethods();
for (Method method : methods) {
if (method.isAnnotationPresent(RequestFail.class)) {
Class<?>[] paramTypes = method.getParameterTypes();
if (paramTypes.length < 2) {
throw new RuntimeException("参数个数不正确");
}
if (!paramTypes[0].isAssignableFrom(int.class)) {
throw new RuntimeException("首个必须是int类型");
}
if (!paramTypes[1].isAssignableFrom(String[].class)) {
throw new RuntimeException("第二个必须是数组类型");
}
method.invoke(target, code, params);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
失败的方法我通过反射去调用实际的对象中的方法,并把注解中设置的值返回
然后在主页面定义了有注解的三个方法
@RequestEntrance(params = "t1", code = 1)
public void request1() {
Log.e(TAG, "request1 方法执行了 " );
}
@RequestEntrance(params = {"t2", "t3"}, code = 2)
public void request2(String... params) {
Log.e(TAG, "request2 方法执行了,参数是 " + Arrays.asList(params).toString());
}
@RequestFail
public void requsetFail(int code, String... params) {
Log.e(TAG, "requestFail " + code +" "+ Arrays.asList(params).toString());
}
分别是一个无参的,一个多个参数的,以及一个失败的方法
先调用第一个request1方法,打印日志是
E/1234: 开始执行代码插入逻辑 -> request1
E/1234: request1 方法执行了
E/1234: 插入逻辑执行结束 -> request1
然后把上面的标记改成false,走失败方法
E/1234: 开始执行代码插入逻辑 -> request1
E/1234: 执行插入失败方法 -> request1
E/1234: requestFail 1 [t1]
可以看出原方法直接绕过没有执行,直接走到了上面定义的requestFail方法,并正确传入了注解的code和param参数
然后调用第二个request2方法,request2("1234","asdf")
打印日志是
E/1234: 开始执行代码插入逻辑 -> request2
E/1234: 执行插入失败方法 -> request2
E/1234: requestFail 2 [t2, t3]
把上面的标记改回true,打印日志是
E/1234: 开始执行代码插入逻辑 -> request2
E/1234: request2 方法执行了,参数是 [这是替换的文字, asdf]
E/1234: 插入逻辑执行结束 -> request2
总结
1.需要声明@Aspect类,标记这里面定义的方法会被检测是否拦截执行方法
2.需要声明@Pointcut注解的方法,在这个注解里会定义所匹配的方法类型,当执行的方法和这里定义的表达式匹配,那么会走到下面@Around定义拦截方法中
3.需要声明@Around注解的方法,这里处理具体拦截的业务逻辑,方法执行前后都可以进行操作,需要定义一个ProceedingJoinPoint参数类型,一般放在第一位;当确定中断拦截,原方法继续执行时,调用这个参数的process方法即可;
4.根据需求定义方法入口和失败的注解类,并标记相应的方法;
调用入口方法, 处理子业务逻辑,处理完毕后继续入口方法的执行,或者绕过该方法走失败的方法回调