主頁 > 軟體設計 > Springboot-注解-通用操作日志組件

Springboot-注解-通用操作日志組件

2021-02-11 12:59:38 軟體設計

Springboot-注解-通用操作日志組件

目錄

  • Springboot-注解-通用操作日志組件
    • 使用方式
      • 基本使用
        • maven依賴添加SDK依賴
        • SpringBoot入口打開開關,添加 @EnableLogRecord 注解
        • 日志埋點
            • 1. 普通的記錄日志
            • 2. 期望記錄失敗的日志, 如果拋出例外則記錄fail的日志,沒有拋出記錄 success 的日志
            • 3. 日志支持種類
            • 4. 支持記錄操作的詳情或者額外資訊
            • 5. 如何指定操作日志的操作人是什么? 框架提供了兩種方法
            • 6. 日志文案調整
            • 7. 日志文案調整 使用 SpEL 三目運算式
        • 框架的擴展點
        • 變數相關
        • 帶擴展
        • 注意點:
    • 原始碼
    • Author

此組件解決的問題是:
「誰」在「什么時間」對「什么」做了「什么事」

本組件目前針對 Spring-boot 做了 Autoconfig,如果是 SpringMVC,也可自己在 xml 初始化 bean

使用方式

基本使用

maven依賴添加SDK依賴

        <dependency>
          <groupId>io.github.mouzt</groupId>
          <artifactId>bizlog-sdk</artifactId>
          <version>1.0.0</version>
        </dependency>

SpringBoot入口打開開關,添加 @EnableLogRecord 注解

tenant是代表租戶的標識,一般一個服務或者一個業務下的多個服務都寫死一個 tenant 就可以

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableTransactionManagement
@EnableLogRecord(tenant = "com.mzt.test")
public class Main {

    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}

日志埋點

1. 普通的記錄日志
  • pefix:是拼接在 bizNo 上作為 log 的一個標識,避免 bizNo 都為整數 ID 的時候和其他的業務中的 ID 重復,比如訂單 ID、用戶 ID 等
  • bizNo:就是業務的 ID,比如訂單ID,我們查詢的時候可以根據 bizNo 查詢和它相關的操作日志
  • success:方法呼叫成功后把 success 記錄在日志的內容中
  • SpEL 運算式:其中用雙大括號包圍起來的(例如:{{#order.purchaseName}})#order.purchaseName 是 SpEL運算式,Spring中支持的它都支持的,比如呼叫靜態方法,三目運算式,SpEL 可以使用方法中的任何引數
  @LogRecordAnnotation(success = "{{#order.purchaseName}}下了一個訂單,購買商品「{{#order.productName}}」,下單結果:{{#_ret}}",
              prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
  public boolean createOrder(Order order) {
      log.info("【創建訂單】orderNo={}", order.getOrderNo());
      // db insert order
      return true;
  }

此時會列印操作日志 “張三下了一個訂單,購買商品「超值優惠紅燒肉套餐」,下單結果:true”

2. 期望記錄失敗的日志, 如果拋出例外則記錄fail的日志,沒有拋出記錄 success 的日志
    @LogRecordAnnotation(
            fail = "創建訂單失敗,失敗原因:「{{#_errorMsg}}」",
            success = "{{#order.purchaseName}}下了一個訂單,購買商品「{{#order.productName}}」,下單結果:{{#_ret}}",
            prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
    public boolean createOrder(Order order) {
        log.info("【創建訂單】orderNo={}", order.getOrderNo());
        // db insert order
        return true;
    }

其中的 #_errorMsg 是取的方法拋出例外后的例外的 errorMessage,

3. 日志支持種類

比如一個訂單的操作日志,有些操作日志是用戶自己操作的,有些操作是系統運營人員做了修改產生的操作日志,我們系統不希望把運營的操作日志暴露給用戶看到,
但是運營期望可以看到用戶的日志以及運營自己操作的日志,這些操作日志的bizNo都是訂單號,所以為了擴展添加了型別欄位,主要是為了對日志做分類,查詢方便,支持更多的業務,

    @LogRecordAnnotation(
            fail = "創建訂單失敗,失敗原因:「{{#_errorMsg}}」",
            category = "MANAGER",
            success = "{{#order.purchaseName}}下了一個訂單,購買商品「{{#order.productName}}」,下單結果:{{#_ret}}",
            prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
    public boolean createOrder(Order order) {
        log.info("【創建訂單】orderNo={}", order.getOrderNo());
        // db insert order
        return true;
    }
4. 支持記錄操作的詳情或者額外資訊

如果一個操作修改了很多欄位,但是success的日志模版里面防止過長不能把修改詳情全部展示出來,這時候需要把修改的詳情保存到 detail 欄位,
detail 是一個 String ,需要自己序列化,這里的 #order.toString() 是呼叫了 Order 的 toString() 方法,
如果保存 JSON,自己重寫一下 Order 的 toString() 方法就可以,

 @LogRecordAnnotation(
            fail = "創建訂單失敗,失敗原因:「{{#_errorMsg}}」",
            category = "MANAGER_VIEW",
            detail = "{{#order.toString()}}",
            success = "{{#order.purchaseName}}下了一個訂單,購買商品「{{#order.productName}}」,下單結果:{{#_ret}}",
            prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
    public boolean createOrder(Order order) {
        log.info("【創建訂單】orderNo={}", order.getOrderNo());
        // db insert order
        return true;
    }
5. 如何指定操作日志的操作人是什么? 框架提供了兩種方法
  • 第一種:手工在LogRecord的注解上指定,這種需要方法引數上有operator
    @LogRecordAnnotation(
            fail = "創建訂單失敗,失敗原因:「{{#_errorMsg}}」",
            category = "MANAGER_VIEW",
            detail = "{{#order.toString()}}",
            operator = "{{#currentUser}}",
            success = "{{#order.purchaseName}}下了一個訂單,購買商品「{{#order.productName}}」,下單結果:{{#_ret}}",
            prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")
    public boolean createOrder(Order order, String currentUser) {
        log.info("【創建訂單】orderNo={}", order.getOrderNo());
        // db insert order
        return true;
    }

這種方法手工指定,需要方法引數上有 operator 引數,或者通過 SpEL 呼叫靜態方法獲取當前用戶,

  • 第二種: 通過默認實作類來自動的獲取操作人,由于在大部分web應用中當前的用戶都是保存在一個執行緒背景關系中的,所以每個注解都加一個operator獲取操作人顯得有些重復勞動,所以提供了一個擴展介面來獲取操作人
    框架提供了一個擴展介面,使用框架的業務可以 implements 這個介面自己實作獲取當前用戶的邏輯,
    對于使用 Springboot 的只需要實作 IOperatorGetService 介面,然后把這個 Service 作為一個單例放到 Spring 的背景關系中,使用 Spring Mvc 的就需要自己手工裝配這些 bean 了,
@Configuration
public class LogRecordConfiguration {

    @Bean
    public IOperatorGetService operatorGetService() {
        return () -> Optional.of(OrgUserUtils.getCurrentUser())
                .map(a -> new OperatorDO(a.getMisId()))
                .orElseThrow(() -> new IllegalArgumentException("user is null"));
    }
}

//也可以這么搞:
@Service
public class DefaultOperatorGetServiceImpl implements IOperatorGetService {

    @Override
    public OperatorDO getUser() {
        OperatorDO operatorDO = new OperatorDO();
        operatorDO.setOperatorId("SYSTEM");
        return operatorDO;
    }
}
6. 日志文案調整

對于更新等方法,方法的引數上大部分都是訂單ID、或者產品ID等,
比如下面的例子:日志記錄的success內容是:“更新了訂單{{#orderId}},更新內容為…”,這種對于運營或者產品來說難以理解,所以引入了自定義函式的功能,
使用方法是在原來的變數的兩個大括號之間加一個函式名稱 例如 “{ORDER{#orderId}}” 其中 ORDER 是一個函式名稱,只有一個函式名稱是不夠的,需要添加這個函式的定義和實作,可以看下面例子
自定義的函式需要實作框架里面的IParseFunction的介面,需要實作兩個方法:

  • functionName() 方法就回傳注解上面的函式名;

  • apply()函式引數是 "{ORDER{#orderId}}"中SpEL決議的#orderId的值,這里是一個數字1223110,接下來只需要在實作的類中把 ID 轉換為可讀懂的字串就可以了,
    一般為了方便排查問題需要把名稱和ID都展示出來,例如:"訂單名稱(ID)"的形式,

這里有個問題:加了自定義函式后,框架怎么能呼叫到呢?
答:對于Spring boot應用很簡單,只需要把它暴露在Spring的背景關系中就可以了,可以加上Spring的 @Component 或者 @Service 很方便😄,Spring mvc 應用需要自己裝配 Bean,

    // 沒有使用自定義函式
    @LogRecordAnnotation(success = "更新了訂單{{#orderId}},更新內容為....",
            prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}",
            detail = "{{#order.toString()}}")
    public boolean update(Long orderId, Order order) {
        return false;
    }

    //使用了自定義函式,主要是在 {{#orderId}} 的大括號中間加了 functionName
    @LogRecordAnnotation(success = "更新了訂單ORDER{#orderId}},更新內容為...",
            prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}",
            detail = "{{#order.toString()}}")
    public boolean update(Long orderId, Order order) {
        return false;
    }

    // 還需要加上函式的實作
    @Component
    public class OrderParseFunction implements IParseFunction {
        @Resource
        @Lazy //為了避免類加載順序的問題 最好為Lazy,沒有問題也可以不加
        private OrderQueryService orderQueryService;
        
        @Override 
        public String functionName() {
            //  函式名稱為 ORDER
            return "ORDER";
        }
    
        @Override
        //這里的 value 可以吧 Order 的JSON物件的傳遞過來,然后反決議拼接一個定制的操作日志內容
        public String apply(String value) {
            if(StringUtils.isEmpty(value)){
                return value;
            }
            Order order = orderQueryService.queryOrder(Long.parseLong(value));
            //把訂單產品名稱加上便于理解,加上 ID 便于查問題
            return order.getProductName().concat("(").concat(value).concat(")");
        }
    }
7. 日志文案調整 使用 SpEL 三目運算式
    @LogRecordAnnotation(prefix = LogRecordTypeConstant.CUSTOM_ATTRIBUTE, bizNo = "{{#businessLineId}}",
            success = "{{#disable ? '停用' : '啟用'}}了自定義屬性{ATTRIBUTE{#attributeId}}")
    public CustomAttributeVO disableAttribute(Long businessLineId, Long attributeId, boolean disable) {
    	return xxx;
    }

框架的擴展點

  • 重寫OperatorGetServiceImpl通過背景關系獲取用戶的擴展,例子如下
@Service
public class DefaultOperatorGetServiceImpl implements IOperatorGetService {

    @Override
    public Operator getUser() {
         return Optional.ofNullable(UserUtils.getUser())
                        .map(a -> new Operator(a.getName(), a.getLogin()))
                        .orElseThrow(()->new IllegalArgumentException("user is null"));
       
    }
}
  • ILogRecordService 保存/查詢日志的例子,使用者可以根據資料量保存到合適的存盤介質上,比如保存在資料庫/或者ES,自己實作保存和洗掉就可以了

也可以只實作保存的介面,畢竟已經保存在業務的存盤上了,查詢業務可以自己實作,不走 ILogRecordService 這個介面,畢竟產品經理會提一些千奇百怪的查詢需求,

@Service
public class DbLogRecordServiceImpl implements ILogRecordService {

    @Resource
    private LogRecordMapper logRecordMapper;

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void record(LogRecord logRecord) {
        log.info("【logRecord】log={}", logRecord);
        LogRecordPO logRecordPO = LogRecordPO.toPo(logRecord);
        logRecordMapper.insert(logRecordPO);
    }

    @Override
    public List<LogRecord> queryLog(String bizKey, Collection<String> types) {
        return Lists.newArrayList();
    }

    @Override
    public PageDO<LogRecord> queryLogByBizNo(String bizNo, Collection<String> types, PageRequestDO pageRequestDO) {
        return logRecordMapper.selectByBizNoAndCategory(bizNo, types, pageRequestDO);
    }
}
  • IParseFunction 自定義轉換函式的介面,可以實作IParseFunction 實作對LogRecord注解中使用的函式擴展
    例子:
@Component
public class UserParseFunction implements IParseFunction {
    private final Splitter splitter = Splitter.on(",").trimResults();

    @Resource
    @Lazy
    private UserQueryService userQueryService;

    @Override
    public String functionName() {
        return "USER";
    }

    @Override
    // 11,12 回傳 11(小明,張三)
    public String apply(String value) {
        if (StringUtils.isEmpty(value)) {
            return value;
        }
        List<String> userIds = Lists.newArrayList(splitter.split(value));
        List<User> misDOList = userQueryService.getUserList(userIds);
        Map<String, User> userMap = StreamUtil.extractMap(misDOList, User::getId);
        StringBuilder stringBuilder = new StringBuilder();
        for (String userId : userIds) {
            stringBuilder.append(userId);
            if (userMap.get(userId) != null) {
                stringBuilder.append("(").append(userMap.get(userId).getUsername()).append(")");
            }
            stringBuilder.append(",");
        }
        return stringBuilder.toString().replaceAll(",$", "");
    }
}

變數相關

LogRecordAnnotation 可以使用的變數出了引數也可以使用回傳值#_ret變數,以及例外的錯誤資訊#_errorMsg,也可以通過SpEL的 T 方式呼叫靜態方法噢

帶擴展

實作一個 Log的 Context,可以解決方法引數中沒有的變數但是想使用的問題,初步想法是可以通過在方法中 add 變數的形式實作,很快就可以實作了 😄

注意點:

?? 整體日志攔截是在方法執行之后記錄的,所以對于方法內部修改了方法引數之后,LogRecordAnnotation 的注解上的 SpEL 對變數的取值是修改后的值哦~

原始碼

https://github.com/mouzt/mzt-biz-log

Author

mail : mztsmile@163.com

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/258773.html

標籤:其他

上一篇:Selenium剛玩一會兒,就感受了私人秘書的體驗

下一篇:【2021屆閱文Android方向筆試卷】- 最長回文子串

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 面試突擊第一季,第二季,第三季

    第一季必考 https://www.bilibili.com/video/BV1FE411y79Y?from=search&seid=15921726601957489746 第二季分布式 https://www.bilibili.com/video/BV13f4y127ee/?spm_id_fro ......

    uj5u.com 2020-09-10 05:35:24 more
  • 第三單元作業總結

    1.前言 這應該是本學期最后一次寫作業總結了吧。總體來說,對作業的節奏也差不多掌握了,作業做起來的效率也更高了。雖然和之前的作業一樣,作業中都要用到新的知識,但是相比之前,更加懂得了如何利用工具以及資料。雖然之間卡過殼,但總體而言,這幾次作業還算完成的比較好。 2.作業程序總結 相比前兩個單元,此單 ......

    uj5u.com 2020-09-10 05:35:41 more
  • 北航OO(2020)第四單元博客作業暨課程總結博客

    北航OO(2020)第四單元博客作業暨課程總結博客 本單元作業的架構設計 在本單元中,由于UML圖具有比較清晰的樹形結構,因此我對其中需要進行查詢操作的元素進行了包裝,在樹的父節點中存盤所有孩子的參考。考慮到性能問題,我采用了快取機制,一次查詢后盡可能快取已經遍歷過的資訊,以減少遍歷次數。 本單元我 ......

    uj5u.com 2020-09-10 05:35:48 more
  • BUAA_OO_第四單元

    一、UML決議器設計 ? 先看下題目:第四單元實作一個基于JDK 8帶有效性檢查的UML(Unified Modeling Language)類圖,順序圖,狀態圖分析器 MyUmlInteraction,實際上我們要建立一個有向圖模型,UML中的物件(元素)可能與同級元素連接,也可與低級元素相連形成 ......

    uj5u.com 2020-09-10 05:35:54 more
  • 6.1邏輯運算子

    邏輯運算子 1. && 短路與 運算式1 && 運算式2 01.運算式1為true并且運算式2也為true 整體回傳為true 02.運算式1為false,將不會執行運算式2 整體回傳為false 03.只要有一個運算式為false 整體回傳為false 2. || 短路或 運算式1 || 運算式2 ......

    uj5u.com 2020-09-10 05:35:56 more
  • BUAAOO 第四單元 & 課程總結

    1. 第四單元:StarUml檔案決議 本單元采用了圖模型決議UML。 UML檔案可以抽象為圖、子圖、邊的邏輯結構。 在實作中,圖的節點包括類、介面、屬性,子圖包括狀態圖、順序圖等。 采用了三次遍歷UML元素的方法建圖,第一遍遍歷建點,第二、三次遍歷設定屬性、連邊,實作圖物件的初始化。這里借鑒了一些 ......

    uj5u.com 2020-09-10 05:36:06 more
  • 談談我對C# 多型的理解

    面向物件三要素:封裝、繼承、多型。 封裝和繼承,這兩個比較好理解,但要理解多型的話,可就稍微有點難度了。今天,我們就來講講多型的理解。 我們應該經常會看到面試題目:請談談對多型的理解。 其實呢,多型非常簡單,就一句話:呼叫同一種方法產生了不同的結果。 具體實作方式有三種。 一、多載 多載很簡單。 p ......

    uj5u.com 2020-09-10 05:36:09 more
  • Python 資料驅動工具:DDT

    背景 python 的unittest 沒有自帶資料驅動功能。 所以如果使用unittest,同時又想使用資料驅動,那么就可以使用DDT來完成。 DDT是 “Data-Driven Tests”的縮寫。 資料:http://ddt.readthedocs.io/en/latest/ 使用方法 dd. ......

    uj5u.com 2020-09-10 05:36:13 more
  • Python里面的xlrd模塊詳解

    那我就一下面積個問題對xlrd模塊進行學習一下: 1.什么是xlrd模塊? 2.為什么使用xlrd模塊? 3.怎樣使用xlrd模塊? 1.什么是xlrd模塊? ?python操作excel主要用到xlrd和xlwt這兩個庫,即xlrd是讀excel,xlwt是寫excel的庫。 今天就先來說一下xl ......

    uj5u.com 2020-09-10 05:36:28 more
  • 當我們創建HashMap時,底層到底做了什么?

    jdk1.7中的底層實作程序(底層基于陣列+鏈表) 在我們new HashMap()時,底層創建了默認長度為16的一維陣列Entry[ ] table。當我們呼叫map.put(key1,value1)方法向HashMap里添加資料的時候: 首先,呼叫key1所在類的hashCode()計算key1 ......

    uj5u.com 2020-09-10 05:36:38 more
最新发布
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:20:47 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:20:25 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:20:17 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:20:10 more
  • 【中介者設計模式詳解】C/Java/JS/Go/Python/TS不同語言實作

    * 中介者模式是一種行為型設計模式,它可以用來減少類之間的直接依賴關系,
    * 將物件之間的通信封裝到一個中介者物件中,從而使得各個物件之間的關系更加松散。
    * 在中介者模式中,物件之間不再直接相互互動,而是通過中介者來中轉訊息。 ......

    uj5u.com 2023-04-20 08:19:44 more
  • 露天煤礦現場調研和交流案例分享

    他們集團的資訊化公司及研究院在一個礦區正在做智能礦山的統一平臺的 試點,專案投資大概1億,包括了礦山的各方面的內容,顯示得我們這次交流有點多余。他們2年前開始做智能礦山的規劃,有很多煤礦行業專家的加持,他們的描述是非常完美,但是去年底應該上線的平臺,現在還沒有看到影子。他們確實有很多場景需求,但是被... ......

    uj5u.com 2023-04-20 08:19:07 more
  • 《社區人員管理》實戰案例設計&個人案例分享

    設計是一個讓人夢想成真程序,開始編碼、測驗、除錯之前進行需求分析和架構設計,才能保證關鍵方面都做正確 ......

    uj5u.com 2023-04-20 08:18:57 more
  • 軟體架構生態化-多角色交付的探索實踐

    作為一個技術架構師,不僅僅要緊跟行業技術趨勢,還要結合研發團隊現狀及痛點,探索新的交付方案。在日常中,你是否遇到如下問題 “ 業務需求排期長研發是瓶頸;非研發角色感受不到研發技改提效的變化;引入ISV 團隊又擔心質量和安全,培訓周期長“等等,基于此我們探索了一種新的技術體系及交付方案來解決如上問題。 ......

    uj5u.com 2023-04-20 08:18:49 more
  • 05單件模式

    #經典的單件模式 public class Singleton { private static Singleton uniqueInstance; //一個靜態變數持有Singleton類的唯一實體。 // 其他有用的實體變數寫在這里 //構造器宣告為私有,只有Singleton可以實體化這個類! ......

    uj5u.com 2023-04-19 08:42:51 more
  • 【架構與設計】常見微服務分層架構的區別和落地實踐

    軟體工程的方方面面都遵循一個最基本的道理:沒有銀彈,架構分層模型更是如此,每一種都有各自優缺點,所以請根據不同的業務場景,并遵循簡單、可演進這兩個重要的架構原則選擇合適的架構分層模型即可。 ......

    uj5u.com 2023-04-19 08:42:41 more