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
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,460評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,067評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,467評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,468評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,184評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,582評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,616評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,794評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,343評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,096評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,291評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,863評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,513評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,941評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,190評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,026評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,253評論 2 375

推薦閱讀更多精彩內容