Android 架构师之路17 AOP 面向切面编程

Android 架构师之路 目录

引言

相信很多做过Web的同学对AspectJ都不陌生,Spring的AOP就是基于它而来的。如果说平常我们随便写写程序的时候,基本也不会用到它,需要调试的话无非就是多加一个System.out.printfln()或者Log.d()。但是由于基于面向对象的固有缺陷,导致很多同模块、同一水平上的工作要在许多类中重复出现。比如说:输出日志,监控方法执行时间,修改程序运行时的参数等等这样的事情,其实它们的代码都是可以重用的。

如果在一个大型的项目当中,使用手动修改源码的方式来达到调试、监控的目的,第一,需要插入许多重复代码(打印日志,监控方法执行时间),代码无法复用;第二,修改的成本太高,处处需要手动修改(分分钟累死、眼花)。

  • OOP: 面向对象把所有的事物都当做对象看待,因此每一个对象都有自己的生命周期,都是一个封装的整体。每一个对象都有自己的一套垂直的系列方法和属性,使得我们使用对象的时候不需要太多的关系它的内部细节和实现过程,只需要关注输入和输出,这跟我们的思维方式非常相近,极大的降低了我们的编写代码成本(而不像C那样让人头痛!)。但在现实世界中,并不是所有问题都能完美得划分到模块中。举个最简单而又常见的例子:现在想为每个模块加上日志功能,要求模块运行时候能输出日志。在不知道AOP的情况下,一般的处理都是:先设计一个日志输出模块,这个模块提供日志输出API,比如Android中的Log类。然后,其他模块需要输出日志的时候调用Log类的几个函数,比如e(TAG,…),w(TAG,…),d(TAG,…),i(TAG,…)等。

  • AOP: OOP固然开启另一个编程时代,但是久而久之也显露了它的缺点,最明显的一点就是它无法横向切割某一类方法、属性,当我们需要了解某一类方法、某一类属性的信息时,就必须要在每一个类的方法里面(即便他们是同样的方法,只因是不同的类所以不同)添加监控代码,在代码量庞大的情况下,这是一个不可取的方法。因此,AOP编产生了,基于AOP的编程可以让我们横向的切割某一类方法和属性(不需要关心他是什么类别!),AOP并不是与OOP对立的,而是为了弥补OOP的不足,因为有了AOP我们的调试和监控就变得简单清晰。

1.AspectJ介绍

1.1 AspectJ只是一个代码编译器

AspectJ 意思就是Java的Aspect,Java的AOP。它其实不是一个新的语言,它就是一个代码编译器(ajc,后面以此代替),在Java编译器的基础上增加了一些它自己的关键字识别和编译方法。因此,ajc也可以编译Java代码。它在编译期将开发者编写的Aspect程序编织到目标程序中,对目标程序作了重构,目的就是建立目标程序与Aspect程序的连接(耦合,获得对方的引用(获得的是声明类型,不是运行时类型)和上下文信息),从而达到AOP的目的(这里在编译期还是修改了原来程序的代码,但是是ajc替我们做的)。

1.2 AspectJ是用来做AOP编程的

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)的过程。

下面这张图简要总结了一下上述这些概念。



传统编程:逐个插入验证用户模块


传统方案

AOP方案:关注点聚焦
AOP方案
1.3、为什么要用AspectJ?
  • 非侵入式监控: 支持编译期和加载时代码注入,可以在不修监控目标的情况下监控其运行,截获某类方法,甚至可以修改其参数和运行轨迹!
  • 易于使用: 它就是Java,只要会Java就可以用它。
  • 功能强大,可拓展性高: 它就是一个编译器+一个库,可以让开发者最大限度的发挥,实现形形色色的AOP程序!

2、下载AspectJ相关资源与build.gradle配置

2.1、下载地址

下载aspectj的地址http://www.eclipse.org/aspectj/downloads.php

2.2、解压aspectj jar包得到aspectjrt.jar
2.3、build.gradle配置

参考build.gradle aspectJ 写法 http://fernandocejas.com/2014/08/03/aspect-oriented-programming-in-android/
根目录中build.gradle配置:

buildscript {
    
    repositories {
        mavenCentral()
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.0.0'
        classpath 'org.aspectj:aspectjtools:1.8.13'
        classpath 'org.aspectj:aspectjweaver:1.8.13'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

modules中build.gradle配置:

apply plugin: 'com.android.application'
import org.aspectj.bridge.IMessage
import org.aspectj.bridge.MessageHandler
import org.aspectj.tools.ajc.Main


android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.haocai.aopdemo"
        minSdkVersion 15
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

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 {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'com.android.support:appcompat-v7:26.1.0'
    implementation 'com.android.support.constraint:constraint-layout:1.0.2'
    testImplementation 'junit:junit:4.12'
    compile files('libs/aspectjrt.jar')
    androidTestImplementation 'com.android.support.test:runner:1.0.1'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
   // compile 'org.aspectj:aspectjrt:1.8.+'
}
注意:

dependencies 不要忘记添加 compile files('libs/aspectjrt.jar') ,aspectjrt.jar就是上一步解压得到的文件,放到libs文件夹下

3、示例程序

创建注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface BehaviorTrace {
    String value();
    int type();
}
Aspect 类
/**
 * Created by Xionghu on 2018/1/23.
 * Desc: 切面
 * 你想要切下来的部分(代码逻辑功能重复模块)
 */
@Aspect
public class BehaviorAspect {
    private static final String TAG = "MainAspect";
    SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    /**
     * 根据切点 切成什么样子
     *
     */
    @Pointcut("execution(@com.haocai.aopdemo.BehaviorTrace * *(..))")
    public void annoBehavior() {

    }
    /**
     * 切成什么样子之后,怎么去处理
     *
     */

    @Around("annoBehavior()")
    public Object dealPoint(ProceedingJoinPoint point) throws Throwable{
        //方法执行前
        MethodSignature methodSignature = (MethodSignature)point.getSignature();
        BehaviorTrace behaviorTrace = methodSignature.getMethod().getAnnotation(BehaviorTrace.class);
        String contentType = behaviorTrace.value();
        int type = behaviorTrace.type();
        Log.i(TAG,contentType+"使用时间:   "+simpleDateFormat.format(new Date()));
        long beagin=System.currentTimeMillis();
        //方法执行时
        Object object = null;
        try{
            object = point.proceed();
        }catch (Exception e){
            e.printStackTrace();
        }

        //方法执行完成
        Log.i(TAG,"消耗时间:"+(System.currentTimeMillis()-beagin)+"ms");
        return object;
    }
}

调用主程序

package com.haocai.aopdemo;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.SystemClock;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;

import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
public class MainActivity extends AppCompatActivity {
    private static final String TAG = "Main";
    SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);





    }
    /**
     * 摇一摇的模块
     *
     * @param view
     */
    @BehaviorTrace(value = "摇一摇",type = 1)
    public  void mShake(View view)
    {
        //摇一摇的代码逻辑
        {
            SystemClock.sleep(3000);
            Log.i(TAG," 摇到一个红包");

        }
    }
    /**
     * 语音的模块
     *
     * @param view
     */
    @BehaviorTrace(value = "语音:",type = 1)
    public  void mAudio(View view)
    {
        //语音代码逻辑
        {
            SystemClock.sleep(3000);
            Log.i(TAG,"发语音:我要到一个红包啦");
        }
    }
    /**
     * 打字模块
     *
     * @param view
     */
    @BehaviorTrace(value = "打字:",type = 1)
    public  void mText(View view)
    {
        //打字模块逻辑
        {
            SystemClock.sleep(3000);
            Log.i(TAG,"打字逻辑,我摇到了一个大红包");

        }

    }


//    /**
//     * 摇一摇的模块
//     *
//     * @param view
//     */
//    @BehaviorTrace(value = "摇一摇",type = 1)
//    public  void mShake(View view)
//    {
//            SystemClock.sleep(3000);
//            Log.i(TAG,"  摇到一个嫩模:  约不约");
//    }
//
//    /**
//     * 摇一摇的模块
//     *
//     * @param view
//     */
//    public  void mShake(View view)
//    {
//
//        long beagin=System.currentTimeMillis();
//        Log.i(TAG,"摇一摇:  使用时间:   "+simpleDateFormat.format(new Date()));
//        //摇一摇的代码逻辑
//        {
//            SystemClock.sleep(3000);
//
//            Log.i(TAG," 摇到一个红包");
//
//        }
//        //事件统计逻辑
//        Log.i(TAG,"消耗时间:  "+(System.currentTimeMillis()-beagin)+"ms");
//
//    }

//    /**
//     * 语音的模块
//     *
//     * @param view
//     */
//    public  void mAudio(View view)
//    {
//        long beagin=System.currentTimeMillis();
//        Log.i(TAG,"语音:  使用时间:   "+simpleDateFormat.format(new Date()));
//        //语音代码逻辑
//        {
//            SystemClock.sleep(3000);
//
//            Log.i(TAG,"发语音:我要到一个红包啦");
//
//        }
//       //事件统计逻辑
//        Log.i(TAG,"消耗时间:  "+(System.currentTimeMillis()-beagin)+"ms");
//    }
//
//    /**
//     * 打字模块
//     *
//     * @param view
//     */
//    public  void mText(View view)
//    {
//        //统计用户行为 的逻辑
//        Log.i(TAG,"文字:  使用时间:   "+simpleDateFormat.format(new Date()));
//        long beagin=System.currentTimeMillis();
//
//        //打字模块逻辑
//        {
//            SystemClock.sleep(3000);
//            Log.i(TAG,"打字逻辑,我摇到了一个大红包");
//
//        }
//        //事件统计逻辑
//        Log.i(TAG,"消耗时间:  "+(System.currentTimeMillis()-beagin)+"ms");
//    }

}

注意:下面注释部分为传统写法
运行结果
01-23 19:39:09.579 13051-13051/com.haocai.aopdemo I/MainAspect: 摇一摇使用时间:   2018-01-23 19:39:09
01-23 19:39:12.589 13051-13051/com.haocai.aopdemo I/Main:  摇到一个红包
01-23 19:39:12.589 13051-13051/com.haocai.aopdemo I/MainAspect: 消耗时间:3001ms
01-23 19:39:12.589 13051-13051/com.haocai.aopdemo I/MainAspect: 语音:使用时间:   2018-01-23 19:39:12
01-23 19:39:15.599 13051-13051/com.haocai.aopdemo I/Main: 发语音:我要到一个红包啦
01-23 19:39:15.599 13051-13051/com.haocai.aopdemo I/MainAspect: 消耗时间:3000ms
01-23 19:39:15.599 13051-13051/com.haocai.aopdemo I/MainAspect: 打字:使用时间:   2018-01-23 19:39:15
01-23 19:39:18.609 13051-13051/com.haocai.aopdemo I/Main: 打字逻辑,我摇到了一个大红包
01-23 19:39:18.609 13051-13051/com.haocai.aopdemo I/MainAspect: 消耗时间:3000ms
源码下载
Github:https://github.com/kpioneer123/AopDemo

推荐阅读更多精彩内容