可使用AOP实现Android端非侵入式埋点数据上报的构想

目录

一、OOP跟AOP

二、AspectJ是什么

三、AspectJ中的几个名词术语解释

四、AOP编程的具体使用场景

五、注入代码的时机

六、几个常用的工具和类库

七、为什么选择AspectJ

八、举例说明

这里我们有两点需要知道:

创建用于控制监听的类

封装一下android.util.Log

关键的Aspect类的实现

在build.gradle文件中的一些必要的配置

测试方法

运行结果:

总结


一、OOP跟AOP

OOP的精髓是把功能或问题模块化,每个模块处理自己的家务事。但在现实世界中,并不是所有问题都能完美得划分到模块中。举个最简单而又常见的例子:现在想为每个模块加上日志功能,要求模块运行时候能输出日志。在不知道AOP的情况下,一般的处理都是:先设计一个日志输出模块,这个模块提供日志输出API,比如Android中的Log类。然后,其他模块需要输出日志的时候调用Log类的几个函数,比如e(TAG,…),w(TAG,…),d(TAG,…),i(TAG,…)等。

但是,从OOP角度看,除了日志模块本身,其他模块的家务事绝大部分情况下应该都不会包含日志输出功能。以ActivityManagerService为例,你能说它的家务事里包含日志输出吗?显然,ActivityManagerService的功能点中不包含输出日志这一项。但实际上,软件中的众多模块确实又需要打印日志。这个日志输出功能,从整体来看,都是一个面上的。而这个面的范围,就不局限在单个模块里了,而是横跨多个模块。

在AOP中:

第一,我们要认识到OOP世界中,有些功能是横跨并嵌入众多模块里的,比如打印日志,数据上报等。这些功能在各个模块里分散得很厉害,可能到处都能见到。 
第二,AOP的目标是把这些功能集中起来,放到一个统一的地方来控制和管理。如果说,OOP如果是把问题划分到单个模块的话,那么AOP就是把涉及到众多模块的某一类问题进行统一管理。比如我们可以设计两个Aspects,一个是管理某个软件中所有模块的日志输出的功能,另外一个是管理该软件中一些特殊函数调用的权限检查。

二、AspectJ是什么


正如面向对象编程是对常见问题的模块化一样,面向切面编程是对横向的同一问题进行模块化,比如在某个包下的所有类中的某一类方法中都需要解决一个相似的问题,可以通过AOP的编程方式对此进行模块化封装,统一解决。关于AOP的具体解释,可以参照维基百科。而AspectJ就是面向切面编程在Java中的一种具体实现。

AspectJ向Java引入了一个新的概念——join point,它包括几个新的结构: pointcuts,advice,inter-type declarations 和 aspects。

join point是在程序流中被定义好的点。pointcut在那些点上选出特定的join point和值。advice是到达join point时被执行的代码。

AspectJ还具有不同类型的类型间声明(inter-type declarations),允许程序员修改程序的静态结构,即其类的成员和类之间的关系。

三、AspectJ中的几个名词术语解释


Cross-cutting concerns:即使在面向对象编程中大多数类都是执行一个单一的、特定的功能,它们也有时候需要共享一些通用的辅助功能。比如我们想要在一个线程进入和退出一个方法时,在数据层和UI层加上输出log的功能。尽管每一个类的主要功能时不同的,但是它们所需要执行的辅助功能是相似的。

Advice:需要被注入到.class字节码文件的代码。通常有三种:before,after和around,分别是在目标方法执行前,执行后以及替换目标代码执行。除了注入代码到方法中外,更进一步的,你还可以做一些别的修改,例如添加成员变量和接口到一个类中。

Join point:程序中执行代码插入的点,例如方法调用时或者方法执行时。

Pointcut:告诉代码注入工具在哪里注入特定代码的表达式(即需要在哪些Joint point应用特定的Advice)。它可以选择一个这样的点(例如,一个单一方法的执行)或者许多相似的点(例如,所有被自定义注解@DebugTrace标记的方法)。

Aspect: Aspect将pointcut和advice 联系在一起。例如,我们通过定义一个pointcut和给出一个准确的advice实现向我们的程序中添加一个打印日志功能的aspect。

Weaving:向目标位置(join point)注入代码(advice)的过程。

上面几个名词间的关系的示意图如下:

这里写图片描述

四、AOP编程的具体使用场景


日志记录
持久化
行为监测
数据验证
缓存 


五、注入代码的时机


运行时:你的代码对增强代码的需求很明确,比如,必须使用动态代理(这可以说并不是真正的代码注入)。

加载时:当目标类被Dalvik或者ART加载的时候修改才会被执行。这是对Java字节码文件或者Android的dex文件进行的注入操作。

编译时:在打包发布程序之前,通过向编译过程添加额外的步骤来修改被编译的类。

具体使用哪一种方式视使用情况而定。

六、几个常用的工具和类库


AspectJ:和Java语言无缝衔接的面向切面的编程的扩展工具(可用于Android)。

Javassist for Android:一个移植到Android平台的非常知名的操纵字节码的java库。

DexMaker:用于在Dalvik VM编译时或运行时生成代码的基于java语言的一套API。

ASMDEX:一个字节码操作库(ASM),但它处理Android可执行文件(DEX字节码)。

七、为什么选择AspectJ


非常强大

易于使用

支持编译时和加载时的代码注入

八、举例说明

现在有一个需求,我们需要计算一个方法的运行时间,我们想通过给这个方法加上我们自定义的注解@DebugTrace来实现这个需求,而不是在业务代码中很生硬地插入计算时间的代码。这里我们就可以通过AspectJ来实现我们的目的。

这里我们有两点需要知道:

1、注解将在我们编译过程中的一个新步骤中被处理。

2、必要的模板代码将会被生成和注入到被注解的方法中。

在这个实例中,我们将分出两个module,一个用于业务代码,一个用于利用AspectJ进行代码注入。(这里要说明一下,AspectJ本身是一套java library,为了让AspectJ在Android上正确运行,我们使用了android library,因为我们必须在编译应用程序时使用一些钩子,只能使用android-library gradle插件。)
创建注解

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

创建用于控制监听的类

/**
 * Class representing a StopWatch for measuring time.
 */
public class StopWatch {
  private long startTime;
  private long endTime;
  private long elapsedTime;

  public StopWatch() {
    //empty
  }

  private void reset() {
    startTime = 0;
    endTime = 0;
    elapsedTime = 0;
  }

  public void start() {
    reset();
    startTime = System.nanoTime();
  }

  public void stop() {
    if (startTime != 0) {
      endTime = System.nanoTime();
      elapsedTime = endTime - startTime;
    } else {
      reset();
    }
  }

  public long getTotalTimeMillis() {
    return (elapsedTime != 0) ? TimeUnit.NANOSECONDS.toMillis(endTime - startTime) : 0;
  }
}

封装一下android.util.Log

/**
 * Wrapper around {@link android.util.Log}
 */
public class DebugLog {

  private DebugLog() {}

  /**
   * Send a debug log message
   *
   * @param tag Source of a log message.
   * @param message The message you would like logged.
   */
  public static void log(String tag, String message) {
    Log.d(tag, message);
  }
}

关键的Aspect类的实现

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

  private static final String POINTCUT_METHOD =
      "execution(@org.android10.gintonic.annotation.DebugTrace * *(..))";

  private static final String POINTCUT_CONSTRUCTOR =
      "execution(@org.android10.gintonic.annotation.DebugTrace *.new(..))";

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

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

  @Around("methodAnnotatedWithDebugTrace() || constructorAnnotatedDebugTrace()")
  public Object weaveJoinPoint(ProceedingJoinPoint joinPoint) throws Throwable {
    MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
    String className = methodSignature.getDeclaringType().getSimpleName();
    String methodName = methodSignature.getName();

    final StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    // 被注解的方法在这一行代码被执行
    Object result = joinPoint.proceed();
    stopWatch.stop();

    DebugLog.log(className, buildLogMessage(methodName, stopWatch.getTotalTimeMillis()));

    return result;
  }

  /**
   * Create a log message.
   *
   * @param methodName A string with the method name.
   * @param methodDuration Duration of the method in milliseconds.
   * @return A string representing message.
   */
  private static String buildLogMessage(String methodName, long methodDuration) {
    StringBuilder message = new StringBuilder();
    message.append("Gintonic --> ");
    message.append(methodName);
    message.append(" --> ");
    message.append("[");
    message.append(methodDuration);
    message.append("ms");
    message.append("]");

    return message.toString();
  }
}

关于上面这段代码这里提两点:

我们声明了两个公共方法和两个pointcut用于过滤所有被”org.android10.gintonic.annotation.DebugTrace”标记的方法和构造器。

我们定义的 “weaveJointPoint(ProceedingJoinPoint joinPoint)” 这个方法被添加了”@Around”注解,这意味着我们的代码注入将发生在被”@DebugTrace”注解标记的方法前后。
 

在build.gradle文件中的一些必要的配置

要是AspectJ在Android上正确运行,还需要在build.gradle文件中进行一些必要的配置,如下:

import com.android.build.gradle.LibraryPlugin
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main

buildscript {
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath 'com.android.tools.build:gradle:0.12.+'
    classpath 'org.aspectj:aspectjtools:1.8.1'
  }
}

apply plugin: 'android-library'

repositories {
  mavenCentral()
}

dependencies {
  compile 'org.aspectj:aspectjrt:1.8.1'
}

android {
  compileSdkVersion 19
  buildToolsVersion '19.1.0'

  lintOptions {
    abortOnError false
  }
}

android.libraryVariants.all { variant ->
  LibraryPlugin plugin = project.plugins.getPlugin(LibraryPlugin)
  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", plugin.project.android.bootClasspath.join(
        File.pathSeparator)]

    MessageHandler handler = new MessageHandler(true);
    new Main().run(args, handler)

    def log = project.logger
    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:
        case IMessage.INFO:
          log.info message.message, message.thrown
          break;
        case IMessage.DEBUG:
          log.debug message.message, message.thrown
          break;
      }
    }
  }
}

测试方法

@DebugTrace
  private void testAnnotatedMethod() {
    try {
      Thread.sleep(10);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

运行结果:

Gintonic --> testAnnotatedMethod --> [10ms]

我们可以通过对apk文件进行反编译来查看被注入后的代码。

总结

AOP编程在进行用户行为统计是是一种非常可靠的解决方案,避免了直接在业务代码中进行埋点,而AOP编程的应用还不仅于此,它在性能监控,数据采集等方面也有着广泛的应用,后续将继续研究,并整理发布。AspectJ是一个很强大的用于AOP编程的库,使用AspectJ关键在于掌握它的pointcut的语法,这里给一个AspectJ的官方的doc链接,需要注意的是,经过实际测试,有一些语法在Android中是无法使用的,需要在实际使用过程中进行总结。

参考文章:

1、https://blog.csdn.net/fly0078/article/details/80719863

2、https://blog.csdn.net/cmder1000/article/details/70193988

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
以下是一个使用AOP实现客户操作接口数据变化的示例代码: 首先,定义一个注解 `@DataChangeLog`,用于标识需要记录数据变化的方法: ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) public @interface DataChangeLog { } ``` 然后,定义一个切面 `DataChangeLogAspect`,用于在标有 `@DataChangeLog` 注解的方法前后记录数据变化: ```java @Aspect @Component public class DataChangeLogAspect { @Autowired private DataChangeLogService dataChangeLogService; @Before("@annotation(com.example.demo.annotation.DataChangeLog)") public void before(JoinPoint joinPoint) { Object[] args = joinPoint.getArgs(); // 记录方法执行前的数据状态 // ... } @AfterReturning(value = "@annotation(com.example.demo.annotation.DataChangeLog)", returning = "result") public void afterReturning(JoinPoint joinPoint, Object result) { Object[] args = joinPoint.getArgs(); // 记录方法执行后的数据状态 // ... // 将数据变化记录到数据库 dataChangeLogService.logDataChange(dataChangeLog); } } ``` 在上面的代码中,`@Before` 注解表示在标有 `@DataChangeLog` 注解的方法执行前执行,`@AfterReturning` 注解表示在标有 `@DataChangeLog` 注解的方法执行后执行并获取方法的返回值。 最后,在客户操作接口的方法上添加 `@DataChangeLog` 注解即可: ```java @Service public class CustomerServiceImpl implements CustomerService { @Autowired private CustomerRepository customerRepository; @Override @DataChangeLog public void updateCustomer(Customer customer) { customerRepository.save(customer); } // ... } ``` 这样,每次调用 `updateCustomer` 方法时,都会自动记录下客户数据的变化,并将数据变化记录到数据库。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值