我們已經不用AOP做操作日誌了!

2020-12-11 酷扯兒

本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫

前言

用戶在操作我們系統的過程中,針對一些重要的業務數據進行增刪改查的時候,我們希望記錄一下用戶的操作行為,以便發生問題時能及時的找到依據,這種日誌就是業務系統的操作日誌。

本篇我們來探討下常見操作日誌的實現方案和可行性

常見的操作日誌類型

用戶登錄日誌重要數據查詢日誌 (但電商可能不重要的數據也做埋點,比如在淘寶上你搜索什麼商品,即使不買,一段時間內首頁也會給你推薦類似的東西)重要數據變更日誌 (如密碼變更,權限變更,數據修改等)數據刪除日誌......總結來說,就是重要的增刪改查根據業務的需要來做操作日誌的埋點。

實現方案對比

基於AOP(切面)傳統的實現方案

優點:實現思路簡單;缺點:增加資料庫的負擔,強依賴前端的傳參,不方便拓展,不支持批量操作,不支持多表關聯;基於資料庫Binlog

優點:解除了數據新舊變化的耦合,支持批量操作,方便多表關聯拓展,不依賴開發語言;缺點:資料庫表設計需要統一的約定;方案實現細節

一、基於AOP切面+註解的傳統方案

傳統的做法就是切面+註解的方式,這種對代碼的侵入性不強,通常記錄ip、業務模塊、操作帳號、操作場景、操作來源等等,一般在註解+攔截器裡這些值都拿得到,如下圖所示:

這種常見的我們在通用方法都可以處理,但是在數據變更方面,一直沒有較好的實現方式,比如數據在變更前是多少,變更後是多少。

以我們以前實現的一套方案來說,基於數據變更的記錄方式不僅要和需求方約定好模板(上百個欄位的不可能都做展示和記錄),也要和前端做一些約定,比如在修改之前的值是多少,修改後的值是多少,如下代碼客官請看:

@Valid @NotNull(message = "新值不能為空") @UpdateNewDataOperationLog private T newData; @Valid @NotNull(message = "舊值不能為空") @UpdateOldDataOperationLog private T oldData;

存在的問題:

1.舊值如果不多查詢一次資料庫則需要依賴前端把舊值封裝到oldData對象中,很有可能已經不是修改前的值;2.無法處理批量的List數據;3.不支持多表操作;再以一個場景為例,再刪除之前需要記錄刪除前的值,是不是還得再查一次~

@PostMapping("/delete") @ApiOperation(value = "刪除用戶信息", notes = "刪除用戶信息") @DeleteOperationLog(system = SystemNameNewEnum.SYS_JMS_LMDM, module = ModuleNameNewEnum.LMDM_AUTH, table = LogBaseTableNameEnum.TABLE_USER, methodName = "detail")

二、基於資料庫Binlog 方案

系統架構圖如下:

「主要分為3塊:」

1:業務應用 生成每次操作的traceid,並更新到操作的業務表中,發送1條業務消息,包含當前操作的操作人相關的信息;2:日誌收集應用 對業務日誌和轉換後的binlog日誌做整合,提供對外的日誌查詢搜索API;3:日誌處理應用利用canal採集和解析業務庫的binlog日誌並投遞到kafka中,解析後的記錄中記錄了當前操作的操作類型,如屬於刪除、修改、新增,和新舊值的記錄,格式如下:

{"data":[{"id":"122158992930664499","bill_type":"1","create_time":"2020-04-2609:15:13","update_time":"2020-04-2613:45:46","version":"2","trace_id":"exclude-f04ff706673d4e98a757396efb711173"}],"database":"yl_spmibill_8","es":1587879945200,"id":17161259,"isDdl":false,"mysqlType":{"id":"bigint(20)","bill_type":"tinyint(2)","create_time":"timestamp","update_time":"timestamp","version":"int(11)","trace_id":"varchar(50)"},"old":[{"update_time":"2020-04-2613:45:45","version":"1","trace_id":"exclude-36aef98585db4e7a98f9694c8ef28b8c"}],"pkNames":["id"],"sql":"","sqlType":{"id":-5,"bill_type":-6,"create_time":93,"update_time":93,"version":4,"trace_id":12},"table":"xxx_transfer_bill_117","ts":1587879945698,"type":"UPDATE"}

處理完binlon日誌轉換後的操作日誌,如下:

{ "id":"120716921250250776", "relevanceInfo":"XX0000097413282,", "remark":"籤收財務網點編碼由【】改為【380000】, 籤收網點名稱由【】改為【泉州南安網點】,籤收網點code由【】改為【2534104】,運單狀態code由【204】改為【205】,籤收財務網點名稱由【】改為【福建代理區】,籤收網點id由【0】改為【461】,籤收標識,1是,0否由【0】改為【1】,籤收時間由【null】改為【2020-04-24 21:09:47】,籤收財務網點id由【0】改為【400】,", "traceId":"120716921250250775" }

庫表設計

1:所有業務系統表需要添加trace_id欄位,每次操作生成一個隨機字符串並保存到業務表中;2:日誌收集應用庫表設計

CREATE TABLE `table_config` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id', `database_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '資料庫名', `table_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ' 資料庫表名', PRIMARY KEY (`id`), UNIQUE KEY `unq_data_name_table_name` (`database_name`,`table_name`) USING BTREE COMMENT '資料庫名表名聯合索引') ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='資料庫配置表';

CREATE TABLE `table_field_config` (`id` bigint(20) NOT NULL AUTO_INCREMENT, `table_config_id` bigint(20) DEFAULT NULL, `field` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '欄位 資料庫', `field_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '欄位 中文名稱', `enum_flag` tinyint(2) DEFAULT NULL COMMENT '是否枚舉欄位(1:是,0:否)', `relevance_flag` tinyint(2) DEFAULT NULL COMMENT '是否是關聯欄位(1:是,0否)', `sort` int(11) DEFAULT NULL COMMENT '排序', PRIMARY KEY (`id`), KEY `idx_table_config_id` (`table_config_id`) USING BTREE COMMENT '表ID索引') ENGINE=InnoDB AUTO_INCREMENT=2431 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='資料庫欄位配置表';

CREATE TABLE `table_field_value` (`id` bigint(20) NOT NULL, `field_config_id` bigint(20) DEFAULT NULL, `field_key` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT ' 枚舉', `filed_value` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '枚舉名稱', PRIMARY KEY (`id`), KEY `ids_field_config_id` (`field_config_id`) USING BTREE) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='數據字典配置表';

效果

image

基於binlog實現方案未來規劃

優化發送業務消息的實現,使用切面攔截減少對業務代碼的侵入;目前暫時不支持對多表關聯操作日誌記錄,需要拓展;總結

本文以操作日誌為題材討論了操作日誌的實現方案和可行性,並且都已經在功能上進行實現,其中使用aop方案也是大部分中小企業的首選實現方案,但是在一些金融領域以及erp相關系統,對操作日誌記錄明細要求極高,常見技術方案很難滿足,即使能夠滿足也會帶來一些代碼強侵入以及性能問題,所以我們又討論了基於binlog實現的方案,該方案雖然比對aop來說增強了技術的複雜性,但是對於有一定技術積累的團隊來說不算什麼難事,並且該方案我們都實現了上線,並且解決了代碼層面上的侵入,屬於跨語言級別的,相信對讀者還是有一定的啟發。

相關焦點

  • Spring AOP是什麼?你都拿它做什麼?
    我們知道Java是一個面向對象(OOP)的語言,但它有一些弊端,比如當我們需要為多個不具有繼承關係的對象引入一個公共行為,例如日誌、權限驗證、事務等功能時,只能在在每個對象裡引用公共行為。這樣做不便於維護,而且有大量重複代碼。AOP的出現彌補了OOP的這點不足。
  • spring AOP是什麼?你都拿它做什麼?
    我們知道java是一個面向對象(OOP)的語言,但它有一些弊端,比如當我們需要為多個不具有繼承關係的對象引入一個公共行為,例如日誌,權限驗證,事務等功能時,只能在在每個對象裡引用公共行為,這樣做不便於維護,而且有大量重複代碼。AOP的出現彌補了OOP的這點不足。 為了闡述清楚spring AOP,我們從將以下方面進行討論: 1.代理模式。
  • 聊聊AOP
    「AOP是編程中常用的一種方式,以Spring為例我們經常使用spring-aop組件和aspectJ的註解或者配置文件來完成對接口或類中的 某些方法的執行的攔截
  • 如何理解 Spring AOP 以及使用 AspectJ?
    AOP 即面向切面編程,也可以叫做面向方向編程,AOP不是一個新東西,它是OOP,即面向對象編程的一種補充,在當前已經成為一種成熟的編程方式。為啥要使用 AOP在學習AOP 之前,我們先了解下為啥我們要使用AOP?
  • Spring框架IOC和AOP簡介
    1、什麼是框架通常是指為了實現某個業界標準或完成特定基本任務的軟體組件規範,也指為了實現某個軟體組件規範時,提供規範所要求之基本功能的軟體產品2、Spring是什麼Spring是一個開源框架,為了解決企業應用開發的複雜性二而創建的,但現在已經不止應用於企業應用
  • 求求你,下次面試別再問我什麼是AOP了!
    手動3種方式ProxyFactory方式這種是硬編碼的方式,可以脫離spring直接使用,用到的比較多,自動化方式創建代理中都是依靠ProxyFactory來實現的,所以這種方式的原理大家一定要了解,上篇文章中已經有介紹過了,不清楚的可以去看一下:Spring系列第32篇:AOP核心源碼、原理詳解AspectJProxyFactory方式AspectJ是一個面向切面的框架
  • Spring5.0源碼學習系列之Spring AOP簡述
    Advice代碼放在一個類中,這個類就是切面類,切面類是跨多個類的關注點的模塊化類看了前面這些理論,您可能不是很理解,所以引用國外網站的圖例進行說明AOP概念,圖來自連結綜上所述,其實所謂的目的其實只是要鎖定在某個切入點(Pointcut)織入(Weaving)特定的增強邏輯(Advice)4、Spring AOP和AspectJ有了前面對AOP概念的概述之後,我們能夠大致理解
  • 詳解Spring框架的AOP機制
    2、 實現AOP案例代碼在實現AOP案例之前,需要確定項目已經引入了Spring框架關於AOP功能的Jar包。下面列出的是spring-aop-5.0版本,其它版本也可以。● spring-aop-5.0.8.RELEASE● spring-aspects-5.0.8.RELEASE另外還需要引入下面的Jar包:● aspectjrt● aspectjweaver在課程案例SpringProgram項目中,相關的Teacher實體類、EmailNotice業務類、Spring配置文件已經存在
  • Java 第一大框架:Spring 的 IoC 跟 AOP 雛形如何實現?
    抽象工廠、工廠方法設計模式」可以幫我們創建對象,「生成器模式」幫我們處理對象間的依賴關係,不也能完成這些功能嗎?可是這些又需要我們創建另一些工廠類、生成器類,我們又要而外管理這些類,增加了我們的負擔。所以用另外的方式,如果對象需要的時候,就自動地生成對象,不用再去創建。舉個例子:原來我們餓了,就出去吃飯,但是現在有了外賣之後,就可以訂餐了,我們可以把我們的需求告訴美團,讓他們給我們送飯。
  • 漫畫| Spring AOP的底層原理是什麼?
    >4、談談Spring如何配置聲明式事務控制聲明式事務管理有兩種常用的方式:基於tx和aop
  • 你知道Spring是怎麼將AOP應用到Bean的生命周期中的嗎?
    」在上篇文章中(Spring中AOP相關的API及源碼解析,原來AOP是這樣子的)我們已經分析過了AOP的實現的源碼,那麼Spring是如何將AOP應用到Bean的生命周期的呢?這篇文章就帶著大家來探究下這個問題。
  • 安卓架構師必備之Android AOP面向切面編程詳解,超實用!
    那幾個有箭頭的地方都是可以點擊進行頁面跳轉的,但是需要先判斷用戶是否登錄,如果已經登錄,則正常跳轉,如果沒有登錄,則跳轉到登錄頁面先登錄,但凡是有註冊,登錄的APP,這樣的操作,大家應該都很熟悉吧。一般情況下,我們的邏輯是這樣的...
  • 一個Spring AOP的坑!很多人都犯過!
    那麼,我們就來簡單回顧一下問題是怎麼樣的。最初我定義了一個註解,希望可以方便統一的對一些資料庫操作做緩存。@Componentpublic class StrategyService extends BaseStrategyService  {    public PricingResponse getFactor(Map<String, String> pricingParams) {        // 做一些參數校驗
  • Spring AOP看這篇就夠了
    來讓我們看一下上面的一個小故事和 AOP 到底有什麼對應關係. 首先我們知道, 在 Spring AOP 中 join point 指代的是所有方法的執行點, 而 point cut 是一個描述信息, 它修飾的是 join point, 通過 point cut, 我們就可以確定哪些 join point 可以被織入 Advice.
  • 嘉年華郵輪CEO:我們已經做了一切能做的
    全球最大的郵輪公司—嘉年華郵輪執行長阿諾德-唐納德(Arnold Donald)在會議上表示,已經盡力做了一切能做的了,非常為手下的員工自豪。唐納德表示,該公司旗下郵輪很快將在德國出海起航。隨著歐洲各邊境的逐漸開放,之後還會增加義大利等其他歐洲國家航線,但目前還暫不清楚何時美國航線可以復航。現在嘉年華郵輪面臨的最大的兩個難題,就是處理船隻和籌資資金。
  • 我們來做個好吃的蛋糕,不用烤箱,我們蒸著吃,做法簡單
    哈嘍大家好,我是一名熱愛美食,熱愛生活,熱愛分享的棟棟談美食,堅持每天分享健康美味的美食,今天給大家分享的美食:今天我們來做個好吃的蛋糕,不用烤箱,我們爭著吃,做法簡單,我們一起來看一下。這樣做的目的是為了防止麵粉起筋。翻拌到沒有乾麵粉,加入20克的純牛奶,繼續翻拌均勻,像這樣就可以了。準備三個小碗刷上一層食用油。把調好的麵糊倒入碗中。端起碗給它震動一下,排出裡面的氣泡,擺上一層洗乾淨的葡萄乾,然後放到蒸鍋裡,蓋上蓋子,水開以後蒸25分鐘,時間到了,現在可以出鍋了。做法簡單,喜歡吃蒸麵包的朋友趕快試一下。
  • 劉貴豪既然已經不用你們動手,我就勉為其難!
    劉貴豪既然已經不用你們動手,我就勉為其難!光頭強可不敢用自己的性命,來驗證楊永輝是不是在耍自己等人玩兒。問題是真要打斷了劉貴豪的手腳,那同樣是在用自己性命賭博啊。幾個大小混混互相看看,誰都沒有率先抬腳離開。光頭強咬了咬牙,正要開口,想要跟楊永輝求情。