AOP實現(xiàn)的操作日志記錄

需求

產(chǎn)品有個需求是記錄用戶在APP的操作抑党,比如切換頁簽到主頁:記錄一條日志 function:“切換主頁”,比如上傳了一份文件記錄:function:上傳文件撵摆,type:MP4底靠,size:34M等,最后統(tǒng)計操作的總數(shù)分析用戶習(xí)慣特铝。先不說這個統(tǒng)計有價值沒暑中,看下如何在android上如何實現(xiàn)。

簡單實現(xiàn):直接封裝一個LogService類負責上傳日志鲫剿,在每個需要的地方組裝日志數(shù)據(jù)鳄逾,然后調(diào)用logservice.send(log),這種方法對功能代碼有很大的破壞性灵莲,隨著版本迭代會難以維護雕凹。

好點的辦法就是開發(fā)者無感,將所有的日志記錄放到一個地方處理政冻,這種思想就是切面編程簡稱AOP枚抵。AOP是一種方法論,JAVA里面的OOP(面向?qū)ο缶幊?明场,精髓是把功能或問題模塊化汽摹,每個模塊處理自己的家務(wù)事。但在現(xiàn)實世界中苦锨,并不是所有問題都能完美得劃分到模塊中逼泣。如果說嫌套,OOP如果是把問題劃分到單個模塊的話,那么AOP就是把涉及到眾多模塊的某一類問題進行統(tǒng)一管理圾旨。比如我們可以設(shè)計一個Aspects,管理某個軟件中所有模塊的日志輸出的功能魏蔗。

@Aspect實現(xiàn)切面記錄操作日志

用到的開源庫:https://github.com/uPhyca/gradle-android-aspectj-plugin

Project的build.gradle 配置:classpath 'com.uphyca.gradle:gradle-android-aspectj-plugin:0.9.14'
Moduler的build.gradle配置:apply plugin: 'com.uphyca.android-aspectj'

基本流程就是在某處定義一個空方法砍的,方法參數(shù)是我們需要上傳服務(wù)器的參數(shù)愈案。比如某個界面CompInfoFragment

    private void collectLog(String function, boolean isSucc, long ppid, long finishOpenTime, long startOpenTime) {
    }

然后在需要記錄的地方調(diào)用這個方法: 比如用戶點擊back鍵需要記錄一條日志

接下來意鲸,我需要找到這個方法的位置,并獲取方法里面的參數(shù)怎囚。定義一個日志類加上@Aspect注解谣旁,在里面定義一個切點床佳,并且在這個切點前執(zhí)行日志的記錄:


image.png

1是定義切點:通過類名+方法名模糊匹配到之前定義的空方法
2是切點名稱
3是切點時機:這里在這個切點(也就是空方法執(zhí)行)前,插入我們的操作日志記錄榄审,注意這里的方法參數(shù)名稱必須和空方法一致砌们,并且不能是對象!不能是enum搁进,只能是基本數(shù)據(jù)類型浪感。
4是真正的錯日志記錄,獲取到業(yè)務(wù)層傳過來的參數(shù)饼问,組裝日志上傳(后面再講)影兽。
在一個日志類里面可以定義多個切點:

@Aspect
public class AttrGroup {
    @Pointcut("execution(* *..CompInfoFragment.collectLog(..))")
    public void logForFunction(){};

    @Before("logForFunction() && args(function,isSucc,ppid,finishOpenTime,startOpenTime)")
    public void log(JoinPoint joinPoint,String function, boolean isSucc,long ppid,long finishOpenTime,long startOpenTime){
        collectLog(function,isSucc,ppid,finishOpenTime,startOpenTime);
    }

    @Pointcut("execution(* *..CompInformationActivity.collectLog(..))")
    public void logForFunction2(){};

    @Before("logForFunction2() && args(ppid,position,enterType)")
    public void log2(JoinPoint joinPoint,int ppid,int position,int enterType){
        collectLog2(ppid,position,enterType);
    }

    private void collectLog(String function, boolean isSucc,long ppid,long finishOpenTime,long startOpenTime) {
        List<Integer> ppids = new ArrayList<Integer>();
        List<SendLogInfoEvent.LogBean> extendInfos = new ArrayList<SendLogInfoEvent.LogBean>();
        SendLogInfoEvent.BVUserOperLog log = new SendLogInfoEvent.BVUserOperLog();
        log.function = function;
        //省略 日志的組裝
        BVLogService.getInstance().addLog(log);
    }

    private void collectLog2(int ppid,int position,int enterType){
        List<Integer> ppids = new ArrayList<Integer>();
        List<SendLogInfoEvent.LogBean> extendInfos = new ArrayList<SendLogInfoEvent.LogBean>();
        SendLogInfoEvent.BVUserOperLog log = new SendLogInfoEvent.BVUserOperLog();
        //省略 日志的組裝
        BVLogService.getInstance().addLog(log);
    }
}

這樣在業(yè)務(wù)層只需要插入一個空方法,真正的日志記錄全部封裝到一個地方管理莱革。同時峻堰,根據(jù)不同的業(yè)務(wù)把日志類分類,不要把所有的切點放到一個類:


image.png

每個類負責維護一個業(yè)務(wù)的日志記錄盅视。

日志記錄類的封裝

上面實現(xiàn)了對操作日志切點的捕捉捐名,下面的工作就是上傳這條日志。用戶點擊一次就上傳一條顯然不合理闹击,為了提高效率可以開啟工作線程收集日志桐筏,滿5條一起上傳。并且while循環(huán)10秒鐘檢測一次拇砰,有日志立刻上傳梅忌。

public class LogService {
    private static LogService _instance = new LogService();
    private LogService.LogerSubmitter submitter;

    protected LogService() {
        this.init();
    }

    public static LogService getInstance() {
        return _instance;
    }

    public void init() {
        this.submitter = new LogService.LogerSubmitter();
        this.submitter.start();
    }

    public void addLog(BVUserOperLog info) {
        if(this.submitter != null) {
            this.submitter.addLog(info);
        }

    }

    public void clearAllLog() {
        if(this.submitter != null) {
            this.submitter.clearLog();
        }

    }

    public void destory() {
        if(this.submitter != null) {
            this.submitter.notifyExit();
            this.submitter = null;
        }

    }

    public interface IAddOperLog {
        @POST("rs/log/addOperLog")
        @ServerName("pdscommon")
        Call<ResponseBody> addOperLog(@Body List<BVUserOperLog> var1);
    }

    private class LogerSubmitter extends Thread {
        protected boolean exitFlag = false;
        protected Event event = new Event();
        protected Queue<BVUserOperLog> queue = new ConcurrentLinkedQueue();
        private static final long COMMIT_PERIOD = 10000L;
        private static final int MAX_LOG_SIZE = 5;

        public LogerSubmitter() {
        }

        public void addLog(BVUserOperLog info) {
            this.queue.add(info);
            if(this.queue.size() >= 5) {
                this.event.event_notifyAll();
            }

        }

        public void clearLog() {
            this.queue.clear();
        }

        public void notifyExit() {
            this.exitFlag = true;
            this.event.event_notifyAll();
        }

        public void run() {
            while(true) {
                if(!this.exitFlag) {
                    ArrayList lst = new ArrayList();

                    while(!this.queue.isEmpty()) {
                        lst.add(this.queue.poll());
                    }

                    this.sendLogNow(lst);
                    if(!this.exitFlag) {
                        this.event.event_wait(10000L);
                        continue;
                    }
                }

                return;
            }
        }

        protected void sendLogNow(List<BVUserOperLog> lst) {
            if(lst != null && !lst.isEmpty()) {
                Method m = LbRestMethod.getMethod(LogService.IAddOperLog.class, "addOperLog", List.class);
                LbRestMethodProxy.callMethodV2(LogService.IAddOperLog.class, lst, m);
            }
        }

        protected MethodResponse callMethodLog(Class<?> service, Object methodParam, Method method) {
            LbRestMethod rm = LbRestMethodProxy.MakeLbRestMethod(method, new LbExceptionHandler());
            MethodResponse res = rm.run(service, methodParam, method);
            return res;
        }
    }
}

至于BVUserOperLog 日志實體怎么定義,根據(jù)需求和 服務(wù)端約定吧除破。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末牧氮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子瑰枫,更是在濱河造成了極大的恐慌踱葛,老刑警劉巖丹莲,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異尸诽,居然都是意外死亡甥材,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進店門性含,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洲赵,“玉大人,你說我怎么就攤上這事商蕴〉迹” “怎么了?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵绪商,是天一觀的道長苛谷。 經(jīng)常有香客問我,道長格郁,這世上最難降的妖魔是什么腹殿? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮例书,結(jié)果婚禮上赫蛇,老公的妹妹穿的比我還像新娘。我一直安慰自己雾叭,他們只是感情好悟耘,可當我...
    茶點故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著织狐,像睡著了一般暂幼。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上移迫,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天旺嬉,我揣著相機與錄音,去河邊找鬼厨埋。 笑死邪媳,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的荡陷。 我是一名探鬼主播雨效,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼废赞!你這毒婦竟也來了徽龟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤唉地,失蹤者是張志新(化名)和其女友劉穎据悔,沒想到半個月后传透,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡极颓,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年朱盐,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片菠隆。...
    茶點故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡兵琳,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出浸赫,到底是詐尸還是另有隱情,我是刑警寧澤赃绊,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布既峡,位于F島的核電站,受9級特大地震影響碧查,放射性物質(zhì)發(fā)生泄漏运敢。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一忠售、第九天 我趴在偏房一處隱蔽的房頂上張望传惠。 院中可真熱鬧,春花似錦稻扬、人聲如沸卦方。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盼砍。三九已至,卻和暖如春逝她,著一層夾襖步出監(jiān)牢的瞬間浇坐,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工黔宛, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留近刘,地道東北人。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓臀晃,卻偏偏與公主長得像觉渴,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子徽惋,可洞房花燭夜當晚...
    茶點故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容