AspectJ教程--AOP面向切面编程框架(Android)

AOP的概念很久前就接触过了,不过没有真正写过项目,甚至Demo都没有,今天把这点缺陷补上。
推荐两篇文章(本文部分图片引自这两篇文章):
1. 【翻译】Android中的AOP编程
2. 【深入理解Android之AOP】

1. 本篇文章总览

这里写图片描述

2. 什么是AOP

2.1 定义

AOP是Aspect Oriented Program的首字母缩写,译为:面向切面编程。类似的OOP,译为:面向对象编程。

  • OOP:面向对象思想简单理解就是,需要把各功能封装为独立模块,然后把他们简单拼装成为产品。Android系统的各个模块封装就遵循OOP(下图)。
    这里写图片描述
  • AOP:在这些独立的模块间,在特定的切入点进行hook,将共同的逻辑添加到模块中而不影响原有模块的独立性。下图,在不同的模块中加入日志、缓存、性能检测功能,并不影响原有的架构。
    这里写图片描述

2.2 相关术语

术语名称术语解释
Cross-cutting concerns(横切关注点)多个模块可能添加相同附属功能的点
Advice(通知)注入到class文件中的代码。典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。 除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。
join Point(连接点)所有可以注入代码的地方
PointCut(切入点)告诉AOP框架,我应该在哪个join point注入一段代码
Aspect(切面)由PointCut和Advice组成的公共逻辑成为切面,切面逻辑只需开发一次,多处调用
Weaving(织入)注入代码到目标位置

2.3 AOP使用场景

  • 日志相关
  • 持久化操作
  • 性能监控
  • 数据校验
  • 缓存
  • 等…

3 OOP和AOP实现具体需求

统计三个模块耗时。

3.1 OOP实现转向AOP实现图例

这里写图片描述

3.2 OOP实现

3.2.1 编写登录模块

简写代码如下:

/**
 * OOP 登录模块
 * Created by Administrator on 2017/10/13.
 */

public class LoginUtils {
    private static final String TAG = "OOP";

    public static boolean Login(String userName, String passWord){
        long start=System.currentTimeMillis();
        long end;
        StringBuffer stringBuffer = new StringBuffer();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if ("张三".equals(userName) && "123456".equals(passWord)){
            end = System.currentTimeMillis();
            stringBuffer.append("登录成功,耗时:")
            .append(end - start);
            Log.e(TAG,stringBuffer.toString());
            return true;
        }else{
            end = System.currentTimeMillis();
            stringBuffer.append("登录失败,耗时:")
                    .append(end - start);
            Log.e(TAG,stringBuffer.toString());
            return false;
        }
    }
}
3.2.2 编写文件上传模块

简写代码如下

/**
 * OOP 文件上传模块
 * Created by Administrator on 2017/10/13.
 */

public class UploadFileUtils {
    private static final String TAG = "OOP";

    public static boolean upload(String url, String path){
        long start=System.currentTimeMillis();
        long end;
        StringBuffer stringBuffer1 = new StringBuffer();
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }


        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append("从本地上传")
                .append(path)
                .append("到")
                .append(url);
        Log.e(TAG,stringBuffer.toString());

        end = System.currentTimeMillis();
        stringBuffer1.append("文件上传成功,耗时:")
                .append(end - start);
        Log.e(TAG,stringBuffer1.toString());

        return true;
    }
}
3.2.3 依次执行登录、转账、本地上传

日志如下:

10-12 13:28:17.300 14605-14605/com.aspectjdemo E/OOP: 登录成功,耗时:2000
10-12 13:28:19.315 14605-14605/com.aspectjdemo E/OOP: 从111112账户转出100.0到222221
10-12 13:28:19.315 14605-14605/com.aspectjdemo E/OOP: 转账成功,耗时:2001
10-12 13:28:21.317 14605-14605/com.aspectjdemo E/OOP: 从本地上传/sd/example.png到www.baidu.com
10-12 13:28:21.317 14605-14605/com.aspectjdemo E/OOP: 文件上传成功,耗时:2001
3.2.4 找出弊端

按照上面的实现方式,弊端有以下几个:

  • 代码冗余
  • 逻辑不清晰
  • 重构不方便

3.3 AOP实现

3.3.1 AspectJ实现AOP

 AspectJ是一个非侵入式的AOP框架,下一章专门介绍。此处只写Android Studio的实现方式,Eclipse实现方式不太一样。

3.3.1.1 第一步 配置AspectJ

在app的gradle文件中添加如下代码,作用:使用ajc代替javac编译java代码。具体说明见:【翻译】Android中的AOP编程

import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'org.aspectj:aspectjtools:1.8.9'
        classpath 'org.aspectj:aspectjweaver:1.8.9'
    }
}

apply plugin: 'com.android.application'

repositories {
    mavenCentral()
}

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.8",
                         "-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;
            }
        }
    }
}
dependencies {
    compile 'org.aspectj:aspectjrt:1.8.11'
}
3.3.1.2 第二步 自定义注解
/**
 * 自定义注解
 * Created by Administrator on 2017/10/13.
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface TimeTrace {
    String value();
}
3.3.1.3 第三步 实现各模块,并加上@TimerTrace注解

拿登录模块来举例,该模块中已经不包含耗时统计的逻辑。

/**
 * AOP 登录模块
 * Created by Administrator on 2017/10/13.
 */

public class AOPLoginUtils {
    private static final String TAG = "OOP";

    @TimeTrace(value = "登录")
    public static boolean Login(String userName, String passWord){

        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        if ("张三".equals(userName) && "123456".equals(passWord)){
            return true;
        }else{
            return false;
        }
    }
}
3.3.1.4 第四步 用@Aspect标注切面类
@Aspect
public class TimeTraceAspect {

}
3.3.1.5 第五步 在切面类中定义PointCut(切入点)
// 语法:execution(@注解 访问权限 返回值的类型 包名.函数名(参数))
// 表示:使用TimeTrace注解的任意类型返回值任意方法名(任意参数)
@Pointcut("execution(@com.aspectjdemo.aop.TimeTrace * *(..))")
public void myPointCut(){

}
3.3.1.6 第六步 在切面类中定义Advance(通知)

具体注入的代码

// Advance比较常用的有:Before():方法执行前,After():方法执行后,Around():代替原有逻辑
@Around("myPointCut()")
public Object dealPoint(ProceedingJoinPoint point) throws Throwable {
    // 方法执行前先记录时间
    long start=System.currentTimeMillis();
    MethodSignature methodSignature = (MethodSignature) point.getSignature();
    // 获取注解
    TimeTrace annotation = methodSignature.getMethod().getAnnotation(TimeTrace.class);
    String value = annotation.value();

    // 执行原方法体
    Object proceed = point.proceed();

    // 方法执行完成后,记录时间,打印日志
    long end = System.currentTimeMillis();
    StringBuffer stringBuffer = new StringBuffer();
    if (proceed instanceof Boolean){
        // 返回的是boolean
        if ((Boolean)proceed){
            stringBuffer.append(value)
                    .append("成功,耗时:")
                    .append(end - start);
        }else{
            stringBuffer.append(value)
                    .append("失败,耗时:")
                    .append(end - start);
        }
    }
    Log.e(TAG,stringBuffer.toString());
    return proceed;
}
3.3.1.6 第六步 依次执行登录、转账、本地上传
10-12 13:25:44.106 12332-12332/com.aspectjdemo E/AOP: 登录成功,耗时:2001
10-12 13:25:46.136 12332-12332/com.aspectjdemo E/OOP: 从111112账户转出100.0到222221
10-12 13:25:46.137 12332-12332/com.aspectjdemo E/AOP: 转账成功,耗时:2002
10-12 13:25:48.140 12332-12332/com.aspectjdemo E/OOP: 从本地上传/sd/example.png到www.baidu.com
10-12 13:25:48.140 12332-12332/com.aspectjdemo E/AOP: 文件上传成功,耗时:2001
3.3.1.7 总结优点
  • 减少代码冗余
  • 代码逻辑更清晰
  • 方便扩展、重构
3.3.2 其他方式实现AOP

不是本文重点,不深入,其实是我还没了解其他方式(~ ̄▽ ̄)~,稍微罗列一下。

  • 原始的AOP模式
  • 动态代理实现AOP
  • 等…
4 AspectJ详解

建议这一部分直接去看这个文章,这个文章,这部分很详细,很多语法,各种说明:【深入理解Android之AOP】

4.1 AspectJ介绍

AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,所以它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件。
使用AspectJ有两种方法:

  1. 完全使用 AspectJ语法(资料比较少,用AS未编译成功,下面不讲)
  2. 使用java开发,加上AspectJ注解(推荐使用,下面语法主要讲解注解的语法)。

官方网站:

AspectJ官方网站(下载AspectJ的jar包,更新AspectJ的adt等):http://www.eclipse.org/aspectj/
AspectJ类库参考文档:http://www.eclipse.org/aspectj/doc/released/runtime-api/index.html
AspectJ注解文档:http://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/index.html

4.2 语法

本节只讲解AspectJ的注解语法

4.2.1 Join Points介绍

在Aspect的术语章节讲过,join Point(连接点):所有可以注入代码的地方,在AspectJ中是有规定的,只有在下表的几个地方才认为是join Ponit。


Join Points

说明

 

示例

method call

函数调用

比如调用Log.e(),这是一处JPoint

method execution

函数执行

比如Log.e()的执行内部,是一处JPoint。注意它和method call的区别。method call是调用某个函数的地方。而execution是某个函数执行的内部。

constructor call

构造函数调用

和method call类似

constructor execution

构造函数执行

和method execution类似

field get

获取某个变量

比如读取DemoActivity.debug成员

field set

设置某个变量

比如设置DemoActivity.debug成员

pre-initialization

Object在构造函数中做得一些工作。

很少使用,详情见下面的例子

initialization

Object在构造函数中做得工作

详情见下面的例子

static initialization

类初始化

比如类的static{}

handler

异常处理

比如try catch(xxx)中,对应catch内的执行

advice execution

这个是AspectJ的内容,稍后再说

 


4.2.2 Pointcuts介绍

在Aspect的术语章节讲过,PointCut(切入点):告诉AOP框架,我应该在哪个join point注入一段代码。那么Pointcuts就是筛选出来的符合条件的所有切入点。

1、 直接选择Join Point

Join PointPonitcut语法示例
Method executionexecution(MethodSignature)在Activtiy的所有生命周期执行前,注入代码:@Before(“execution(* android.app.Activity.on**(..))”)
Method callcall(MethodSignature)在调用指定方法后,注入代码:@Before(“execution(* android.app.Activity.on**(..))”)
constructor callcall(ConstructorSignature)在调用指定构造方法后,注入代码:@Before(“call(com.aspectjdemo.aopexample.UIUtils.new())”)
constructor executionexecution(ConstructorSignature)在执行指定构造方法后,注入代码:@After(“call(com.aspectjdemo.aopexample.UIUtils.new())”)
field getget(FieldSignature)在调用指定字段get方法后,注入代码:@After(“get(String com.aspectjdemo.aopexample.AspectJActivity.userName)”)
field setset(FieldSignature)在调用指定字段get方法前,注入代码:@Before(“set(String com.aspectjdemo.aopexample.AspectJActivity.userName)”)
Object initializationinitialization(ConstructorSignature)在指定的对象初始化后,注入代码:@After(“initialization(com.aspectjdemo.aopexample.UIUtils.new())”)

MethodSignature匹配规则:

@注解 访问权限 返回值的类型 包名.函数名(参数)
1. @注解和访问权限(public/private/protect,以及static/final)属于可选项。如果不设置它们,则默认都会选择。以访问权限为例,如果没有设置访问权限作为条件,那么public,private,protect及static、final的函数都会进行搜索。
2. 返回值类型就是普通的函数的返回值类型。如果不限定类型的话,就用*通配符表示
3. 包名.函数名用于查找匹配的函数。可以使用通配符,包括*和..以及+号。其中*号用于匹配除.号之外的任意字符,而..则表示任意子package,+号表示子类。
比如:
java.*.Date:可以表示java.sql.Date,也可以表示java.util.Date
Test*:可以表示TestBase,也可以表示TestDervied
java..*:表示java任意子类
java..*Model+:表示Java任意package中名字以Model结尾的子类,比如TabelModel,TreeModel

4. 最后来看函数的参数。参数匹配比较简单,主要是参数类型,比如:
(int, char):表示参数只有两个,并且第一个参数类型是int,第二个参数类型是char
(String, ..):表示至少有一个参数。并且第一个参数类型是String,后面参数类型不限。在参数匹配中,
..代表任意参数个数和类型
(Object …):表示不定个数的参数,且类型都是Object,这里的…不是通配符,而是Java中代表不定参数的意思

ConstructorSignature匹配规则:

Constructorsignature和Method Signature类似,只不过构造函数没有返回值,而且函数名必须叫new。比如:
public *..TestDerived.new(..):
public:选择public访问权限
*..代表任意包名
TestDerived.new:代表TestDerived的构造函数
(..):代表参数个数和类型都是任意

FieldSignature匹配规则:

Field Signature标准格式:
@注解 访问权限 类型 类名.成员变量名
其中,@注解和访问权限是可选的类型:成员变量类型,*代表任意类型类名.成员变量名:成员变量名可以是*,代表任意成员变量
比如,
set(*.base):表示设置所有包名下base变量时的JPoint

2、 间接选择Join Point

这一类,在demo中没有示例,有兴趣的自己在Demo中添加查看效果。


关键词

说明

 

示例

within(TypePattern)

TypePattern标示package或者类。TypePatter可以使用通配符

表示某个Package或者类中的所有JPoint。比如

within(Test):Test类中(包括内部类)所有JPoint。图2所示的例子就是用这个方法。

withincode(Constructor Signature|Method Signature)

表示某个构造函数或其他函数执行过程中涉及到的JPoint

比如

withinCode(* TestDerived.testMethod(..))

表示testMethod涉及的JPoint

withinCode( *.Test.new(..))

表示Test构造函数涉及的JPoint

cflow(pointcuts)

cflow是call flow的意思

cflow的条件是一个pointcut

比如

cflow(call TestDerived.testMethod):表示调用TestDerived.testMethod函数时所包含的JPoint,包括testMethod的call这个JPoint本身

cflowbelow(pointcuts)

cflow是call flow的意思。

比如

cflowblow(call TestDerived.testMethod):表示调用TestDerived.testMethod函数时所包含的JPoint,包括testMethod的call这个JPoint本身

this(Type)

JPoint的this对象是Type类型。

(其实就是判断Type是不是某种类型,即是否满足instanceof Type的条件)

JPoint是代码段(不论是函数,异常处理,static block),从语法上说,它都属于一个类。如果这个类的类型是Type标示的类型,则和它相关的JPoint将全部被选中。

图2示例的testMethod是TestDerived类。所以

this(TestDerived)将会选中这个testMethod JPoint

target(Type)

JPoint的target对象是Type类型

和this相对的是target。不过target一般用在call的情况。call一个函数,这个函数可能定义在其他类。比如testMethod是TestDerived类定义的。那么

target(TestDerived)就会搜索到调用testMethod的地方。但是不包括testMethod的execution JPoint

args(TypeSignature)

用来对JPoint的参数进行条件搜索的

比如args(int,..),表示第一个参数是int,后面参数个数和类型不限的JPoint。

 


4.2.3 advice介绍

前面例子中已经用过了,具体看下面表中说明即可。


关键词

说明

 

示例

before()

before advice

表示在JPoint执行之前,需要干的事情

after()

after advice

表示JPoint自己执行完了后,需要干的事情。

after():returning(返回值类型)

after():throwing(异常类型)

returning和throwing后面都可以指定具体的类型,如果不指定的话则匹配的时候不限定类型

假设JPoint是一个函数调用的话,那么函数调用执行完有两种方式退出,一个是正常的return,另外一个是抛异常。

注意,after()默认包括returning和throwing两种情况

返回值类型 around()

before和around是指JPoint执行前或执行后备触发,而around就替代了原JPoint

around是替代了原JPoint,如果要执行原JPoint的话,需要调用proceed


4.3 实现步骤

PS:这一节已经很详细的在 3.3.1中写了,还有代码示例,往上看。

4.4 AspectJ原理
4.4.1 找到AS下的class文件

路径如下:app->build->intermediates->classes->debug->com包下即是我们使用ajc编译后的class代码。

4.4.2 Around原理

使用Around处理后,编译出来的class文件

 @TimeTrace("登录")
public static boolean Login(String userName, String passWord) {
    JoinPoint var5 = Factory.makeJP(ajc$tjp_0, (Object)null, (Object)null, userName, passWord);
    TimeTraceAspect var10000 = TimeTraceAspect.aspectOf();
    Object[] var6 = new Object[]{userName, passWord, var5};
    return Conversions.booleanValue(var10000.dealPoint((new AOPLoginUtils$AjcClosure1(var6)).linkClosureAndJoinPoint(65536)));
}

可以看出来,在这个方法的开头和结尾,都被注入了一些代码,成为我们最终运行到手机上的class文件。

5. 总结

本文只是对AspectJ做了一个入门的介绍,很多高级的用法都未加入进来,在实际项目使用时再进行挖掘吧。

如果有对应的AOP使用场景,建议使用AspectJ,你会感觉到很爽的。

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值