Using AspectJ to Implement Non-intrusive Embedding Point at Android End

Preface

Recently, in the project, there is a need to collect user behavior through buried points. Because the project runs in the LAN, and there are some very detailed requirements, after comparing several technical schemes, we choose to bury points through AspectJ. This paper mainly introduces the author's summary of learning and using AspectJ.

What is AspectJ

Just as object-oriented programming is the modularization of common problems, aspect-oriented programming is the modularization of horizontal identical problems. For example, a similar problem needs to be solved in one class of methods in all classes under a package. It can be modularized and encapsulated by AOP programming and solved uniformly. The specific explanation of AOP can be referred to. Wikipedia . AspectJ is a concrete implementation of aspect-oriented programming in Java.

AspectJ introduces a new concept to Java, join point, which includes several new structures: pointcuts, advice, inter-type declarations and aspects.

Join points are defined points in a program flow. Point cuts select specific join points and values at those points. advice is the code that is executed when it reaches join point.

AspectJ also has different types of inter-type declarations that allow programmers to modify the static structure of programs, that is, the relationships between members of their classes and classes.

Explanation of Several Terms in AspectJ

  • Cross-cutting concerns: Even though most classes perform a single, specific function in object-oriented programming, they sometimes need to share some common auxiliary functions. For example, we want to add the function of output log to the data layer and UI layer when a thread enters and exits a method. Although the main functions of each class are different, the auxiliary functions they need to perform are similar.

  • Advice: Code that needs to be injected into the. class bytecode file. Usually there are three kinds: before, after and around, which are executed before, after and after the execution of the target method, respectively, and replaced by the execution of the target code. In addition to injecting code into methods, you can make other modifications, such as adding member variables and interfaces to a class.

  • Join point: The point in a program where code insertion is performed, such as when a method is called or when a method is executed.

  • Pointcut: Tell the code injection tool where to inject a specific code expression (that is, which Joint Points need to apply a specific Advice). It can choose one such point (for example, the execution of a single method) or many similar points (for example, all methods tagged with a custom annotation @DebugTrace).

  • Aspect: Aspect connects pointcut with advice. For example, we add a print log aspect to our program by defining a pointcut and giving an accurate advice.

  • Weaving: The process of injecting advice into the target location.

The diagrams of the relations between the above nouns are as follows:


Specific usage scenarios for AOP programming

  • Logging
  • Persistence
  • Behavior monitoring
  • data validation
  • cache
    ...

Time to inject code

  • Runtime: Your code has a clear need for enhanced code, such as the need to use dynamic proxies (which can be said not to be true code injection).

  • Loading: Modifications are performed when the target class is loaded by Dalvik or ART. This is an injection of Java bytecode files or Android dex files.

  • Compile time: Before packaging the publisher, modify the compiled class by adding additional steps to the compilation process.

Which way to use depends on the use.

Several commonly used tools and class libraries

  • AspectJ Aspect-oriented programming extensions (available for Android) seamlessly linked to the Java language.

  • Javassist for Android A well-known java library for bytecode manipulation ported to the Android platform.

  • DexMaker A set of java-based API s for generating code at Dalvik VM compile-time or run-time.

  • ASMDEX A bytecode operation library (ASM), but it handles Android executable files (DEX bytecodes).

Why choose AspectJ

  • Very powerful

  • Easy to use

  • Supporting code injection at compile time and load time

Take a chestnut


Now there is a requirement that we need to calculate the running time of a method. We want to implement this requirement by adding our custom annotation @DebugTrace to this method, rather than inserting computational time code into business code. Here we can achieve our goal through AspectJ.

Here are two things we need to know:

  • Annotations will be processed in a new step in our compilation process.

  • The necessary template code will be generated and injected into the annotated method.

This process can be understood by the following schematic diagram:


In this example, we will divide two module s, one for business code and one for code injection using AspectJ. (To illustrate that AspectJ itself is a set of Java libraries. In order for AspectJ to run properly on Android, we use the android library, because we have to use some hooks when compiling the application, and we can only use the android-library gradle plug-in.)

Create Note
@Retention(RetentionPolicy.CLASS)
@Target({ ElementType.CONSTRUCTOR, ElementType.METHOD })
public @interface DebugTrace {}
Create classes for control listening
/**
 * 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;
  }
}
Encapsulate 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);
  }
}
Implementation of key Aspect classes
/**
 * 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();
    // The annotated method is executed in this line of code
    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();
  }
}

Here are two points about the above code:

  • We declared two common methods and two pointcut s for filtering all methods and constructors tagged with "org. Android 10. gintonic. annotation. DebugTrace".

  • The method we defined as "weave Joint Point" (Proceeding Join Point Joint Point) has been added with the "@Around" annotation, which means that our code injection will occur before and after the method marked by the "@DebugTrace" annotation.

The following diagram will help to understand the composition of pointcut:


Some necessary configurations in the build.gradle file

If AspectJ works correctly on Android, some necessary configurations need to be made in the build.gradle file, as follows:

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;
      }
    }
  }
}
test method
@DebugTrace
  private void testAnnotatedMethod() {
    try {
      Thread.sleep(10);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

Operation results:

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

We can view the injected code by decompiling the apk file.

summary

AOP programming in user behavior statistics is a very reliable solution to avoid burying points directly in business code, and the application of AOP programming is not only this, it also has a wide range of applications in performance monitoring, data acquisition, etc., follow-up will continue to study, and collate and publish. AspectJ is a powerful library for AOP programming. The key to using AspectJ is to master its pointcut grammar. Here's an official doc link to AspectJ It should be noted that after actual testing, some grammars are not available in Android, and need to be summarized in the actual use process.

Keywords: Android Programming Java Gradle

Added by mysterio2099 on Tue, 09 Jul 2019 02:37:48 +0300