AOP之AspectJ - 代码注入

AOP之AspectJ - 代码注入


[TOC]

一、AOP简介

1.1 什么是AOP编程

AOP是Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。

AOP编程是一种区别OOP编程的概念,从切面的角度看待问题,是函数式编程的一种衍生范型。在AOP中,我们不需要显式的修改就可以向代码中添加可执行的代码块,有效的保证了业务逻辑的各个部分的隔离,降低耦合度,提高程序的可重用性,同时提高了开发的效率。

OOP的思想让我们把功能或问题模块化,每个模块有自己的职责和使命。相比较,AOP让我们在保持开发模块隔离的同时可以将一些需要横跨多个模块的代码嵌入其中,把涉及到众多模块的某一类问题进行统一管理。

AOP-横跨.png

1.2 使用场景

  • 性能监控: 在方法调用前后记录调用时间,方法执行太长或超时报警。
  • 无痕埋点: 在需要埋点的地方添加对应统计代码。
  • 缓存代理: 缓存某方法的返回值,下次执行该方法时,直接从缓存里获取。
  • 记录日志: 在方法执行前后记录系统日志。
  • 权限验证: 方法执行前验证是否有权限执行当前方法,没有则抛出没有权限执行异常,由业务代码捕捉。
  • 其它

1.3 工具和库

目前已有不少的工具和库能帮助我们方便使用AOP。

  • AspectJ: 一个 JavaTM 语言的面向切面编程的无缝扩展(适用Android)。
    • 简介:可以织入所有类;支持编译期和加载时代码注入;编写简单,功能强大。需要使用ajc编译器编译,ajc编译器是java编译器的扩展,具有其所有功能。
  • Javassist for Android: 用于字节码操作的知名 java 类库 Javassist 的 Android 平台移植版。
    • 简介:可以织入绝大部分类;运行时生成,减少不必要的生成开销;通过将切面逻辑写入字节码,减少了生成子类的开销,不会产生过多子类。运行时加入切面逻辑,产生性能开销。
  • DexMaker: Dalvik 虚拟机上,在编译期或者运行时生成代码的 Java API。
    • 简介:支持编译期和加载时代码注入;运行在Android Dalvik VM上,利用Java编写,来动态生成DEX字节码的API。
  • ASMDEX: 一个类似 ASM 的字节码操作库,运行在Android平台,操作Dex字节码。
    • 简介:可以织入所有类;支持编译期和加载时代码注入。修改字节码,需要对class文件比较熟悉,编写过程复杂。

二、AspectJ

2.1 简介

AspectJ是一个面向切面的框架,它扩展了Java语言。AspectJ定义了AOP语法,它有一个专门的编译器用来生成遵守Java字节编码规范的Class文件,支持静态编译和动态编译。

  • 优点:可以织入所有类;支持编译期和加载时代码注入;编写简单,功能强大。
  • 缺点:需要使用ajc编译器编译,ajc编译器是java编译器的扩展,具有其所有功能。

使用AspectJ编码更为简洁,API简单易用。个人觉得,在Android开发中,是实现AOP的首选。

2.2 一些专业术语

  • Cross-cutting concerns(横切关注点): 尽管面向对象模型中大多数类会实现单一特定的功能,但通常也会开放一些通用的附属功能给其他类。例如,我们希望在数据访问层中的类中添加日志,同时也希望当UI层中一个线程进入或者退出调用一个方法时添加日志。尽管每个类都有一个区别于其他类的主要功能,但在代码里,仍然经常需要添加一些相同的附属功能。
  • Advice(通知): 注入到class文件中的代码。典型的 Advice 类型有 before、after 和 around,分别表示在目标方法执行之前、执行后和完全替代目标方法执行的代码。 除了在方法中注入代码,也可能会对代码做其他修改,比如在一个class中增加字段或者接口。
  • Joint point(连接点): 程序中可能作为代码注入目标的特定的点,例如一个方法调用或者方法入口。
  • Pointcut(切入点): 告诉代码注入工具,在何处注入一段特定代码的表达式。例如,在哪些 joint points 应用一个特定的 Advice。切入点可以选择唯一一个,比如执行某一个方法,也可以有多个选择,比如,标记了一个定义成@DebguTrace 的自定义注解的所有方法。
  • Aspect(切面): Pointcut 和 Advice 的组合看做切面。例如,我们在应用中通过定义一个 pointcut 和给定恰当的advice,添加一个日志切面。
  • Weaving(织入): 注入代码(advices)到目标位置(joint points)的过程。

下面引用《Aspect Oriented Programming in Android》中的两张图来帮助我们更好地理解这些概念:

图1:

AspectOrientedProgramming

图2:

AspectWeaving

2.3 基础知识

继续向下阅读之前,你可能需要先了解一些基础知识以便更好地理解。由于篇幅有限,无法扩展详解,可以根据以下列出的内容先行了解。

往后的内容我们将针对AspectJ的具体使用和如何根据使用场景做成插件来展开探讨。

2.4 AspectJ使用配置

(Android Studio Gradle)

1.添加 dependencies classpath

classpath 'org.aspectj:aspectjtools:1.8.10'

2.添加 dependencies compile

compile 'org.aspectj:aspectjrt:1.8.10'

3.build.gradle添加task命令
(若为多Module则每个Module对应的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 // when in application module
// final def variants = project.android.libraryVariants // when in library module
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.5",
                         "-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;
            }
        }
    }
}

三、使用场景

3.1一个简单的示例

本节将演示利用AspectJ在编译期向标有注解的方法织入两行代码,分别在执行前和执行后,并输出log。

工程结构如下:

  • GodMonitor
    • godmonitor-example
    • godmonitor-annotations (注解所在)
    • godmonitor-runtime (AspectJ代码注入)

0.添加相关依赖和声明

gradle中的配置请参考上面 2.4 AspectJ使用配置

1.定义一个注解

@Retention(RetentionPolicy.CLASS)
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.CONSTRUCTOR})
public @interface GMonitor {
}

2.定义代码注入Aspect类

/**
 * Aspect representing the cross cutting-concern: Method and Constructor Tracing.
 */
@Aspect
public class GodMonitor {

  private static final String POINTCUT_METHOD =
          "execution(@com.kido.godmonitor.weaving.GMonitor * *(..))"; // 通过GMonitor注解的方法

  private static final String POINTCUT_CONSTRUCTOR =
          "execution(@com.kido.godmonitor.weaving.GMonitor *.new(..))"; // 通过GMonitor注解的构造函数

  @Pointcut(POINTCUT_METHOD)
  public void methodAnnotatedWithDebugTrace() {}

  @Pointcut(POINTCUT_CONSTRUCTOR)
  public void constructorAnnotatedDebugTrace() {}

  @Around("methodAnnotatedWithDebugTrace() || constructorAnnotatedDebugTrace()") // 筛选出所有通过GMonitor注解的方法和构造函数
  public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    String className = methodSignature.getDeclaringType().getSimpleName();
    String methodName = methodSignature.getName();

    DebugLog.log(className, buildLogMessage(methodName, " before execution "));
    Object result = joinPoint.proceed(); // 注解所在的方法/构造函数的执行的地方
    DebugLog.log(className, buildLogMessage(methodName, " after execution "));

    return result;
  }

  /**
   * Create a log message.
   *
   * @param methodName A string with the method name.
   * @param info Extra info.
   * @return A string representing message.
   */
  private static String buildLogMessage(String methodName, String info) {
    StringBuilder message = new StringBuilder();
    message.append("GodMonitor --> ");
    message.append(methodName);
    message.append(" --> ");
    message.append("[");
    message.append(info);
    message.append("]");

    return message.toString();
  }
}

3.在MainActivity中测试

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "kido";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        findViewById(R.id.button1).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                doAction1();
            }
        });
        findViewById(R.id.button2).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                doAction2();
            }
        });
    }

    @GMonitor
    private void doAction1() {
        Log.d(TAG, "you do the action111111.");
    }

    @GMonitor
    private void doAction2() {
        Log.d(TAG, "you do the action222222.");
    }
}

4.运行结果

运行程序点击按钮,可以看到在方法前后成功输出了我们织入的代码片段输出的log。

com.kido.godmonitor D/MainActivity: GodMonitor --> doAction1 --> [ before execution ]
com.kido.godmonitor D/kido: you do the action111111.
com.kido.godmonitor D/MainActivity: GodMonitor --> doAction1 --> [ after execution ]

com.kido.godmonitor D/MainActivity: GodMonitor --> doAction2 --> [ before execution ]
com.kido.godmonitor D/kido: you do the action222222.
com.kido.godmonitor D/MainActivity: GodMonitor --> doAction2 --> [ after execution ]

5.反编译看生成的class

反编译看生成的class文件,可以看到在标有注解的地方中被织入了aspect的相关代码:

    @GMonitor
    private void doAction1() {
        JoinPoint var1 = Factory.makeJP(ajc$tjp_0, this, this);
        GodMonitor var10000 = GodMonitor.aspectOf();
        Object[] var2 = new Object[]{this, var1};
        var10000.weaveJoinPoint((new MainActivity$AjcClosure1(var2)).linkClosureAndJoinPoint(69648));
    }

6.本例源码地址

GodMonitor-pre

3.2 一个简单的示例(后续)

上一节我们利用AspectJ实现了一个简单的代码注入示例,但是我们会发现,我们在Module的build.gradle需要添加一大串的AspectJ的task命令,如果一旦Module很多,那么将会异常繁琐。
那么,有什么简单快捷的替代方案吗?答案肯定是有的,我们可以自定义gradle plugin,将这部分逻辑移到plugin中实现,那么在需要的Module处我们直接声明引用plugin即可。

3.2.1 自定义plugin

添加sub module,名为godmonitor-plugin,用于使用Groovy开发我们对应的插件。那么,工程结构就会相应变成如下:

  • GodMonitor
    • godmonitor-example
    • godmonitor-annotations (注解所在)
    • godmonitor-runtime (AspectJ代码注入)
    • godmonitor-plugin (gradle插件)

1.GodMonitorPlugin.groovy


class GodMonitorPlugin implements Plugin<Project> {
  @Override void apply(Project project) {
    def hasApp = project.plugins.withType(AppPlugin)
    def hasLib = project.plugins.withType(LibraryPlugin)
    if (!hasApp && !hasLib) {
      throw new IllegalStateException("'android' or 'android-library' plugin required.")
    }

    final def log = project.logger
    final def variants
    if (hasApp) {
      variants = project.android.applicationVariants
    } else {
      variants = project.android.libraryVariants
    }

    project.dependencies {
      compile 'com.kido.godmonitor:godmonitor-runtime:0.0.1'
      // TODO this should come transitively
      compile 'org.aspectj:aspectjrt:1.8.10'
      compile 'com.kido.godmonitor:godmonitor-annotations:0.0.1'
    }

    project.extensions.create('godmonitor', GodMonitorExtension)

    variants.all { variant ->
      if (!project.godmonitor.enabled) {
        log.debug("GodMonitor is not disabled.")
        return;
      }

      JavaCompile javaCompile = variant.javaCompile
      javaCompile.doLast {
        String[] args = [
            "-showWeaveInfo",
            "-1.5",
            "-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;
          }
        }
      }
    }
  }
}

编译成插件之后,你会发现你在example中只需引用如下即可:

classpath 'com.kido.godmonitor:godmonitor-plugin:0.0.1'
...
apply plugin: 'godmonitor'

3.2.2 发布到Maven

  1. 添加发布脚本gradle-mvn-push.gradle。
  2. 在gradle.properties中设置发布信息。
  3. 在插件工程build.gradle中引用脚本。
  4. 使用gradle的uploadArchives命令上传发布。

(详情可参见项目源码)

3.2.3 一些说明

maven { url 'http://100.84.197.220:8089/repository/android-releases/' }
...
classpath 'com.kido.godmonitor:godmonitor-plugin:0.0.1'
...
apply plugin: 'godmonitor'
  • 本例插件使用示例:
    (只需在要织入代码的方法上面对应添加注解即可)
    @GMonitor
    private void doAction1() {
        Log.d(TAG, "you do the action111111.");
    }

    @GMonitor
    private void doAction2() {
        Log.d(TAG, "you do the action222222.");
    }

四、小结

AOP编程在进行用户行为统计方面是一种非常可靠的解决方案,避免了直接在业务代码中进行埋点,另外,它在性能监控、数据采集等方面也有着广泛的应用。AspectJ作为AOP编程的一个实现框架,方便易用,主要关键在于掌握它的pointcut的语法。在实际使用中,我们可以根据具体场景将我们的AOP模块封装成插件的方式,隐藏实现细节,业务层只需引用插件即可,同时也方便维护。

五、参考资料

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

推荐阅读更多精彩内容