<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          SpringBoot 操作日志

          共 13441字,需瀏覽 27分鐘

           ·

          2021-08-06 11:05


            Java大聯(lián)盟

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

          關(guān)注



          作者:mztBang

          csdn.net/weixin_43954303/article/details/113781801


          B 站搜索:楠哥教你學Java

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


          Spring Boot 操作日志

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

          本組件目前針對 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 是代表租戶的標識,一般一個服務(wù)或者一個業(yè)務(wù)下的多個服務(wù)都寫死一個 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 都為整數(shù) ID 的時候和其他的業(yè)務(wù)中的 ID 重復。比如訂單 ID、用戶 ID 等
          • bizNo:就是業(yè)務(wù)的 ID,比如訂單 ID,我們查詢的時候可以根據(jù) bizNo 查詢和它相關(guān)的操作日志
          • success:方法調(diào)用成功后把 success 記錄在日志的內(nèi)容中
          • SpEL 表達式:其中用雙大括號包圍起來的(例如:{{#order.purchaseName}})#order.purchaseName 是 SpEL 表達式。Spring 中支持的它都支持的。比如調(diào)用靜態(tài)方法,三目表達式。SpEL 可以使用方法中的任何參數(shù)
          @LogRecordAnnotation(success = "{{#order.purchaseName}}下了一個訂單,購買商品「{{#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;
          }


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


          2. 期望記錄失敗的日志, 如果拋出異常則記錄 fail 的日志,沒有拋出記錄 success 的日志
          @LogRecordAnnotation(
          fail = "創(chuàng)建訂單失敗,失敗原因:「{{#_errorMsg}}」",
          success = "{{#order.purchaseName}}下了一個訂單,購買商品「{{#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. 日志支持種類
          比如一個訂單的操作日志,有些操作日志是用戶自己操作的,有些操作是系統(tǒng)運營人員做了修改產(chǎn)生的操作日志,我們系統(tǒng)不希望把運營的操作日志暴露給用戶看到,
          但是運營期望可以看到用戶的日志以及運營自己操作的日志,這些操作日志的 bizNo 都是訂單號,所以為了擴展添加了類型字段, 主要是為了對日志做分類,查詢方便,支持更多的業(yè)務(wù)。


          @LogRecordAnnotation(
          fail = "創(chuàng)建訂單失敗,失敗原因:「{{#_errorMsg}}」",
          category = "MANAGER",
          success = "{{#order.purchaseName}}下了一個訂單,購買商品「{{#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. 支持記錄操作的詳情或者額外信息
          如果一個操作修改了很多字段,但是 success 的日志模版里面防止過長不能把修改詳情全部展示出來,這時候需要把修改的詳情保存到 detail 字段,
          detail 是一個 String ,需要自己序列化。這里的 #order.toString() 是調(diào)用了 Order 的 toString() 方法。
          如果保存 JSON,自己重寫一下 Order 的 toString() 方法就可以。


          @LogRecordAnnotation(
          fail = "創(chuàng)建訂單失敗,失敗原因:「{{#_errorMsg}}」",
          category = "MANAGER_VIEW",
          detail = "{{#order.toString()}}",
          success = "{{#order.purchaseName}}下了一個訂單,購買商品「{{#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}}下了一個訂單,購買商品「{{#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)方法獲取當前用戶。


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


          • functionName() 方法就返回注解上面的函數(shù)名;
          • apply() 函數(shù)參數(shù)是 "{ORDER{#orderId}}" 中 SpEL 解析的 #orderId 的值,這里是一個數(shù)字 1223110,接下來只需要在實現(xiàn)的類中把 ID 轉(zhuǎn)換為可讀懂的字符串就可以了,
            一般為了方便排查問題需要把名稱和 ID 都展示出來,例如:"訂單名稱(ID)" 的形式。
          這里有個問題:加了自定義函數(shù)后,框架怎么能調(diào)用到呢?
          答:對于 Spring boot 應(yīng)用很簡單,只需要把它暴露在 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}} 的大括號中間加了 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ù)的實現(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對象的傳遞過來,然后反解析拼接一個定制的操作日志內(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 三目表達式
          @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) 的方法添加變量,第一個對象為變量名稱,后面為變量的對象,
          然后我們就可以使用 SpEL 使用這個變量了,例如:例子中的 {{#innerOrder.productName}} 是在方法中設(shè)置的變量
          @Override
          @LogRecordAnnotation(
          success = "{{#order.purchaseName}}下了一個訂單,購買商品「{{#order.productName}}」,測試變量「{{#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)部變量測試");
          LogRecordContext.putVariable("innerOrder", order1);
          return true;
          }

          9. 函數(shù)中使用 LogRecordContext 的變量
          使用 LogRecordContext.putVariable(variableName, Object) 添加的變量除了可以在注解的 SpEL 表達式上使用,還可以在自定義函數(shù)中使用, 這種方式比較復雜,下面例子中示意了列表的變化,比如從 [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);
          }
          }

          框架的擴展點

          • 重寫 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 保存 / 查詢?nèi)罩镜睦? 使用者可以根據(jù)數(shù)據(jù)量保存到合適的存儲介質(zhì)上,比如保存在數(shù)據(jù)庫 / 或者 ES。自己實現(xiàn)保存和刪除就可以了
          也可以只實現(xiàn)查詢的接口,畢竟已經(jīng)保存在業(yè)務(wù)的存儲上了,查詢業(yè)務(wù)可以自己實現(xiàn),不走 ILogRecordService 這個接口,畢竟產(chǎn)品經(jīng)理會提一些千奇百怪的查詢需求。
          @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ù)的接口,可以實現(xiàn) IParseFunction 實現(xiàn)對 LogRecord 注解中使用的函數(shù)擴展
            例子:
          @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 變量,以及異常的錯誤信息 #_errorMsg,也可以通過 SpEL 的 T 方式調(diào)用靜態(tài)方法噢

          Change Log & TODO

          注意點:

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


          源碼

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

          推薦閱讀

          1、Spring Boot+Vue項目實戰(zhàn)

          2、B站:4小時上手MyBatis Plus

          3、一文搞懂前后端分離

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


          楠哥簡介

          資深 Java 工程師,微信號 southwindss

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

          騰訊課程官方 Java 面試官今日頭條認證大V

          GitChat認證作者,B站認證UP主(楠哥教你學Java)

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




          有收獲,就在看 
          瀏覽 83
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人AV电影网 久久爱 | 天天爽天天澡天天爽视频 - 百度 无码毛片一区二区三区四区五区六区 | 性欧美熟妇 | 久久艹逼视频 | 国产三级理论视频在线 |