1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        SpringBoot 操作日志

        共 13441字,需瀏覽 27分鐘

         ·

        2021-08-06 11:05


          Java大聯(lián)盟

          幫助萬千Java學(xué)習(xí)者持續(xù)成長

        關(guān)注



        作者:mztBang

        csdn.net/weixin_43954303/article/details/113781801


        B 站搜索:楠哥教你學(xué)Java

        獲取更多優(yōu)質(zhì)視頻教程


        Spring Boot 操作日志

        此組件解決的問題是:
        「誰」在「什么時(shí)間」對(duì)「什么」做了「什么事」

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

        使用方式

        基本使用

        maven 依賴添加 SDK 依賴

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

        SpringBoot 入口打開開關(guān), 添加 @EnableLogRecord 注解


        tenant 是代表租戶的標(biāo)識(shí),一般一個(gè)服務(wù)或者一個(gè)業(yè)務(wù)下的多個(gè)服務(wù)都寫死一個(gè) 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);
        }
        }

        日志埋點(diǎn)

        1. 普通的記錄日志
        • pefix:是拼接在 bizNo 上作為 log 的一個(gè)標(biāo)識(shí)。避免 bizNo 都為整數(shù) ID 的時(shí)候和其他的業(yè)務(wù)中的 ID 重復(fù)。比如訂單 ID、用戶 ID 等
        • bizNo:就是業(yè)務(wù)的 ID,比如訂單 ID,我們查詢的時(shí)候可以根據(jù) bizNo 查詢和它相關(guān)的操作日志
        • success:方法調(diào)用成功后把 success 記錄在日志的內(nèi)容中
        • SpEL 表達(dá)式:其中用雙大括號(hào)包圍起來的(例如:{{#order.purchaseName}})#order.purchaseName 是 SpEL 表達(dá)式。Spring 中支持的它都支持的。比如調(diào)用靜態(tài)方法,三目表達(dá)式。SpEL 可以使用方法中的任何參數(shù)
        @LogRecordAnnotation(success = "{{#order.purchaseName}}下了一個(gè)訂單,購買商品「{{#order.productName}}」,下單結(jié)果:{{#_ret}}",
        prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")

        public boolean createOrder(Order order) {
        log.info("【創(chuàng)建訂單】orderNo={}", order.getOrderNo());
        // db insert order
        return true;
        }


        此時(shí)會(huì)打印操作日志 “張三下了一個(gè)訂單, 購買商品「超值優(yōu)惠紅燒肉套餐」, 下單結(jié)果: true”


        2. 期望記錄失敗的日志, 如果拋出異常則記錄 fail 的日志,沒有拋出記錄 success 的日志
        @LogRecordAnnotation(
        fail = "創(chuàng)建訂單失敗,失敗原因:「{{#_errorMsg}}」",
        success = "{{#order.purchaseName}}下了一個(gè)訂單,購買商品「{{#order.productName}}」,下單結(jié)果:{{#_ret}}",
        prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")

        public boolean createOrder(Order order) {
        log.info("【創(chuàng)建訂單】orderNo={}", order.getOrderNo());
        // db insert order
        return true;
        }


        其中的 #_errorMsg 是取的方法拋出異常后的異常的 errorMessage。


        3. 日志支持種類
        比如一個(gè)訂單的操作日志,有些操作日志是用戶自己操作的,有些操作是系統(tǒng)運(yùn)營人員做了修改產(chǎn)生的操作日志,我們系統(tǒng)不希望把運(yùn)營的操作日志暴露給用戶看到,
        但是運(yùn)營期望可以看到用戶的日志以及運(yùn)營自己操作的日志,這些操作日志的 bizNo 都是訂單號(hào),所以為了擴(kuò)展添加了類型字段, 主要是為了對(duì)日志做分類,查詢方便,支持更多的業(yè)務(wù)。


        @LogRecordAnnotation(
        fail = "創(chuàng)建訂單失敗,失敗原因:「{{#_errorMsg}}」",
        category = "MANAGER",
        success = "{{#order.purchaseName}}下了一個(gè)訂單,購買商品「{{#order.productName}}」,下單結(jié)果:{{#_ret}}",
        prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")

        public boolean createOrder(Order order) {
        log.info("【創(chuàng)建訂單】orderNo={}", order.getOrderNo());
        // db insert order
        return true;
        }

        4. 支持記錄操作的詳情或者額外信息
        如果一個(gè)操作修改了很多字段,但是 success 的日志模版里面防止過長不能把修改詳情全部展示出來,這時(shí)候需要把修改的詳情保存到 detail 字段,
        detail 是一個(gè) String ,需要自己序列化。這里的 #order.toString() 是調(diào)用了 Order 的 toString() 方法。
        如果保存 JSON,自己重寫一下 Order 的 toString() 方法就可以。


        @LogRecordAnnotation(
        fail = "創(chuàng)建訂單失敗,失敗原因:「{{#_errorMsg}}」",
        category = "MANAGER_VIEW",
        detail = "{{#order.toString()}}",
        success = "{{#order.purchaseName}}下了一個(gè)訂單,購買商品「{{#order.productName}}」,下單結(jié)果:{{#_ret}}",
        prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")

        public boolean createOrder(Order order) {
        log.info("【創(chuàng)建訂單】orderNo={}", order.getOrderNo());
        // db insert order
        return true;
        }


        5. 如何指定操作日志的操作人是什么?框架提供了兩種方法
        • 第一種:手工在 LogRecord 的注解上指定。這種需要方法參數(shù)上有 operator
        @LogRecordAnnotation(
        fail = "創(chuàng)建訂單失敗,失敗原因:「{{#_errorMsg}}」",
        category = "MANAGER_VIEW",
        detail = "{{#order.toString()}}",
        operator = "{{#currentUser}}",
        success = "{{#order.purchaseName}}下了一個(gè)訂單,購買商品「{{#order.productName}}」,下單結(jié)果:{{#_ret}}",
        prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")

        public boolean createOrder(Order order, String currentUser) {
        log.info("【創(chuàng)建訂單】orderNo={}", order.getOrderNo());
        // db insert order
        return true;
        }


        這種方法手工指定,需要方法參數(shù)上有 operator 參數(shù),或者通過 SpEL 調(diào)用靜態(tài)方法獲取當(dāng)前用戶。


        • 第二種:通過默認(rèn)實(shí)現(xiàn)類來自動(dòng)的獲取操作人,由于在大部分 web 應(yīng)用中當(dāng)前的用戶都是保存在一個(gè)線程上下文中的,所以每個(gè)注解都加一個(gè) operator 獲取操作人顯得有些重復(fù)勞動(dòng),所以提供了一個(gè)擴(kuò)展接口來獲取操作人
          框架提供了一個(gè)擴(kuò)展接口,使用框架的業(yè)務(wù)可以 implements 這個(gè)接口自己實(shí)現(xiàn)獲取當(dāng)前用戶的邏輯,
          對(duì)于使用 Springboot 的只需要實(shí)現(xiàn) IOperatorGetService 接口,然后把這個(gè) Service 作為一個(gè)單例放到 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. 日志文案調(diào)整
        對(duì)于更新等方法,方法的參數(shù)上大部分都是訂單 ID、或者產(chǎn)品 ID 等,
        比如下面的例子:日志記錄的 success 內(nèi)容是:“更新了訂單 {{#orderId}}, 更新內(nèi)容為…”,這種對(duì)于運(yùn)營或者產(chǎn)品來說難以理解,所以引入了自定義函數(shù)的功能。
        使用方法是在原來的變量的兩個(gè)大括號(hào)之間加一個(gè)函數(shù)名稱 例如 “{ORDER{#orderId}}” 其中 ORDER 是一個(gè)函數(shù)名稱。只有一個(gè)函數(shù)名稱是不夠的, 需要添加這個(gè)函數(shù)的定義和實(shí)現(xiàn)??梢钥聪旅胬?br style="max-width: 100%;box-sizing: border-box !important;overflow-wrap: break-word !important;">自定義的函數(shù)需要實(shí)現(xiàn)框架里面的 IParseFunction 的接口,需要實(shí)現(xiàn)兩個(gè)方法:


        • functionName() 方法就返回注解上面的函數(shù)名;
        • apply() 函數(shù)參數(shù)是 "{ORDER{#orderId}}" 中 SpEL 解析的 #orderId 的值,這里是一個(gè)數(shù)字 1223110,接下來只需要在實(shí)現(xiàn)的類中把 ID 轉(zhuǎn)換為可讀懂的字符串就可以了,
          一般為了方便排查問題需要把名稱和 ID 都展示出來,例如:"訂單名稱(ID)" 的形式。
        這里有個(gè)問題:加了自定義函數(shù)后,框架怎么能調(diào)用到呢?
        答:對(duì)于 Spring boot 應(yīng)用很簡(jiǎn)單,只需要把它暴露在 Spring 的上下文中就可以了,可以加上 Spring 的 @Component 或者 @Service 很方便??。Spring mvc 應(yīng)用需要自己裝配 Bean。
        // 沒有使用自定義函數(shù)
        @LogRecordAnnotation(success = "更新了訂單{{#orderId}},更新內(nèi)容為....",
        prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}",
        detail = "{{#order.toString()}}")

        public boolean update(Long orderId, Order order) {
        return false;
        }

        //使用了自定義函數(shù),主要是在 {{#orderId}} 的大括號(hào)中間加了 functionName
        @LogRecordAnnotation(success = "更新了訂單ORDER{#orderId}},更新內(nèi)容為...",
        prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}",
        detail = "{{#order.toString()}}")

        public boolean update(Long orderId, Order order) {
        return false;
        }

        // 還需要加上函數(shù)的實(shí)現(xiàn)
        @Component
        public class OrderParseFunction implements IParseFunction {
        @Resource
        @Lazy //為了避免類加載順序的問題 最好為Lazy,沒有問題也可以不加
        private OrderQueryService orderQueryService;

        @Override
        public String functionName() {
        // 函數(shù)名稱為 ORDER
        return "ORDER";
        }

        @Override
        //這里的 value 可以吧 Order 的JSON對(duì)象的傳遞過來,然后反解析拼接一個(gè)定制的操作日志內(nèi)容
        public String apply(String value) {
        if(StringUtils.isEmpty(value)){
        return value;
        }
        Order order = orderQueryService.queryOrder(Long.parseLong(value));
        //把訂單產(chǎn)品名稱加上便于理解,加上 ID 便于查問題
        return order.getProductName().concat("(").concat(value).concat(")");
        }
        }

        7. 日志文案調(diào)整 使用 SpEL 三目表達(dá)式
        @LogRecordAnnotation(prefix = LogRecordTypeConstant.CUSTOM_ATTRIBUTE, bizNo = "{{#businessLineId}}",
        success = "{{#disable ? '停用' : '啟用'}}了自定義屬性{ATTRIBUTE{#attributeId}}")

        public CustomAttributeVO disableAttribute(Long businessLineId, Long attributeId, boolean disable) {
        return xxx;
        }

        8. 日志文案調(diào)整 模版中使用方法參數(shù)之外的變量
        可以在方法中通過 LogRecordContext.putVariable(variableName, Object) 的方法添加變量,第一個(gè)對(duì)象為變量名稱,后面為變量的對(duì)象,
        然后我們就可以使用 SpEL 使用這個(gè)變量了,例如:例子中的 {{#innerOrder.productName}} 是在方法中設(shè)置的變量
        @Override
        @LogRecordAnnotation(
        success = "{{#order.purchaseName}}下了一個(gè)訂單,購買商品「{{#order.productName}}」,測(cè)試變量「{{#innerOrder.productName}}」,下單結(jié)果:{{#_ret}}",
        prefix = LogRecordType.ORDER, bizNo = "{{#order.orderNo}}")

        public boolean createOrder(Order order) {
        log.info("【創(chuàng)建訂單】orderNo={}", order.getOrderNo());
        // db insert order
        Order order1 = new Order();
        order1.setProductName("內(nèi)部變量測(cè)試");
        LogRecordContext.putVariable("innerOrder", order1);
        return true;
        }

        9. 函數(shù)中使用 LogRecordContext 的變量
        使用 LogRecordContext.putVariable(variableName, Object) 添加的變量除了可以在注解的 SpEL 表達(dá)式上使用,還可以在自定義函數(shù)中使用, 這種方式比較復(fù)雜,下面例子中示意了列表的變化,比如從 [A,B,C] 改到 [B,D] 那么日志顯示:「刪除了 A,增加了 D」
        @LogRecord(success = "{DIFF_LIST{'文檔地址'}}", bizNo = "{{#id}}", prefix = REQUIREMENT)
        public void updateRequirementDocLink(String currentMisId, Long id, List<String> docLinks) {
        RequirementDO requirementDO = getRequirementDOById(id);
        LogRecordContext.putVariable("oldList", requirementDO.getDocLinks());
        LogRecordContext.putVariable("newList", docLinks);

        requirementModule.updateById("docLinks", RequirementUpdateDO.builder()
        .id(id)
        .docLinks(docLinks)
        .updater(currentMisId)
        .updateTime(new Date())
        .build());
        }


        @Component
        public class DiffListParseFunction implements IParseFunction {

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

        @SuppressWarnings("unchecked")
        @Override
        public String apply(String value) {
        if (StringUtils.isBlank(value)) {
        return value;
        }
        List<String> oldList = (List<String>) LogRecordContext.getVariable("oldList");
        List<String> newList = (List<String>) LogRecordContext.getVariable("newList");
        oldList = oldList == null ? Lists.newArrayList() : oldList;
        newList = newList == null ? Lists.newArrayList() : newList;
        Set<String> deletedSets = Sets.difference(Sets.newHashSet(oldList), Sets.newHashSet(newList));
        Set<String> addSets = Sets.difference(Sets.newHashSet(newList), Sets.newHashSet(oldList));
        StringBuilder stringBuilder = new StringBuilder();
        if (CollectionUtils.isNotEmpty(addSets)) {
        stringBuilder.append("新增了 <b>").append(value).append("</b>:");
        for (String item : addSets) {
        stringBuilder.append(item).append(",");
        }
        }
        if (CollectionUtils.isNotEmpty(deletedSets)) {
        stringBuilder.append("刪除了 <b>").append(value).append("</b>:");
        for (String item : deletedSets) {
        stringBuilder.append(item).append(",");
        }
        }
        return StringUtils.isBlank(stringBuilder) ? null : stringBuilder.substring(0, stringBuilder.length() - 1);
        }
        }

        框架的擴(kuò)展點(diǎn)

        • 重寫 OperatorGetServiceImpl 通過上下文獲取用戶的擴(kuò)展,例子如下
        @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 保存 / 查詢?nèi)罩镜睦? 使用者可以根據(jù)數(shù)據(jù)量保存到合適的存儲(chǔ)介質(zhì)上,比如保存在數(shù)據(jù)庫 / 或者 ES。自己實(shí)現(xiàn)保存和刪除就可以了
        也可以只實(shí)現(xiàn)查詢的接口,畢竟已經(jīng)保存在業(yè)務(wù)的存儲(chǔ)上了,查詢業(yè)務(wù)可以自己實(shí)現(xiàn),不走 ILogRecordService 這個(gè)接口,畢竟產(chǎn)品經(jīng)理會(huì)提一些千奇百怪的查詢需求。
        @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 自定義轉(zhuǎn)換函數(shù)的接口,可以實(shí)現(xiàn) IParseFunction 實(shí)現(xiàn)對(duì) LogRecord 注解中使用的函數(shù)擴(kuò)展
          例子:
        @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(小明),12(張三)
        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(",$", "");
        }
        }

        變量相關(guān)

        LogRecordAnnotation 可以使用的變量出了參數(shù)也可以使用返回值 #_ret 變量,以及異常的錯(cuò)誤信息 #_errorMsg,也可以通過 SpEL 的 T 方式調(diào)用靜態(tài)方法噢

        Change Log & TODO

        注意點(diǎn):

        ?? 整體日志攔截是在方法執(zhí)行之后記錄的,所以對(duì)于方法內(nèi)部修改了方法參數(shù)之后,LogRecordAnnotation 的注解上的 SpEL 對(duì)變量的取值是修改后的值哦~


        源碼

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

        推薦閱讀

        1、Spring Boot+Vue項(xiàng)目實(shí)戰(zhàn)

        2、B站:4小時(shí)上手MyBatis Plus

        3、一文搞懂前后端分離

        4、快速上手Spring Boot+Vue前后端分離


        楠哥簡(jiǎn)介

        資深 Java 工程師,微信號(hào) southwindss

        《Java零基礎(chǔ)實(shí)戰(zhàn)》一書作者

        騰訊課程官方 Java 面試官,今日頭條認(rèn)證大V

        GitChat認(rèn)證作者,B站認(rèn)證UP主(楠哥教你學(xué)Java)

        致力于幫助萬千 Java 學(xué)習(xí)者持續(xù)成長。




        有收獲,就點(diǎn)個(gè)在看 
        瀏覽 83
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            成人午夜视频免费看 | 免费 无码 国产真人视频九色 | 国产一级片观看 | 欧美精品九九99久久 | 日韩AV之家 | 成人午夜精品无码区毛片四季视频 | 男女啪啪网站 | 日韩欧美在线中文字幕 | 日本护士给病人满足hd | 日韩成人无码一区二区视频 |