一個註解搞定 Spring Boot 日誌!還有誰不會?

此組件解決的問題是:java

「誰」在「什麼時間」對「什麼」作了「什麼事」git

本組件目前針對 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入口打開開關,添加 @EnableLogRecord 註解

tenant是表明租戶的標識,通常一個服務或者一個業務下的多個服務都寫死一個 tenant 就能夠github

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

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

Spring Boot 最新教程推薦看下面這個。web

https://github.com/javastacks...面試

日誌埋點

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

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. 日誌支持種類

好比一個訂單的操做日誌,有些操做日誌是用戶本身操做的,有些操做是系統運營人員作了修改產生的操做日誌,咱們系統不但願把運營的操做日誌暴露給用戶看到,mvc

可是運營指望能夠看到用戶的日誌以及運營本身操做的日誌,這些操做日誌的bizNo都是訂單號,因此爲了擴展添加了類型字段,主要是爲了對日誌作分類,查詢方便,支持更多的業務。intellij-idea

@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() 方法。app

若是保存 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(小明),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(",$", "");
    }
}

變量相關

LogRecordAnnotation 能夠使用的變量出了參數也能夠使用返回值#_ret變量,以及異常的錯誤信息#_errorMsg,也能夠經過SpEL的 T 方式調用靜態方法噢

待擴展

實現一個 Log的 Context,能夠解決方法參數中沒有的變量可是想使用的問題,初步想法是能夠經過在方法中 add 變量的形式實現,很快就能夠實現了 😄

注意點:

⚠️ 總體日誌攔截是在方法執行以後記錄的,因此對於方法內部修改了方法參數以後,LogRecordAnnotation 的註解上的 SpEL 對變量的取值是修改後的值哦。

源碼:https://github.com/mouzt/mzt-...

版權聲明:本文爲CSDN博主「mztBang」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處連接及本聲明。
原文連接:https://blog.csdn.net/weixin_...

近期熱文推薦:

1.600+ 道 Java面試題及答案整理(2021最新版)

2.終於靠開源項目弄到 IntelliJ IDEA 激活碼了,真香!

3.阿里 Mock 工具正式開源,幹掉市面上全部 Mock 工具!

4.Spring Cloud 2020.0.0 正式發佈,全新顛覆性版本!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

以爲不錯,別忘了隨手點贊+轉發哦!

相關文章
相關標籤/搜索