AOP之AspectJ - 代码注入

Posted everlastxgb

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了AOP之AspectJ - 代码注入相关的知识,希望对你有一定的参考价值。

AOP之AspectJ - 代码注入


一、AOP简介

1.1 什么是AOP编程

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

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

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

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:

图2:

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模块封装成插件的方式,隐藏实现细节,业务层只需引用插件即可,同时也方便维护。

五、参考资料

以上是关于AOP之AspectJ - 代码注入的主要内容,如果未能解决你的问题,请参考以下文章

AOP之@AspectJ技术原理详解

AOP之@AspectJ技术原理详解

一起学Spring之注解和Schema方式实现AOP

Android AOP编程之AspectJ

Android AOP编程之AspectJ

Android AOP编程之AspectJ