<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>

          舒服!一個(gè)注解,搞定 SpringBoot 操作日志

          共 18197字,需瀏覽 37分鐘

           ·

          2021-03-03 11:46

          往期熱門文章:
          1、不要再封裝各種Util工具類了,這個(gè)神級(jí)框架值得擁有!
          2、阿里一面:如何保障消息100%投遞成功、消息冪等性?
          3、面試中會(huì)遇到的 15 個(gè) Java 冷知識(shí),你懂多少?
          4、一個(gè)比 Spring Boot 快 44 倍的 Java 框架
          5、面試官:如何保障消息100%投遞成功、消息冪等性?


          此組件解決的問題是:「誰(shuí)」在「什么時(shí)間」對(duì)「什么」做了「什么事」
          本組件目前針對(duì) Spring-boot 做了 Autoconfig,如果是 SpringMVC,也可自己在 xml 初始化 bean

          使用方式

          基本使用

          maven依賴添加SDK依賴

                  <dependency>
                      <groupId>io.github.mouzt</groupId>
                      <artifactId>bizlog-sdk</artifactId>
                      <version>1.0.1</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.classargs);
              }
          }

          日志埋點(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)包圍起來(lái)的(例如:{{#order.purchaseName}})#order.purchaseName 是 SpEL表達(dá)式。Spring中支持的它都支持的。比如調(diào)用靜態(tài)方法,三目表達(dá)式。SpEL 可以使用方法中的任何參數(shù)
            @LogRecordAnnotation(success = "{{#order.purchaseName}}下了一個(gè)訂單,購(gòu)買商品「{{#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è)訂單,購(gòu)買商品「超值優(yōu)惠紅燒肉套餐」,下單結(jié)果:true”
          2. 期望記錄失敗的日志, 如果拋出異常則記錄fail的日志,沒有拋出記錄 success 的日志
              @LogRecordAnnotation(
                      fail = "創(chuàng)建訂單失敗,失敗原因:「{{#_errorMsg}}」",
                      success = "{{#order.purchaseName}}下了一個(gè)訂單,購(gòu)買商品「{{#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)營(yíng)人員做了修改產(chǎn)生的操作日志,我們系統(tǒng)不希望把運(yùn)營(yíng)的操作日志暴露給用戶看到, 但是運(yùn)營(yíng)期望可以看到用戶的日志以及運(yùn)營(yíng)自己操作的日志,這些操作日志的bizNo都是訂單號(hào),所以為了擴(kuò)展添加了類型字段,主要是為了對(duì)日志做分類,查詢方便,支持更多的業(yè)務(wù)。
              @LogRecordAnnotation(
                      fail = "創(chuàng)建訂單失敗,失敗原因:「{{#_errorMsg}}」",
                      category = "MANAGER",
                      success = "{{#order.purchaseName}}下了一個(gè)訂單,購(gòu)買商品「{{#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的日志模版里面防止過(guò)長(zhǎng)不能把修改詳情全部展示出來(lái),這時(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è)訂單,購(gòu)買商品「{{#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è)訂單,購(gòu)買商品「{{#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ù),或者通過(guò) SpEL 調(diào)用靜態(tài)方法獲取當(dāng)前用戶。
          • 第二種:通過(guò)默認(rèn)實(shí)現(xiàn)類來(lái)自動(dòng)的獲取操作人,由于在大部分web應(yīng)用中當(dāng)前的用戶都是保存在一個(gè)線程上下文中的,所以每個(gè)注解都加一個(gè)operator獲取操作人顯得有些重復(fù)勞動(dòng),所以提供了一個(gè)擴(kuò)展接口來(lái)獲取操作人 框架提供了一個(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)營(yíng)或者產(chǎn)品來(lái)說(shuō)難以理解,所以引入了自定義函數(shù)的功能。使用方法是在原來(lái)的變量的兩個(gè)大括號(hào)之間加一個(gè)函數(shù)名稱 例如 “{ORDER{#orderId}}” 其中 ORDER 是一個(gè)函數(shù)名稱。只有一個(gè)函數(shù)名稱是不夠的,需要添加這個(gè)函數(shù)的定義和實(shí)現(xiàn)。可以看下面例子 自定義的函數(shù)需要實(shí)現(xiàn)框架里面的IParseFunction的接口,需要實(shí)現(xiàn)兩個(gè)方法:
          • functionName() 方法就返回注解上面的函數(shù)名;
          • apply()函數(shù)參數(shù)是 "{ORDER{#orderId}}"中SpEL解析的#orderId的值,這里是一個(gè)數(shù)字1223110,接下來(lái)只需要在實(shí)現(xiàn)的類中把 ID 轉(zhuǎn)換為可讀懂的字符串就可以了, 一般為了方便排查問題需要把名稱和ID都展示出來(lái),例如:"訂單名稱(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 //為了避免類加載順序的問題 最好為L(zhǎng)azy,沒有問題也可以不加
                  private OrderQueryService orderQueryService;

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

                  @Override
                  //這里的 value 可以吧 Order 的JSON對(duì)象的傳遞過(guò)來(lái),然后反解析拼接一個(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;
              }

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

          • 重寫OperatorGetServiceImpl通過(guò)上下文獲取用戶的擴(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ù)庫(kù)/或者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,也可以通過(guò)SpEL的 T 方式調(diào)用靜態(tài)方法噢

          待擴(kuò)展

          實(shí)現(xiàn)一個(gè) Log的 Context,可以解決方法參數(shù)中沒有的變量但是想使用的問題,初步想法是可以通過(guò)在方法中 add 變量的形式實(shí)現(xiàn),很快就可以實(shí)現(xiàn)了 ??

          注意點(diǎn):

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

          源碼

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

          Author

          mail : [email protected]
          來(lái)源:blog.csdn.net/weixin_43954303/article/details/113781801

          最近熱文閱讀:

          1、不要再封裝各種Util工具類了,這個(gè)神級(jí)框架值得擁有!
          2、阿里一面:如何保障消息100%投遞成功、消息冪等性?
          3、面試中會(huì)遇到的 15 個(gè) Java 冷知識(shí),你懂多少?
          4、一個(gè)比 Spring Boot 快 44 倍的 Java 框架
          5、面試官:如何保障消息100%投遞成功、消息冪等性?
          6、太牛逼了!項(xiàng)目中用了Disruptor之后,性能提升了2.5倍
          7、最常用的分布式 ID 解決方案,都在這里了
          8、這 7 個(gè) Spring Boot 項(xiàng)目夠經(jīng)典!
          9、用 Java 擼了一款 SSH 客戶端
          10、處理 Exception 的幾種實(shí)踐,很優(yōu)雅,被很多團(tuán)隊(duì)采納!

          關(guān)注公眾號(hào),你想要的Java都在這里

          瀏覽 58
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

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

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <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>
                  wwwav视频 | 人人干人人操人人干 | 99爱精品视频在线观看 | 日韩无码链接 | 香蕉女人久久 |