【Android】AOP 面向切面程式設計(一) AspectJ 處理網路錯誤

指間沙似流年發表於2017-12-23

什麼是AOP,與OOP的區別

OOP: (Object Oriented Programming) 物件導向的程式設計。所謂“物件”在顯式支援物件導向的語言中,一般是指類在記憶體中裝載的例項,具有相關的成員變數和成員函式(也稱為:方法)。

AOP: (Aspect Oriented Programming) 面向切面程式設計。是目前軟體開發中的一個熱點,也是Spring框架中容。利用AOP可以對業務邏輯的各個部分進行隔離,從而使得業務邏輯各部分之間的耦合度降低,提高程式的可重用性,同時提高了開發的效率。

[圖片上傳失敗...(image-926a06-1512701224758)]

AOP的適用範圍

埋點,日誌記錄,效能統計,安全控制,事務處理,異常處理等等。

AOP方式

  1. 程式碼預編譯 -- AspectJ
  2. 執行期動態代理

AspectJ介紹

AspectJ是一個面向切面的框架,它擴充套件了Java語言。AspectJ定義了AOP語法,所以它有一個專門的編譯器用來生成遵守Java位元組編碼規範的Class檔案。

AspectJ概念

  1. pointcut

    是一個(組)基於正規表示式的表示式,有點繞,就是說他本身是一個表示式,但是他是基於正則語法的。通常一個pointcut,會選取程式中的某些我們感興趣的執行點,或者說是程式執行點的集合。 一個切入點通過一個普通的方法定義來提供,並且切入點表示式使用@Pointcut註解來宣告,註解的方法返回型別必須為void型。 要建立複雜的切入點表示式,可以通過&&、||和!進行組合,也可以通過名字引用切入點表示式。

    // 匹配所有com.fastaoe.aspectjdemo包下以test結尾的類的方法都會被執行
    @Pointcut("execution(*com.fastaoe.aspectjdemo.*test * *(..))")  
    public void pointcut1(){}  
    
    // 匹配所有com.fastaoe.aspectjdemo.biz包下所有的類
    @Pointcut("within(com.fastaoe.aspectjdemo.biz.*)")  
    public void pointcut2(){} 
    
    // 同時匹配2個方法
    @Pointcut("pointcut1()&&pointcut2()")  
    private void tradingOperation(){} 
    複製程式碼

    aop_pointcut

  2. joinPoint

    通過pointcut選取出來的集合中的具體的一個執行點,我們就叫JoinPoint.

  3. Advice

    在選取出來的JoinPoint上要執行的操作、邏輯。關於5種型別,我不多說,不懂的同學自己補基礎。

    • Before Advice:@Before
    • After returning advice:@AfterReturning,可在通知體內得到返回的實際值;
    • After throwing advice:@AfterThrowing
    • After (finally) advice : @After 最終通知必須準備處理正常和異常兩種返回情況,它通常用於釋放資源。
    • Around advice : @Around 環繞通知使用@Around註解來宣告,通知方法的第一個引數必須是ProceedingJoinPoint型別,在通知內部呼叫ProceedingJoinPoint的Proceed()方法會導致執行真正的方法,傳入一個Object[]物件,陣列中的值將被作為一個引數傳遞給方法。
  4. aspect

    就是我們關注點的模組化。這個關注點可能會橫切多個物件和模組,事務管理是橫切關注點的很好的例子。它是一個抽象的概念,從軟體的角度來說是指在應用程式不同模組中的某一個領域或方面。又pointcut和advice組成。

  5. Target

    被aspectj橫切的物件。我們所說的joinPoint就是Target的某一行,如方法開始執行的地方、方法類呼叫某個其他方法的程式碼。

AspectJ例子

一般情況下,如果我們需要在獲取網路資料的時候需要判斷網路是否存在,如果不存在,Toast提示使用者的話,程式碼是這樣的。

public void getNetData1() {
   if (isNetworkAvailable(this)) {
       Toast.makeText(this, "開始獲取新的網路資訊1", Toast.LENGTH_LONG).show();
   } else {
       Toast.makeText(this,"請檢查您的網路",Toast.LENGTH_LONG).show();
   }
}

public void getNetData2() {
   if (isNetworkAvailable(this)) {
       Toast.makeText(this, "開始獲取新的網路資訊2", Toast.LENGTH_LONG).show();
   } else {
       Toast.makeText(this,"請檢查您的網路",Toast.LENGTH_LONG).show();
   }
}

/**
* 檢查當前網路是否可用
*
* @return
*/
private static boolean isNetworkAvailable(Context context) {
   ConnectivityManager connectivityManager = (ConnectivityManager)
           context.getSystemService(Context.CONNECTIVITY_SERVICE);
   if (connectivityManager != null) {
       NetworkInfo[] networkInfo = connectivityManager.getAllNetworkInfo();

       if (networkInfo != null && networkInfo.length > 0) {
           for (int i = 0; i < networkInfo.length; i++) {
               if (networkInfo[i].getState() == NetworkInfo.State.CONNECTED) {
                   return true;
               }
           }
       }
   }
   return false;
}
複製程式碼

如果只有幾個這樣的程式碼段維護還是比較簡單的,但是如果在整個專案中有非常多的網路請求,如果都是這樣的方式來判斷網路是否可用的話,這就是非常痛苦的事情。 並且當產品的需求改變的時候,每個程式碼段都需要修改,很有可能修改的時候會出現修改不完全有遺漏的地方,並且迴歸測試的時候對於測試人員來說也是非常痛苦的事情。 所以我們需要其他的方式來改變這個現狀,其中比較好的方式就是AOP。

  1. 引入aspectjrt.jar

    aspectj-downloads

    下載完成之後新增到app的libs中,並在build.gradle引入

    compile files('libs/aspectjrt.jar')
    複製程式碼
  2. 在build.gradle(app)中新增

    import org.aspectj.bridge.IMessage
    import org.aspectj.bridge.MessageHandler
    import org.aspectj.tools.ajc.Main
    
    buildscript {
        repositories {
            mavenCentral()
        }
        dependencies {
            classpath 'org.aspectj:aspectjtools:1.8.9'
            classpath 'org.aspectj:aspectjweaver:1.8.9'
        }
    }
    
    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;
                }
            }
        }
    }
    
    複製程式碼
  3. 新增註解

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.METHOD)
    public @interface CheckNet {
    }
    複製程式碼
  4. 建立AspectJ檔案

    1. 需要在類上引用Aspect註解
    2. @Pointcut("execution(@com.fastaoe.aspectjdemo.CheckNet * *(..))") - 定義切入點,也就是需要處理的方法,切入點的內容是一個表示式,來描述切入哪些物件的哪些方法,("excute (*add*(..))")切入點表達表示將要切入所有以add開頭的方法,該方法可帶任意個數的引數
    3. @Around("checkNetBehavior()") - 定義處理的核心方法
    @Aspect
    public class SectionAspect {
    
        @Pointcut("execution(@com.fastaoe.aspectjdemo.CheckNet * *(..))")
        public void checkNetBehavior() {
    
        }
    
        @Around("checkNetBehavior()")
        public Object checkNet(ProceedingJoinPoint joinPoint) throws Throwable {
            Log.d("SectionAspect", "checkNetStart");
            MethodSignature signature = (MethodSignature) joinPoint.getSignature();
            CheckNet annotation = signature.getMethod().getAnnotation(CheckNet.class);
            if (annotation != null) {
                Object object = joinPoint.getThis();
                Context context = getContext(object);
                if (context != null) {
                    if (!isNetworkAvailable(context)) {
                        Toast.makeText(context,"請檢查您的網路",Toast.LENGTH_LONG).show();
                        return null;
                    }
                }
            }
            return joinPoint.proceed();
        }
    
        /**
         * 通過物件獲取上下文
         *
         * @param object
         * @return
         */
        private Context getContext(Object object) {
            if (object instanceof Activity) {
                return (Activity) object;
            } else if (object instanceof Fragment) {
                Fragment fragment = (Fragment) object;
                return fragment.getActivity();
            } else if (object instanceof View) {
                View view = (View) object;
                return view.getContext();
            }
            return null;
        }
    
        /**
         * 檢查當前網路是否可用
         *
         * @return
         */
        private static boolean isNetworkAvailable(Context context) {
            ConnectivityManager connectivityManager = (ConnectivityManager)
                    context.getSystemService(Context.CONNECTIVITY_SERVICE);
            if (connectivityManager != null) {
                NetworkInfo[] networkInfo = connectivityManager.getAllNetworkInfo();
    
                if (networkInfo != null && networkInfo.length > 0) {
                    for (int i = 0; i < networkInfo.length; i++) {
                        if (networkInfo[i].getState() == NetworkInfo.State.CONNECTED) {
                            return true;
                        }
                    }
                }
            }
            return false;
        }
    }
    複製程式碼
  5. 使用註解

    @CheckNet
    private void getNetData() {
    	Toast.makeText(this, "開始獲取新的網路資訊", Toast.LENGTH_LONG).show();
    }
    複製程式碼

相關文章