AopArms一款基於AOP的Android註解框架

前言

當下Java後端的SpringBoot微服務框架大火,緣由離不開註解的使用,其簡單易配置的註解方式使得更多的社區爲其編寫適用於SpringBoot的框架,也就是註解逐漸取代了傳統的xml配置方式。那麼註解在Android中也一樣的獲得了昇華,著名的框架有ButterKnife、 Dagger二、Retrofit等等。今天帶來一款Android中比較實用的註解框架AopArms,其用法簡單,裏面編寫了Android開發中經常使用的一套註解,如日誌、攔截(登陸)、異步處理、緩存、SP、延遲操做、定時任務、重試機制、try-catch安全機制、過濾頻繁點擊等,後續還會有更多更強大的註解功能加入。android

1、簡介

AOP(Aspect-Oriented Programming,面向切面編程),可謂是OOP(Object-Oriented Programing,面向對象編程)的補充和完善。AOP技術利用一種稱爲「橫切」的技術,剖解開封裝的對象內部,並將那些影響了多個類的公共行爲封裝到一個可重用模塊,並將其名爲「Aspect」,即方面。所謂「方面」,簡單地說,就是將那些與業務無關,卻爲業務模塊所共同調用的邏輯或責任封裝起來,便於減小系統的重複代碼,下降 模塊間的耦合度,並有利於將來的可操做性和可維護性。Android相關AOP經常使用的方法有 JNI HOOK 和 靜態織入,本文以靜態織入的方式之一AspectJ來進行講解使用。(在編譯期織入,切面直接以字節碼的形式編譯到目標字節碼文件中,這要求使用特殊的 Java 編譯器。)git

2、實踐

一、添加依賴
apply plugin: 'android-aspectjx'

dependencies {
    ...
    compile 'org.aspectj:aspectjrt:1.8.9'
}
複製代碼

項目跟目錄的gradle腳本中加入github

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        //此處推薦該庫,由滬江出品,可免去配置各類複雜任務
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
    }
}
複製代碼
二、AOP註解與使用
@Aspect:聲明切面,標記類
@Pointcut(切點表達式):定義切點,標記方法
@Before(切點表達式):前置通知,切點以前執行
@Around(切點表達式):環繞通知,切點先後執行
@After(切點表達式):後置通知,切點以後執行
@AfterReturning(切點表達式):返回通知,切點方法返回結果以後執行
@AfterThrowing(切點表達式):異常通知,切點拋出異常時執行
複製代碼

切點表達式的示例:編程

如:execution(@com.xxx.aop.TimeLog *.(..))
切點表達式的組成:
execution(@註解 訪問權限 返回值的類型 包名.函數名(參數))
複製代碼

下面簡單列舉兩個簡單例子:json

@Before("execution(@com.xxx.aop.activity * *(..))")
public void before(JoinPoint point) {
    Log.i(TAG, "method excute before...");
}
匹配activity包下的全部方法,在方法執行前輸出日誌
複製代碼
@Around("execution(@cn.com.xxx.Async * *(..))")
public void doAsyncMethod(ProceedingJoinPoint joinPoint) {
    Log.i(TAG, "method excute before...");
    joinPoint.proceed();
    Log.i(TAG, "method excute after...");
}
匹配帶有Async註解的方法,在方法執行先後分別輸出日誌
複製代碼
@Around("execution(@cn.com.xxx.Async * *(..)) && @annotation(async)")
public void doAsyncMethod(ProceedingJoinPoint joinPoint, Async async) {
    Log.i(TAG, "value>>>>>"+async.value());
    <!--to do somethings-->
    joinPoint.proceed();
    <!--to do somethings-->
}
//注意:@annotation(xxx)必須與下面的的參數值對應
匹配帶有Async註解的方法,並獲取註解中的值,去作相應處理。
複製代碼
三、實際應用舉例
場景1、利用註解實現緩存處理

一、定義註解Cache以下:後端

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Cache {

    String key(); //緩存的key

    int expiry() default -1; // 過時時間,單位是秒
}
複製代碼

二、編寫Aspect實現緩存

@Aspect
public class CacheAspect {
    private static final String POINTCUT_METHOD = "execution(@cn.com.xxx.annotation.Cache * *(..))";

    @Pointcut(POINTCUT_METHOD)
    public void onCacheMethod() {
    }

    @Around("onCacheMethod() && @annotation(cache)")
    public Object doCacheMethod(ProceedingJoinPoint joinPoint, Cache cache) throws Throwable {
        //獲取註解中的key
        String key = cache.key();
        //獲取註解中的過時時間
        int expiry = cache.expiry();
        //執行當前註解的方法(放行)
        Object result = joinPoint.proceed();
        //方法執行後進行緩存(緩存對象必須是方法返回值)
        ArmsCache aCache = ArmsCache.get(AopArms.getContext());
        if (expiry>0) {
            aCache.put(key,(Serializable)result,expiry);
        } else {
            aCache.put(key,(Serializable)result);
        }
        return result;
    }
}
複製代碼

此處引用ACache該項目中的緩存實現,僅一個Acache文件,我的以爲仍是比較好用的,固然你也能夠本身去實現。 三、測試安全

public static void main(String[] args) {
        initData();
        getUser();
    }
    
    //緩存數據
    @Cache(key = "userList")
    private ArrayList<User> initData() {
        ArrayList<User> list = new ArrayList<>();
        for (int i=0; i<5; i++){
            User user = new User();
            user.setName("艾神一不當心:"+i);
            user.setPassword("密碼:"+i);
            list.add(user);
        }
        return list;
    }
    
    //獲取緩存
    private void getUser() {
        ArrayList<User> users = ArmsCache.get(this).getAsList("userList", User.class);
        Log.e(TAG, "getUser: "+users);
    }
    
複製代碼
場景2、利用註解實現緩存移除

一、定義註解CacheEvict以下:bash

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CacheEvict {

    //須要移除的key
    String key();

    // 緩存的清除是否在方法以前執行, 默認表明緩存清除操做是在方法執行以後執行;若是出現異常緩存就不會清除
    boolean beforeInvocation() default false;
    
    //是否清空全部緩存
    boolean allEntries() default false;
}
複製代碼

二、編寫Aspect實現app

@Aspect
public class CacheEvictAspect {
    private static final String POINTCUT_METHOD = "execution(@cn.com.xxx.annotation.CacheEvict * *(..))";

    //切點位置,全部的CacheEvict處
    @Pointcut(POINTCUT_METHOD)
    public void onCacheEvictMethod() {
    }
    
    //環繞處理,並拿到CacheEvict註解值
    @Around("onCacheEvictMethod() && @annotation(cacheEvict)")
    public Object doCacheEvictMethod(ProceedingJoinPoint joinPoint, CacheEvict cacheEvict) throws Throwable {
        String key = cacheEvict.key();
        boolean beforeInvocation = cacheEvict.beforeInvocation();
        boolean allEntries = cacheEvict.allEntries();
        ArmsCache aCache = ArmsCache.get(AopArms.getContext());
        Object result = null;
        if (allEntries){
            //若是是所有清空,則key不須要有值
            if (!TextUtils.isEmpty(key))
                throw new IllegalArgumentException("Key cannot have value when cleaning all caches");
            aCache.clear();
        }
        if (beforeInvocation){
            //方法執行前,移除緩存
            aCache.remove(key);
            result = joinPoint.proceed();
        }else {
            //方法執行後,移除緩存,若是出現異常緩存就不會清除(推薦)
            result = joinPoint.proceed();
            aCache.remove(key);
        }
        return result;
    }
}
複製代碼

三、測試

public static void main(String[] args) {
        removeUser();
        getUser();
    }
    
    //移除緩存數據
    @CacheEvict(key = "userList")
    private void removeUser() {
        Log.e(TAG, "removeUser: >>>>");
    }
    
    //獲取緩存
    private void getUser() {
        ArrayList<User> users = ArmsCache.get(this).getAsList("userList", User.class);
        Log.e(TAG, "getUser: "+users);
    }
複製代碼

能夠看到執行結果:

3、AopArms的使用

一、引入方式

一、在主工程中添加依賴

//引入aspectjx插件
apply plugin: 'android-aspectjx'

dependencies {
    ...
    implementation 'cn.com.superLei:aop-arms:1.0.3'
}
複製代碼

二、項目跟目錄的gradle腳本中加入

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        //該庫基於滬江aspect插件庫
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.4'
    }
}

複製代碼

三、在Application中初始化

AopArms.init(this);
複製代碼

二、基本使用

一、緩存篇(可緩存任意類型)

一、插入緩存
     /**
     * key:緩存的鍵
     * expiry:緩存過時時間,單位s
     * @return 緩存的值
     */
    @Cache(key = "userList", expiry = 60 * 60 * 24)
    private ArrayList<User> initData() {
        ArrayList<User> list = new ArrayList<>();
        for (int i=0; i<5; i++){
            User user = new User();
            user.setName("艾神一不當心:"+i);
            user.setPassword("密碼:"+i);
            list.add(user);
        }
        return list;
    }
    
二、獲取緩存
    private ArrayList<User> getUser() {
        return ArmsCache.get(this).getAsList("userList", User.class);
    }

三、移除緩存
   /**
     * key:緩存的鍵
     * beforeInvocation:緩存的清除是否在方法以前執行, 若是出現異常緩存就不會清除   默認false
     * allEntries:是否清空全部緩存(與key互斥)  默認false
     */
    @CacheEvict(key = "userList", beforeInvocation = true, allEntries = false)
    public void removeUser() {
        Log.e(TAG, "removeUser: >>>>");
    }
    
複製代碼

二、SharedPreferences篇(可保存對象)

一、保存key到sp
    @Prefs(key = "article")
    private Article initArticle() {
        Article article = new Article();
        article.author = "jerry";
        article.title = "hello android";
        article.createDate = "2019-05-31";
        article.content = "this is a test demo";
        return article;
    }
    
二、從sp中移除key
    /**
     * key:sp的鍵
     * allEntries:是否清空全部存儲(與key互斥)  默認false
     */
    @PrefsEvict(key = "article", allEntries = false)
    public void removeArticle() {
        Log.e(TAG, "removeArticle: >>>>");
    }
    
三、經過key從sp中獲取value
    public void getArticle() {
        Article article = ArmsPreference.get(this, "article", null);
        Log.e(TAG, "getArticle: "+article);
    }    
複製代碼

三、異步篇

@Async
    public void asyn() {
        Log.e(TAG, "useAync: "+Thread.currentThread().getName());
    }
複製代碼

四、try-catch安全機制篇

//自動幫你try-catch   容許你定義回調方法
    @Safe(callBack = "throwMethod")
    public void safe() {
        String str = null;
        str.toString();
    }
    
    //自定義回調方法(注意要和callBack的值保持一致)
    private void throwMethod(Throwable throwable){
        Log.e(TAG, "throwMethod: >>>>>"+throwable.toString());
    }
複製代碼

五、重試機制篇

/**
     * @param count 重試次數
     * @param delay 每次重試的間隔
     * @param asyn 是否異步執行
     * @param retryCallback 自定義重試結果回調
     * @return 當前方法是否執行成功
     */
    @Retry(count = 3, delay = 1000, asyn = true, retryCallback = "retryCallback")
    public boolean retry() {
        Log.e(TAG, "retryDo: >>>>>>"+Thread.currentThread().getName());
        return false;
    }
    
    private void retryCallback(boolean result){
        Log.e(TAG, "retryCallback: >>>>"+result);
    }
複製代碼

六、定時任務篇

/**
     * @param interval 初始化延遲
     * @param interval 時間間隔
     * @param timeUnit 時間單位
     * @param count 執行次數
     * @param taskExpiredCallback 定時任務到期回調
     */
    @Scheduled(interval = 1000L, count = 10, taskExpiredCallback = "taskExpiredCallback")
    public void scheduled() {
        Log.e(TAG, "scheduled: >>>>");
    }
    
    private void taskExpiredCallback(){
        Log.e(TAG, "taskExpiredCallback: >>>>");
    }
複製代碼

七、延遲任務篇

//開啓延遲任務(10s後執行該方法)
    @Delay(key = "test", delay = 10000L)
    public void delay() {
        Log.e(TAG, "delay: >>>>>");
    }
    
    //移除延遲任務
    @DelayAway(key = "test")
    public void cancelDelay() {
        Log.e(TAG, "cancelDelay: >>>>");
    }
複製代碼

八、過濾頻繁點擊

//value默認500ms
    @SingleClick(value = 2000L)
    private void onclick(){
        Log.e(TAG, "onclick: >>>>");
    }
複製代碼

九、攔截篇(如登陸)

一、在須要進行攔截的方法添加註解
    @Intercept("login_intercept")
    public void loginIntercept() {
        Log.e(TAG, "intercept: 已登錄>>>>");
    }
二、(建議,統一處理)在Application中進行進行監聽攔截回調
public class MyApplication extends Application {

    private static final String TAG = "MyApplication";
    private static MyApplication mApplication;
    
    @Override
    public void onCreate() {
        super.onCreate();
        mApplication = this;
        AopArms.init(this);
        AopArms.setInterceptor(new Interceptor() {
            @Override
            public boolean intercept(String key, String methodName) throws Throwable {
                Log.e(TAG, "intercept methodName:>>>>>"+methodName);
                if ("login_intercept".equals(key)){
                    String userId = ArmsPreference.get(mApplication, "userId", "");
                    if (TextUtils.isEmpty(userId)){
                        Toast.makeText(mApplication, "您尚未登陸", Toast.LENGTH_SHORT).show();
                        return true;//表明攔截
                    }
                }
                return false;//放行
            }
        });
    }
}

複製代碼

十、動態受權篇

一、開啓請求權限
    /**
     * @param value 權限值
     * @param rationale 拒絕後的下一次提示(開啓後,拒絕後,下一次會先提示該權限申請提示語)
     * @param requestCode 權限請求碼標識
     */
    @Permission(value = {Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, rationale = "爲了更好的體驗,請打開相關權限")
    public void permission(View view) {
        Log.e(TAG, "permission: 權限已打開");
    }

    二、請求拒絕註解回調
    @PermissionDenied
    public void permissionDenied(int requestCode, List<String> denyList){
        Log.e(TAG, "permissionDenied: "+requestCode);
        Log.e(TAG, "permissionDenied>>>: "+denyList.toString());
    }

    三、請求拒絕且不在提示註解回調
    @PermissionNoAskDenied
    public void permissionNoAskDenied(int requestCode, List<String> denyNoAskList){
        Log.e(TAG, "permissionNoAskDenied: "+requestCode);
        Log.e(TAG, "permissionNoAskDenied>>>: "+denyNoAskList.toString());
        //前往設置頁面打開權限
        AopPermissionUtils.showGoSetting(this, "爲了更好的體驗,建議前往設置頁面打開權限");
    }
複製代碼

十一、測試方法耗時篇

@TimeLog
    public void getUser(View view) {
        ArrayList<User> users = ArmsCache.get(this).getAsList("userList", User.class);
        Log.e(TAG, "getUser: " + users);
    }
複製代碼

以上是庫的一些經常使用的基本用法,後續會添加更多的註解來簡化Android開發,歡迎前來issues來提問或者提出你認爲所須要的更多註解需求。

GitHub地址:AopArms

相關文章
相關標籤/搜索