如何優(yōu)雅的記錄操作日志

1. 背景

日志幾乎存在于所有系統(tǒng)中,開(kāi)發(fā)調(diào)試日志的記錄我們有l(wèi)og4j,logback等來(lái)實(shí)現(xiàn),但對(duì)于要展示給用戶看的日志,我并沒(méi)有發(fā)現(xiàn)一個(gè)簡(jiǎn)單通用的實(shí)現(xiàn)方案。所以決定為之后的開(kāi)發(fā)項(xiàng)目提供一個(gè)通用的操作日志組件。

2. 系統(tǒng)日志和操作日志

所有系統(tǒng)都會(huì)有日志,但我們區(qū)分了 系統(tǒng)日志操作日志

  • 系統(tǒng)日志:主要用于開(kāi)發(fā)者調(diào)試排查系統(tǒng)問(wèn)題的,不要求固定格式和可讀性
  • 操作日志:主要面向用戶的,要求簡(jiǎn)單易懂,反映出用戶所做的動(dòng)作。

通過(guò)操作日志可追溯到 某人在某時(shí)干了某事情,如:

租戶 操作人 時(shí)間 操作 內(nèi)容
A租戶 小明 2022/2/27 20:15:00 新增 新增了一個(gè)用戶:Mr.Wang
B租戶 大米 2022/2/28 10:35:00 更新 修改訂單 [xxxxxx] 價(jià)格為 xx 元
C租戶 老王 2022/2/28 22:55:00 查詢 查詢了名為: [xx] 的所有交易

3. 需要哪些功能

3.1 訴求:

  1. 基于SpringBoot能夠快速接入
  2. 對(duì)業(yè)務(wù)代碼具有低入侵性

3.2 解決思路:

基于以上兩點(diǎn),我們想想如何實(shí)現(xiàn)。

spingboot快速接入,需要我們來(lái)自定義spring boot starter;

業(yè)務(wù)入侵性低,首先想到了AOP,一般操作日志都是在增刪改查的方法中,所以我們可以使用注解在這些方法上,通過(guò)AOP攔截這些方法。

3.3 待實(shí)現(xiàn):

因此,我們需要實(shí)現(xiàn)以下功能:

  • 自定義spring boot starter
  • 定義日志注解
  • AOP攔截日志注解方法
  • 定義日志動(dòng)態(tài)內(nèi)容模板

模板中又需要實(shí)現(xiàn):

  • 動(dòng)態(tài)模板表達(dá)式解析:用強(qiáng)大的SpEL來(lái)解析表達(dá)式
  • 自定義函數(shù):支持目標(biāo)方法前置/后置的自定義函數(shù)

3.4 展現(xiàn)

所以我們最終期望的大概是這樣:

@EasyLog(module = "用戶模塊", type = "新增",
        content = "測(cè)試 {functionName{#userDto.name}}",
        condition = "#userDto.name == 'easylog'")
public String test(UserDto userDto) {
    return "test";
}

4. 實(shí)現(xiàn)步驟

4.1 定義日志注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface EasyLog {
    String tenant() default "";
    String operator() default "";
    String module() default "";
    String type() default "";
    String bizNo() default "";
    String content();
    String fail() default "";
    String detail() default "";
    String condition() default "";
}
字段 意義 支持SpEl表達(dá)式 必填
tenant 租戶,SAAS系統(tǒng)中區(qū)分不同租戶
operator 操作者
module 模塊,區(qū)分不同業(yè)務(wù)模塊
type 操作類型,形如:增刪改查
bizNo 業(yè)務(wù)編號(hào),便于查詢
content 日志模板內(nèi)容
fail 操作失敗時(shí)的模板內(nèi)容
detail 額外的記錄信息
condition 是否記錄的條件 (默認(rèn):true 記錄)

4.2 自定義函數(shù)

這里的自定義函數(shù),并不是指SpEL中的自定義函數(shù),因?yàn)镾pEL中的自定義函數(shù)必須是靜態(tài)方法才可以注冊(cè)到其中,因?yàn)殪o態(tài)方法使用中并沒(méi)有我們自己定義方法來(lái)的方便,所以這里的自定義函數(shù)僅僅指代我們定義的一個(gè)普通方法。

public interface ICustomFunction {
    /**
     * 目標(biāo)方法執(zhí)行前 執(zhí)行自定義函數(shù)
     * @return 是否是前置函數(shù)
     */
    boolean executeBefore();

    /**
     * 自定義函數(shù)名
     * @return 自定義函數(shù)名
     */
    String functionName();

    /**
     * 自定義函數(shù)
     * @param param 參數(shù)
     * @return 執(zhí)行結(jié)果
     */
    String apply(String param);
}

我們定義好自定義函數(shù)接口,實(shí)現(xiàn)交給使用者。使用者將實(shí)現(xiàn)類交給Spring容器管理,我們解析的時(shí)候從Spring容器中獲取即可。

4.3 SpEL表達(dá)式解析

主要牽涉下面幾個(gè)核心類:

  • 解析器ExpressionParser,用于將字符串表達(dá)式轉(zhuǎn)換為Expression表達(dá)式對(duì)象。
  • 表達(dá)式Expression,最后通過(guò)它的getValute方法對(duì)表達(dá)式進(jìn)行計(jì)算取值。
  • 上下文EvaluationContext,通過(guò)上下文對(duì)象結(jié)合表達(dá)式來(lái)計(jì)算最后的結(jié)果。
ExpressionParser parser =new SpelExpressionParser(); // 創(chuàng)建一個(gè)表達(dá)式解析器
StandardEvaluationContext ex = new StandardEvaluationContext(); // 創(chuàng)建上下文
ex.setVariables("name", "easylog"); // 將自定義參數(shù)添加到上下文
Expression exp = parser.parseExpression("'歡迎你! '+ #name"); //模板解析
String val = exp.getValue(ex,String.class); //獲取值

我們只需要拿到日志注解中的動(dòng)態(tài)模板即可通過(guò)SpEL來(lái)解析。

4.4 自定義函數(shù)的解析

我們采用 { functionName { param }} 的形式在模板中展示自定義函數(shù),解析整個(gè)模板前,我們先來(lái)解析下自定義函數(shù),將解析后的值替換掉模板中的字符串即可。

if (template.contains("{")) {
   Matcher matcher = PATTERN.matcher(template);
   while (matcher.find()) {
       String funcName = matcher.group(1);
       String param = matcher.group(2);
       if (customFunctionService.executeBefore(funcName)) {
          String apply = customFunctionService.apply(funcName, param);
       }
   }
}

4.5 獲取操作者信息

一般我們都是將登錄者信息存入應(yīng)用上下文中,所以我們不必每次都在日志注解中指出,我們可統(tǒng)一設(shè)置,定義一個(gè)獲取操作者接口,由使用者實(shí)現(xiàn)。

public interface IOperatorService {
    // 獲取當(dāng)前操作者
    String getOperator();
    // 當(dāng)前租戶
    String getTenant();
}

4.6 定義日志內(nèi)容接收

我們要將解析完成后的日志內(nèi)容實(shí)體信息發(fā)送給我們的使用者,所以我們需要定義一個(gè)日志接收的接口,具體的實(shí)現(xiàn)交給使用者來(lái)實(shí)現(xiàn),無(wú)論他接收到日志存儲(chǔ)在數(shù)據(jù)庫(kù),MQ還是哪里,讓使用者來(lái)決定。

public interface ILogRecordService {
    /**
     * 保存 log
     * @param easyLogInfo 日志實(shí)體
     */
    void record(EasyLogInfo easyLogInfo);
}

4.7 定義AOP攔截

@Aspect
@Component
@AllArgsConstructor
public class EasyLogAspect {

    @Pointcut("@annotation(**.EasyLog)")
    public void pointCut() {}

    // 環(huán)繞通知
    @Around("pointCut() && @annotation(easyLog)")
    public Object around(ProceedingJoinPoint joinPoint, EasyLog easyLog) throws Throwable {

        //前置自定義函數(shù)解析
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
        }
        //SpEL解析
        //后置自定義函數(shù)解析
        return result;
    }
}

4.8 自定義 spring boot starter

創(chuàng)建自動(dòng)配置類,將定義的一些來(lái)交給Spring容器管理:

@Configuration
@ComponentScan("**")
public class EasyLogAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(ICustomFunction.class)
    @Role(BeanDefinition.ROLE_APPLICATION)
    public ICustomFunction customFunction(){
        return new DefaultCustomFunction();
    }

    @Bean
    @ConditionalOnMissingBean(IOperatorService.class)
    @Role(BeanDefinition.ROLE_APPLICATION)
    public IOperatorService operatorGetService() {
        return new DefaultOperatorServiceImpl();
    }

    @Bean
    @ConditionalOnMissingBean(ILogRecordService.class)
    @Role(BeanDefinition.ROLE_APPLICATION)
    public ILogRecordService recordService() {
        return new DefaultLogRecordServiceImpl();
    }
}

上一篇我已經(jīng)完整的介紹了如何自定義 spring boot starter ,可去參考:
如何自定義 spring boot starter ?

5. 我們可以學(xué)到什么?

你可以拉取easy-log源碼,用于學(xué)習(xí),通過(guò)easy-log你可以學(xué)到:

  • 注解的定義及使用
  • AOP的應(yīng)用
  • SpEL表達(dá)式的解析
  • 自定義 Spring boot starter
  • 設(shè)計(jì)模式

6. 源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容