Android面向切面编程(AOP)

一、简述

1.AOP概念

AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。一句话概括:在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程。

2.运用场景

项目开发过程中,可能会有这样的需求,需要我们在方法执行完成后,记录日志(后台开发中比较常见~),或是计算这个方法的执行时间,在不使用AOP的情况下,我们可以在方法最后调用另一个专门记录日志的方法,或是在方法体的首尾分别获取时间,然后通过计算时间差来计算整个方法执行所消耗的时间,这样也可以完成需求。那如果不只一个方法要这么玩怎么办?每个方法都写上一段相同的代码吗?后期处理逻辑变了要怎么办?其实这种问题我们完全可以用AOP来解决。

3.AOP的实现方式

AOP仅仅只是个概念,实现它的方式(工具和库)有以下几种:

  • AspectJ: 一个 JavaTM 语言的面向切面编程的无缝扩展(适用Android)。
  • Javassist for Android: 用于字节码操作的知名 java 类库 Javassist 的 Android 平台移植版。
  • DexMaker: Dalvik 虚拟机上,在编译期或者运行时生成代码的 Java API。
  • ASMDEX: 一个类似 ASM 的字节码操作库,运行在Android平台,操作Dex字节码。

下面就来看看AspectJ方式的AOP如何在Android开发中进行使用的。

二、AspectJ导入

项目gradle添加

buildscript {
    repositories {
        google()
        jcenter()
        
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.4.0'
        classpath 'org.aspectj:aspectjtools:1.9.4'
        
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

主model添加依赖

dependencies {
    ...
     implementation 'org.aspectj:aspectjrt:1.9.4'
}

添加gradle任务,直接粘贴到build.gradle文件的末尾即可,不要嵌套在别的指令中。


import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

final def log = project.logger
final def variants = project.android.applicationVariants

//在构建工程时,执行编辑
variants.all { variant ->
    if (!variant.buildType.isDebuggable()) {
        log.debug("Skipping non-debuggable build type '${variant.buildType.name}'.")
        return;
    }

    JavaCompile javaCompile = variant.javaCompile
    javaCompile.doLast {
        String[] args = ["-showWeaveInfo",
                         "-1.9",
                         "-inpath", javaCompile.destinationDir.toString(),
                         "-aspectpath", javaCompile.classpath.asPath,
                         "-d", javaCompile.destinationDir.toString(),
                         "-classpath", javaCompile.classpath.asPath,
                         "-bootclasspath", project.android.bootClasspath.join(File.pathSeparator)]
        log.debug "ajc args: " + Arrays.toString(args)

        MessageHandler handler = new MessageHandler(true);
        new Main().run(args, handler);
        for (IMessage message : handler.getMessages(null, true)) {
            switch (message.getKind()) {
                case IMessage.ABORT:
                case IMessage.ERROR:
                case IMessage.FAIL:
                    log.error message.message, message.thrown
                    break;
                case IMessage.WARNING:
                    log.warn message.message, message.thrown
                    break;
                case IMessage.INFO:
                    log.info message.message, message.thrown
                    break;
                case IMessage.DEBUG:
                    log.debug message.message, message.thrown
                    break;
            }
        }
    }
}

三、AspectJ基础知识

在这里插入图片描述
AOP注解

四、AspectJ实现AOP

1.创建自定义注解

用注解来标记切点,一般会使用自定义注解,方便我们拓展。

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TestAnnoTrace {
    String value();
    int type();
}
  • @Target(ElementType.METHOD):表示该注解只能注解在方法上。如果想类和方法都可以用,那可以这么写:@Target({ElementType.METHOD,ElementType.TYPE}),依此类推。
  • @Retention(RetentionPolicy.RUNTIME):表示该注解在程序运行时是可见的(还有SOURCE、CLASS分别指定注解对于那个级别是可见的,一般都是用RUNTIME)。

其中的value和type是自己拓展的属性,方便存储一些额外的信息。

2.使用自定义注解标记切点

这个自定义注解只能注解在方法上(构造方法除外,构造方法也叫构造器,需要使用ElementType.CONSTRUCTOR),像平常使用其它注解一样使用它即可:

@TestAnnoTrace(value = "test", type = 1)
public void test(View view) {
    System.out.println("Hello");
}

3.创建切面类

切点表达式的组成如下:

execution(<@注解类型模式>? <修饰符模式>? <返回类型模式> <方法名模式>(<参数模式>) <异常模式>?)

要织入一段代码到目标类方法的前前后后,必须要有一个切面类,下面就是切面类的代码:

@Aspect
public class TestAnnoAspect {

    //切点表达式使用自定义的注解,一定是@+注解全路径
    @Pointcut("execution(@com.hxl.androidaopdemo.TestAnnoTrace * *(..))")
    public void pointcut() {}
    
    @Before("pointcut()")
    public void before(JoinPoint point) {
        System.out.println("@Before");
    }

    @Around("pointcut()")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("@Around");
        joinPoint.proceed();// 目标方法执行完毕
    }

    @After("pointcut()")
    public void after(JoinPoint point) {
        System.out.println("@After");
    }

    @AfterReturning("pointcut()")
    public void afterReturning(JoinPoint point, Object returnValue) {
        System.out.println("@AfterReturning");
    }

    @AfterThrowing(value = "pointcut()", throwing = "ex")
    public void afterThrowing(Throwable ex) {
        System.out.println("@afterThrowing");
        System.out.println("ex = " + ex.getMessage());
    }
}

打印结果:

@Before
@Around
Hello
@After
@AfterReturning

少了一个@AfterThrowing通知。这个通知只有在切点抛出异常时才会执行

4.JoinPoint的作用

发现没有,上面所有的通知都会至少携带一个JointPoint参数,这个参数包含了切点的所有信息,下面就结合按钮的点击事件方法test()来解释joinPoint能获取到的方法信息有哪些:

MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String name = signature.getName(); // 方法名:test
Method method = signature.getMethod(); // 方法:public void com.hxl.androidaopdemo.MainActivity.test(android.view.View)
Class returnType = signature.getReturnType(); // 返回值类型:void
Class declaringType = signature.getDeclaringType(); // 方法所在类名:MainActivity
String[] parameterNames = signature.getParameterNames(); // 参数名:view
Class[] parameterTypes = signature.getParameterTypes(); // 参数类型:View

//上面在编写自定义注解时就声明了两个属性,分别是value和type,
//通过Method对象得到切点上的注解
TestAnnoTrace annotation = method.getAnnotation(TestAnnoTrace.class);
String value = annotation.value();
int type = annotation.type();

5.方法耗时计算的实现

因为@Around是环绕通知,可以在切点的前后分别执行一些操作,AspectJ为了能肯定操作是在切点前还是在切点后,所以在@Around通知中需要手动执行joinPoint.proceed()来确定切点已经执行,故在joinPoint.proceed()之前的代码会在切点执行前执行,在joinPoint.proceed()之后的代码会切点执行后执行。于是,方法耗时计算的实现就是这么简单:

@Around("pointcut()")
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
    long beginTime = SystemClock.currentThreadTimeMillis();
    joinPoint.proceed();
    long endTime = SystemClock.currentThreadTimeMillis();
    long dx = endTime - beginTime;
    System.out.println("耗时:" + dx + "ms");
}

五、动态代理实现AOP

动态代理也叫做JDK代理、接口代理。不需要实现目标对象的接口。生成代理对象,使用的是Java的API,动态的在内存中构件代理对象(这需要我们指定创建代理对象/目标对象的接口的类型)。

在java的动态代理机制中,有两个重要的类或接口,一个是 InvocationHandler(Interface)、另一个则是Proxy(Class),这一个类和接口是实现我们动态代理所必须用到的。

InvocationHandler

public interface InvocationHandler {
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable;
}

每一个代理实类例的invocation handler都要实现InvocationHandler这个接口。并且每个代理类的实例都关联到了一个handler,当我们通过代理对象调用一个方法的时候,这个方法的调用就会被转发为由InvocationHandler这个接口的 invoke 方法来进行调用。

  • proxy:指代生成的代理对象;
  • method:指代的是我们所要调用真实对象的某个方法的Method对象;
  • args:指代的是调用真实对象某个方法时接受的参数;

我们来看看Proxy这个类,这个类的作用就是用来动态创建一个代理对象的类,它提供了许多的方法,但用的最多的就是 newProxyInstance 这个方法:

public static Object newProxyInstance(ClassLoader loader,
                                          Class<?>[] interfaces,
                                          InvocationHandler h)
  • loader:一个ClassLoader对象,定义了由哪个ClassLoader对象来对生成的代理对象进行加载
  • interfaces:一个Interface对象的数组,表示的是我将要给我需要代理的对象提供一组什么接口,如果我提供了一组接口给它,那么这个代理对象就宣称实现了该接口(多态),这样我就能调用这组接口中的方法了
  • h:一个InvocationHandler对象,表示的是当我这个动态代理对象在调用方法的时候,会关联到哪一个InvocationHandler对象上。

通过上面的需要传入接口的参数可以看出,JDK动态代理需要借助接口来实现,如果我们要代理的对象功能没有抽成任何接口,那么我们就无法通过JDK动态代理的方式来实现。

好了,在介绍完这两个接口(类)以后,我们来通过一个实例来看看我们的动态代理模式是什么样的。首先我们定义了一个Subject类型的接口,为其声明了两个方法,这两个方法表示被代理类需要实现的功能:

public interface Subject {
    public void sayGoodBye();
    public void sayHello(String str);
}

接着,定义了一个类来实现这个接口,这个类就是我们的真实对象(被代理类),RealSubject类:

public class RealSubject implements Subject {
    @Override
    public void sayGoodBye() {
        System.out.println("RealSubject sayGoodBye");
    }

    @Override
    public void sayHello(String str) {
        System.out.println("RealSubject sayHello  " + str);
    }
}

下一步,我们就要定义一个InvocationHandler了,相当于一个代理处理器。前面说个,每一个动态代理类实例的invocation handler 都必须要实现 InvocationHandler 这个接口:

public class SubjectInvocationHandler implements InvocationHandler {
    //这个就是我们要代理的真实对象
    private Object subject;

    //构造方法,给我们要代理的真实对象赋初值
    public SubjectInvocationHandler(Object subject) {
        this.subject = subject;
    }

    @Override
    public Object invoke(Object object, Method method, Object[] args) throws Throwable {
        //在代理真实对象前我们可以添加一些自己的操作
        System.out.println("before Method invoke");
        System.out.println("Method:" + method);
        //当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
        method.invoke(subject, args);
        //在代理真实对象后我们也可以添加一些自己的操作
        System.out.println("after Method invoke");
        return null;
    }
}

SubjectInvocationHandler并不是真正的代理类,而是用于定义代理类需要扩展、增强那些方法功能的类。在invoke函数中,对代理对象的所有方法的调用都被转发至该函数处理。在这里可以灵活的自定义各种你能想到的逻辑。

最后,来看看我们的Client类:

 public static void main(String[] args) {
       //被代理类
       Subject realSubject = new RealSubject();
       //我们要代理哪个类,就将该对象传进去,最后是通过该被代理对象来调用其方法的
       SubjectInvocationHandler handler = new SubjectInvocationHandler(realSubject);
       //生成代理类
       Subject subject = (Subject) Proxy.newProxyInstance(handler.getClass().getClassLoader(), 
                                                          realSubject.getClass().getInterfaces(), handler);
       //输出代理类对象
       System.out.println("Proxy : "+ subject.getClass().getName());
       System.out.println("Proxy super : "+ subject.getClass().getSuperclass().getName());
       System.out.println("Proxy interfaces : "+ subject.getClass().getInterfaces()[0].getName());
       //调用代理类sayGoodBye方法
       subject.sayGoodBye();
       System.out.println("--------");
       //调用代理类sayHello方法
       subject.sayHello("Test");
   }

输出结果:

Proxy : com.sun.proxy.$Proxy0
Proxy super : java.lang.reflect.Proxy
Proxy interfaces : com.company.ha.Subject
before Method invoke
Method:public abstract void com.company.ha.Subject.sayGoodBye()
RealSubject sayGoodBye
after Method invoke
--------
before Method invoke
Method:public abstract void com.company.ha.Subject.sayHello(java.lang.String)
RealSubject sayHello  Test
after Method invoke

如果我们定义的方法有返回值,那么可以通过invoke中把该方法的返回值进行返回,因为返回值的对象是Object,所以支持返回值为空(void)的返回。

 @Override
public Object invoke(Object object, Method method, Object[] args) throws Throwable {
    //直接返回调用对象的返回值
    return method.invoke(subject,args);
}

六、动态代理应用(Retrofit)

Android中的网络注解框架retrofit内部实现其实就是应用了动态代理技术,通常我们定义网络接口是这样的:

public interface ApiStore {

    // 员工登录
    @FormUrlEncoded
    @POST("/resource/d/member/login")
    Observable<BaseResponse<LoginResult>> login(@FieldMap Map<String, String> params);

    // 退出登录
    @FormUrlEncoded
    @POST("/resource/d/member/signOut")
    Observable<BaseResponse<LogOutResult>> logout(@FieldMap Map<String, String> params);
    
    //....
}

创建Retrofit:

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://ww.xxx.com/")
    .build();
ApiStore service = retrofit.create(ApiStore.class);

我们可以看retrofit.create内部具体的实现方式:

public <T> T create(final Class<T> service) {
    //...
    return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
        new InvocationHandler() {
          private final Platform platform = Platform.get();

          @Override public Object invoke(Object proxy, Method method, @Nullable Object[] args)
              throws Throwable {
            // If the method is a method from Object then defer to normal invocation.
            if (method.getDeclaringClass() == Object.class) {
              return method.invoke(this, args);
            }
            if (platform.isDefaultMethod(method)) {
              return platform.invokeDefaultMethod(method, service, proxy, args);
            }
            ServiceMethod<Object, Object> serviceMethod =
                (ServiceMethod<Object, Object>) loadServiceMethod(method);
            OkHttpCall<Object> okHttpCall = new OkHttpCall<>(serviceMethod, args);
            return serviceMethod.callAdapter.adapt(okHttpCall);
          }
        });
  }

看到没,retrofit就是通过Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },new InvocationHandler() )的方式创建的一个代理类,invoke中的代码就是当网络接口被调用的时候需要做的处理。在这个方法里,首先根据接口定义的方法,生成一个ServiceMethod对象,在ServiceMethod对象中会反射接口中定义的注解,解析出具体的网络请求方式,然后拿到封装好的ServiceMethod对象后,构造一个OkHttpCall对象,以便与进行真正的网络请求(OkHttp)。

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

推荐阅读更多精彩内容