前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

原文链接:https://blog.csdn.net/MeituanTech/article/details/120340493

1、系统日志和操作日志的区别

1.1、系统日志:

系统日志主要是为开发排查问题提供依据,一般打印在日志文件中;系统日志的可读性要求没那么高,日志中会包含代码的信息,比如在某个类的某一行打印了一个日志。

1.2、操作日志:

主要是对某个对象进行新增操作或者修改操作后记录下这个新增或者修改,操作日志要求可读性比较强,因为它主要是给用户看的,比如订单的物流信息,用户需要知道在什么时间发生了什么事情。再比如,客服对工单的处理记录信息。

1.3.2、操作日志的记录格式

大概分为下面几种:

1、单纯的文字记录,比如:2021-09-16 10:00 订单创建。

2、简单的动态的文本记录,比如:2021-09-16 10:00 订单创建,订单号:NO.11089999,其中涉及变量订单号“NO.11089999”。

3、修改类型的文本,包含修改前和修改后的值,比如:2021-09-16 10:00 用户小明修改了订单的配送地址:从“金灿灿小区”修改到“银盏盏小区” ,其中涉及变量配送的原地址“金灿灿小区”和新地址“银盏盏小区”。

4、修改表单,一次会修改多个字段。

2、操作日志如何和系统日志区分开

2.1、log4j2.xml

<logger name="businessLog" additivity="false" level="INFO">
  <appender-ref ref="businessLog"/>
</logger>
<?xml version="1.0" encoding="UTF-8"?>

<!--status  Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,
 你会看到log4j2内部各种详细输出。可以设置成OFF(关闭)或Error(只输出错误信息)-->
<!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数-->
<configuration status="error" monitorInterval="30">
    <!--    %d{yyyy-MM-dd HH:mm:ss, SSS} : 日志生产时间-->
    <!--    %p : 日志输出格式-->
    <!--    %thread表示线程名,-->
    <!--    %c : logger的名称-->
    <!--    %m : 日志内容,即 logger.info("message")-->
    <!--    %n : 换行符-->
    <!--    %C : Java类名-->
    <!--    %L : 日志输出所在行数-->
    <!--    %M : 日志输出所在方法名-->
    <!--    hostName : 本地机器名-->
    <!--    hostAddress : 本地ip地-->

    <!-- 日志文件目录和压缩文件目录配置 -->
    <Properties>
        <Property name="level">debug</Property>
        <Property name="LOG_PATTERN">
            %d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level -[%-32X{REQ_UID}]- %msg%xEx %logger{36}.%M[%L]%n
        </Property>

        <Property name="logDri">/Users/healerjean/Desktop/logs</Property>
        <Property name="logFileName">hlj-client</Property>

        <Property name="infoLogDri">${logDri}/info</Property>
        <Property name="infoLogGz">${infoLogDri}/gz</Property>
        <Property name="infoLogFileName">${logFileName}.log</Property>

        <Property name="errorLogDri">${logDri}/error</Property>
        <Property name="errorLogGz">${errorLogDri}/gz</Property>
        <Property name="errorLogFileName">${logFileName}.error</Property>


        <Property name="businessLogDri">${logDri}/business</Property>
        <Property name="businessLogGz">${businessLogDri}/gz</Property>
        <Property name="businessLogFileName">${logFileName}.bus.log</Property>
    </Properties>

    <appenders>
        <console name="Console" target="SYSTEM_OUT">
            <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)-->
            <ThresholdFilter level="${level}" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
            <!--输出日志的格式-->
            <PatternLayout pattern="${LOG_PATTERN}"/>
        </console>


        <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingRandomAccessFile name="infoFile" fileName="${infoLogDri}/${infoLogFileName}"
                                 filePattern="${infoLogGz}/${date:yyyy-MM}/%d{yyyy-MM-dd}-%i.${infoLogFileName}.gz">
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!-- 基于指定文件大小的滚动策略,size属性用来定义每个日志文件的大小 -->
                <SizeBasedTriggeringPolicy size="500 MB"/>
                <!-- 基于时间的滚动策略,interval属性用来指定多久滚动一次,默认是1 hour -->
                <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
            </Policies>
            <Filters>
                <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)
                onMatch属性设置为DENY,过滤掉高等级的日志;onMismatch设置为NEUTRAL,把低等级的日志放行,
                -->
                <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="NEUTRAL"/>
                <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
            <!-- 指定每天(文件夹是以天的,看上面的)的最大压缩包个数,默认7个,超过了会覆盖之前的(用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的(通过max属性)) -->
            <DefaultRolloverStrategy max="2000"/>
        </RollingRandomAccessFile>


        <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档-->
        <RollingRandomAccessFile name="errorFile" fileName="${errorLogDri}/${errorLogFileName}"
                                 filePattern="${errorLogGz}/${date:yyyy-MM}/%d{yyyy-MM-dd}-%i.${errorLogFileName}.gz">
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <!-- 基于指定文件大小的滚动策略,size属性用来定义每个日志文件的大小 -->
                <SizeBasedTriggeringPolicy size="500 MB"/>
                <!-- 基于时间的滚动策略,interval属性用来指定多久滚动一次,默认是1 hour -->
                <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
            </Policies>
            <Filters>
                <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)
                onMatch属性设置为DENY,过滤掉高等级的日志;onMismatch设置为NEUTRAL,把低等级的日志放行,
                -->
                <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
            <!-- 指定每天(文件夹是以天的,看上面的)的最大压缩包个数,默认7个,超过了会覆盖之前的(用来指定同一个文件夹下最多有几个日志文件时开始删除最旧的,创建新的(通过max属性)) -->
            <DefaultRolloverStrategy max="2000"/>
        </RollingRandomAccessFile>


        <!--操作日志-->
        <RollingRandomAccessFile name="businessLog" fileName="${businessLogDri}/${businessLogFileName}"
                                 filePattern="${businessLogGz}/${date:yyyy-MM}/%d{yyyy-MM-dd}-%i.${businessLogFileName}.gz">
            <PatternLayout pattern="${LOG_PATTERN}"/>
            <Policies>
                <SizeBasedTriggeringPolicy size="500 MB"/>
                <TimeBasedTriggeringPolicy interval="6" modulate="true"/>
            </Policies>
            <Filters>
                <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/>
            </Filters>
            <DefaultRolloverStrategy max="2000"/>
        </RollingRandomAccessFile>


    </appenders>


    <!--然后定义logger,只有定义了logger并引入的appender,appender才会生效-->
    <loggers>
        <!-- AsyncRoot - 异步记录日志 - 需要LMAX Disruptor的支持 -->
        <!-- additivity如果设置为true将会输出两次日志,意思和log4j里面意思是否追加 -->
        <AsyncRoot level="${level}" additivity="false" includeLocation="true">
            <AppenderRef ref="Console"/>
            <AppenderRef ref="infoFile"/>
            <AppenderRef ref="errorFile"/>
        </AsyncRoot>

        <logger name="businessLog" additivity="false" level="INFO">
            <appender-ref ref="businessLog"/>
        </logger>
    </loggers>

</configuration>

2.2、样例

@Slf4j
@Service
public class BusinessLogService {

    private static final Logger BUSINESS_LOG = LoggerFactory.getLogger("businessLog");

    @Test
    public void test(){
        BUSINESS_LOG.info("业务日志");
        log.debug("INFO 系统日志");
        log.info("INFO 系统日志");
        log.error("ERROR 系统日志");
    }

}

3、注解操作日志

一提到注解操作日志,就想到动态模板,就会涉及到让变量通过占位符的方式解析模板,从而达到通过注解记录操作日志的目的。模板解析的方式有很多种,这里使用了 SpELSpring Expression LanguageSpring 表达式语言)来实现。

3.1、代码拆解

3.1.1、代码结构

image-20230531211905788

3.1.2、代码逻辑

image-20230531214834501

3.2、注解

3.1.1、LogRecordAnnotation 注解

package com.healerjean.proj.service.bizlog.anno;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface LogRecordAnnotation {

    /**
     * 操作成功的的文本模版
     */
    String success();

    /**
     * 操作失败的的文本模版
     */
    String fail() default "";

    /**
     * 操作人
     */
    String operator() default "";

    /**
     * 日志类型
     */
    String prefix();

    /**
     * 业务编号
     */
    String bizNo();

    /**
     * 日志分类
     */
    String category() default "";

    /**
     * 扩展参数,记录日志的详情数据
     */java
    String detail() default "";

    /**
     * 记录日志的条件
     */
    String condition() default "";
}

3.3、公共包

3.3.1、BizLogEnum 枚举

package com.healerjean.proj.service.bizlog.common.enums;

import com.healerjean.proj.service.bizlog.service.function.impl.DefaultParseFunction;
import com.healerjean.proj.service.bizlog.service.function.impl.OrderParseFunction;
import com.healerjean.proj.service.bizlog.service.function.impl.UserParseFunction;

/**
 * @author zhangyujin
 * @date 2023/5/31  16:23.
 */
public interface BizLogEnum {

    /**
     * IParseFunctionEnum
     *
     * @author zhangyujin
     * @date 2023/5/31  15:23.
     */
    enum IParseFunctionEnum {


        /**
         * 默认
         */
        DEFAULT("default", DefaultParseFunction.class),
        ORDER_PARSE_FUNCTION("orderParse", OrderParseFunction.class),
        USER_PARSE_FUNCTION("userParse", UserParseFunction.class),


        ;


        /**
         * clazz
         */
        private final String function;

        /**
         * clazz
         */
        private final Class<?> clazz;

        /**
         * IParseFunctionEnum
         *
         * @param function function
         * @param clazz    clazz
         */
        IParseFunctionEnum(String function, Class<?> clazz) {
            this.function = function;
            this.clazz = clazz;
        }

        public String getFunction() {
            return function;
        }

        public Class<?> getClazz() {
            return clazz;
        }
    }


    /**
     * LogRecordTypeEnum
     *
     * @author zhangyujin
     * @date 2023/5/31  16:21.
     */
    enum LogRecordTypeEnum {

        /**
         * ORDER_TYPE
         */
        ORDER_TYPE("OrderType", "订单类型");

        /**
         * code
         */
        private final String code;

        /**
         * desc
         */
        private final String desc;

        /**
         * LogRecordTypeEnum
         *
         * @param code code
         * @param desc desc
         */
        LogRecordTypeEnum(String code, String desc) {
            this.code = code;
            this.desc = desc;
        }

        public String getCode() {
            return code;
        }

        public String getDesc() {
            return desc;
        }
    }


}

3.3.2、BizLogConstants 常亮

package com.healerjean.proj.service.bizlog.common;

/**
 * @author zhangyujin
 * @date 2023/5/30  19:42.
 */
public class BizLogConstants {

    /**
     * BizLogTypeConstant
     */
    public static class BizLogTypeConstant {
        /**
         * ORDER_TYPE
         */
        public static final String ORDER_TYPE = "OrderType";

    }


    /**
     * BizLogResultConstant
     */
    public static class BizLogResultConstant {
        /**
         * SUCCESS
         */
        public static final String SUCCESS = "OrderType";

        /**
         * FAIL
         */
        public static final String FAIL = "OrderType";java

    }

}

3.4、数据集

3.4.1、BizLogContext 上下文

package com.healerjean.proj.service.bizlog.data;

import com.healerjean.proj.service.bizlog.anno.LogRecordAnnotation;
import lombok.Data;
import lombok.experimental.Accessors;
import org.springframework.beans.factory.BeanFactory;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;

/**
 * 业务日志上下文
 *
 * @author zhangyujin
 * @date 2023/5/30  18:29.
 */
@Accessors(chain = true)
@Data
public class BizLogContext {

    /**
     * logRecordAnnotation
     */
    private LogRecordAnnotation logRecordAnnotation;

    /**
     * 切面类
     */
    private Class<?> targetClass;

    /**
     * 切面方法
     */
    private Method method;

    /**
     * 参数数组
     */
    private Object[] args;

    /**
     * spel 模版
     */
    private List<String> spElTemplates;

    /**
     * spel 函数解析结果(key 函数 value 结果)
     */
    private Map<String, String> functionReturnMap;

    /**
     * 方法执行结果
     */
    private MethodExecuteResult methodExecuteResult;

    /**
     * bean工厂
     */
    private BeanFactory beanFactory;


    @Accessors(chain = true)
    @Data
    public static class MethodExecuteResult {

        /**
         * 切面方法执行结果
         */
        private Object result;

        /**
         * 切面方法是否成功
         */
        private boolean success;

        /**
         * 切面方法 异常
         */
        private Throwable throwable;

        /**
         * 切面方法 异常信息
         */
        private String errorMsg;
    }



}

3.4.2、LogRecordBO

package com.healerjean.proj.service.bizlog.data.bo;

import lombok.Builder;
import lombok.Data;

/**
 * 解析后的模版
 *
 * @author zhangyujin
 * @date 2023/5/30  15:16
 */
@Data
@Builder
public class LogRecordBO {

    /**
     * 操作成功的的文本模版
     */
    private String successLogTemplate;
    /**
     * 操作失败的的文本模版
     */
    private String failLogTemplate;
    /**
     * 操作人
     */
    private String operatorId;
    /**
     * 业务key由 prefix + bizNo拼接而成
     */
    private String bizKey;
    /**
     * 业务编号
     */
    private String bizNo;
    /**
     * 日志分类
     */
    private String category;

    /**
     * 扩展参数,记录日志的详情数据
     */
    private String detail;

    /**
     * 记录日志的条件
     */
    private String condition;
}

3.4.3、表实体

3.4.3.1、LogRecord

package com.healerjean.proj.service.bizlog.data.po;

import lombok.*;

import java.util.Date;

/**
 * 分析出需要记录的操作日志
 */
@Setter
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
@ToString
public class LogRecord {

    /**
     * Id
     */
    private Integer id;

    /**
     * 租户,是为了多租户使用的
     */
    private String tenant;

    /**
     * bizKey
     */
    private String bizKey;

    /**
     * bizNo
     */
    private String bizNo;

    /**
     * operator
     */
    private String operator;

    /**
     * action
     */
    private String action;

    /**
     * category
     */
    private String category;

    /**
     * createTime
     */
    private Date createTime;

    /**
     * detail
     */
    private String detail;
}

3.4.3.2、Operator

package com.healerjean.proj.service.bizlog.data.po;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * Operator
 * @author zhangyujin
 * @date 2023/5/30  14:22
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Operator {

    /**
     * operatorId
     */
    private String operatorId;
}

3.4.3.3、Order

package com.healerjean.proj.service.bizlog.data.po;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
 *
 * Order
 * @author zhangyujin
 * @date 2023/5/31  16:04
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Order {
    /**
     * orderId
     */
    private Long orderId;
    /**
     * orderNo
     */
    private String orderNo;
    /**
     * purchaseName
     */
    private String purchaseName;
    /**
     * productName
     */
    private String productName;
    /**
     * createTime
     */
    private Date createTime;

    /**
     * userId
     */
    private String userId;

}

3.4.3.4、User

package com.healerjean.proj.service.bizlog.data.po;

import lombok.Data;
import lombok.experimental.Accessors;

/**
 * User实体对象
 *
 * @author zhangyujin
 * @date 2023/5/31  16:05.
 */
@Accessors(chain = true)
@Data
public class User {
    /**
     * 用户名
     */
    private String name;

    /**
     * userId
     */
    private String userId;
}

3.5、Service

3.5.1、业务

3.5.1.1、ILogRecordService

package com.healerjean.proj.service.bizlog.service;


import com.healerjean.proj.service.bizlog.data.po.LogRecord;

import java.util.List;

/**
 * 日志保存Service
 */
public interface ILogRecordService {
    /**
     * 保存log
     *
     * @param logRecord 日志实体
     */
    void record(LogRecord logRecord);

    /**
     * 返回最多100条记录
     *
     * @param bizKey 日志前缀+bizNo
     * @return 操作日志列表
     */
    List<LogRecord> queryLog(String bizKey);

    /**
     * 返回最多100条记录
     *
     * @param bizNo 业务标识
     * @return 操作日志列表
     */
    List<LogRecord> queryLogByBizNo(String bizNo);
}

package com.healerjean.proj.service.bizlog.service.impl;

import com.google.common.collect.Lists;
import com.healerjean.proj.service.bizlog.data.po.LogRecord;
import com.healerjean.proj.service.bizlog.service.ILogRecordService;
import com.healerjean.proj.utils.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.List;

/**
 * @author muzhantong
 * create on 2020/4/29 4:34 下午
 */
@Service
@Slf4j
public class DefaultLogRecordServiceImpl implements ILogRecordService {

    @Override
    public void record(LogRecord logRecord) {
        log.info("【logRecord】log={}", JsonUtils.toJsonString(logRecord));
    }

    @Override
    public List<LogRecord> queryLog(String bizKey) {
        return Lists.newArrayList();
    }

    @Override
    public List<LogRecord> queryLogByBizNo(String bizNo) {
        return Lists.newArrayList();
    }
}

3.5.1.2、IOperatorGetService

package com.healerjean.proj.service.bizlog.service;


import com.healerjean.proj.service.bizlog.data.po.Operator;

/**
 * 用户获取Service
 * @author zhangyujin
 * @date 2023/5/30  19:58
 */
public interface IOperatorGetService {

    /**
     * 可以在里面外部的获取当前登陆的用户,比如UserContext.getCurrentUser()
     *
     * @return 转换成Operator返回
     */
    Operator getUser();
}

package com.healerjean.proj.service.bizlog.service.impl;


import com.healerjean.proj.service.bizlog.data.po.Operator;
import com.healerjean.proj.service.bizlog.service.IOperatorGetService;
import org.springframework.stereotype.Service;

/**
 * @author muzhantong
 * create on 2020/4/29 5:45 下午
 */
@Service
public class DefaultOperatorGetServiceImpl implements IOperatorGetService {

    /**
     * 获取用户
     * @return Operator
     */
    @Override
    public Operator getUser() {
        Operator operator = new Operator();
        operator.setOperatorId("2222");
        return operator;
    }
}

3.5.1.3、IFunctionService

package com.healerjean.proj.service.bizlog.service;

/**
 * 函数service
 */
public interface IFunctionService {

    /**
     * apply
     * @param functionName functionName
     * @param value value
     * @return String
     */
    String apply(String functionName, String value);

    /**
     * beforeFunction
     * @param functionName functionName
     * @return boolean
     */
    boolean check(String functionName);
}

package com.healerjean.proj.service.bizlog.service.impl;


import com.healerjean.proj.service.bizlog.service.IFunctionService;
import com.healerjean.proj.service.bizlog.service.function.IParseFunction;
import com.healerjean.proj.service.bizlog.service.function.factory.ParseFunctionFactory;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * 默认函数
 * @author zhangyujin
 * @date 2023/5/30  19:56
 */
@Service
public class DefaultFunctionServiceImpl implements IFunctionService {

    /**
     * 函数解析工厂
     */
    @Resource
    private  ParseFunctionFactory parseFunctionFactory;


    /**
     * 函数执行
     * @param functionName functionName
     * @param value value
     * @return String
     */
    @Override
    public String apply(String functionName, String value) {
        IParseFunction function = parseFunctionFactory.getFunction(functionName);
        if (function == null) {
            return value;
        }
        return function.apply(value);
    }

    /**
     * beforeFunction
     * @param functionName functionName
     * @return boolean
     */
    @Override
    public boolean check(String functionName) {
        return parseFunctionFactory.isBeforeFunction(functionName);
    }
}

3.5.2、自定义函数

3.5.2.1、接口:IParseFunction

package com.healerjean.proj.service.bizlog.service.function;


/**
 * 解析函数
 */
public interface IParseFunction {

    /**
     * 校验是否执行
     *
     * @return boolean
     */
    default boolean check() {
        return false;
    }

    /**
     * 函数名
     *
     * @return String
     */
    String functionName();

    /**
     * 函数调用
     *
     * @param applyReq applyReq
     * @return String
     */
    String apply(String applyReq);
}

3.5.2.2、DefaultParseFunction

package com.healerjean.proj.service.bizlog.service.function.impl;


import com.healerjean.proj.service.bizlog.common.enums.BizLogEnum;
import com.healerjean.proj.service.bizlog.service.function.IParseFunction;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * DefaultParseFunction
 */
@Slf4j
@Service
public class DefaultParseFunction implements IParseFunction {

    /**
     * executeBefore
     *
     * @return boolean
     */
    @Override
    public boolean check() {
        return true;
    }

    /**
     * 函数名
     *
     * @return String
     */
    @Override
    public String functionName() {
        return BizLogEnum.IParseFunctionEnum.DEFAULT.getFunction();
    }

    /**
     * 函数调用
     *
     * @param applyReq applyReq
     * @return String
     */
    @Override
    public String apply(String applyReq) {
        log.info("[DefaultParseFunction#apply] applyReq:{}", applyReq);
        return null;
    }
}

3.5.2.3、OrderParseFunction

package com.healerjean.proj.service.bizlog.service.function.impl;

import com.healerjean.proj.service.bizlog.common.enums.BizLogEnum;
import com.healerjean.proj.service.bizlog.data.po.Order;
import com.healerjean.proj.service.OrderQueryService;
import com.healerjean.proj.service.bizlog.service.function.IParseFunction;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.helpers.MessageFormatter;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;

/**
 *
 */
@Slf4j
@Component
public class OrderParseFunction implements IParseFunction {
    @Resource
    private OrderQueryService orderQueryService;

    @Override
    public boolean check() {
        return true;
    }

    /**
     * 函数名称
     *
     * @return functionName
     */
    @Override
    public String functionName() {
        return BizLogEnum.IParseFunctionEnum.ORDER_PARSE_FUNCTION.getFunction();
    }

    /**
     * apply
     * @param orderId orderId
     * @return String
     */
    @Override
    public String apply(String orderId) {
        log.info("[OrderParseFunction#apply] orderId:{}", orderId);
        if (StringUtils.isEmpty(orderId)) {
            return orderId;
        }
        Order order = orderQueryService.queryOrder(Long.parseLong(orderId));
        return  MessageFormatter.arrayFormat("【产品名称:[{}]-订单号:[{}]】",
                                             new Object[]{order.getProductName(), orderId}).getMessage();
    }
}

3.5.2.4、UserParseFunction

package com.healerjean.proj.service.bizlog.service.function.impl;

import com.healerjean.proj.service.UserQueryService;
import com.healerjean.proj.service.bizlog.common.enums.BizLogEnum;
import com.healerjean.proj.service.bizlog.data.po.User;
import com.healerjean.proj.service.bizlog.service.function.IParseFunction;
import com.healerjean.proj.utils.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;

/**
 * UserParseFunction
 *
 * @author zhangyujin
 * @date 2023/5/31  16:07
 */
@Slf4j
@Component
public class UserParseFunction implements IParseFunction {

    /**
     * userQueryService
     */
    @Resource
    private UserQueryService userQueryService;


    @Override
    public String functionName() {
        return BizLogEnum.IParseFunctionEnum.USER_PARSE_FUNCTION.getFunction();
    }

    /**
     * 函数调用
     *
     * @param userId userId
     * @return String
     */
    @Override
    public String apply(String userId) {
        if (StringUtils.isEmpty(userId)) {
            return userId;
        }
        User user = userQueryService.getUser(userId);
        log.info("[UserParseFunction#apply] userId:{}, user:{}", userId, JsonUtils.toJsonString(user));
        return user.getUserId();

    }
}

3.5.3、场景 Service

3.5.3.1、UserQueryService

package com.healerjean.proj.service;


import com.healerjean.proj.service.bizlog.data.po.User;

/**
 * UserQueryService
 * @author zhangyujin
 * @date 2023/5/31  16:02
 */
public interface UserQueryService {

    /**
     * getUser
     * @param userId userId
     * @return User
     */
    User getUser(String userId);
}

package com.healerjean.proj.service.impl;

import com.healerjean.proj.service.UserQueryService;
import com.healerjean.proj.service.bizlog.anno.LogRecordAnnotation;
import com.healerjean.proj.service.bizlog.common.BizLogConstants;
import com.healerjean.proj.service.bizlog.data.po.User;
import com.healerjean.proj.service.bizlog.utils.LogTheadLocal;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;


/**
 * UserQueryServiceImpl
 * @author zhangyujin
 * @date 2023/5/31  15:58
 */
@Slf4j
@Service
public class UserQueryServiceImpl implements UserQueryService {

    /**
     * getUser
     * @param userId userId
     * @return User
     */
    @Override
    @LogRecordAnnotation(success = "获取用户列表,内层方法调用人",
            prefix = BizLogConstants.BizLogTypeConstant.ORDER_TYPE,
            bizNo = "")
    public User getUser(String userId) {
        LogTheadLocal.putVariable("user", userId);
        return null;
    }
}

3.5.3.2、IOrderService

package com.healerjean.proj.service;


import com.healerjean.proj.service.bizlog.data.po.Order;

/**
 * @author muzhantong
 * create on 2020/6/12 11:07 上午
 */
public interface IOrderService {
    boolean createOrder(Order order);

    boolean update(Long orderId, Order order);

    boolean testCondition(Long orderId, Order order, String condition);

    boolean testContextCallContext(Long orderId, Order order);
}

package com.healerjean.proj.service.impl;

import com.healerjean.proj.service.IOrderService;
import com.healerjean.proj.service.UserQueryService;
import com.healerjean.proj.service.bizlog.anno.LogRecordAnnotation;
import com.healerjean.proj.service.bizlog.common.BizLogConstants;
import com.healerjean.proj.service.bizlog.data.po.Order;
import com.healerjean.proj.service.bizlog.utils.LogTheadLocal;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * OrderServiceImpl
 * @author zhangyujin
 * @date 2023/5/31  16:10
 */
@Service
@Slf4j
public class OrderServiceImpl implements IOrderService {

    /**
     * userQueryService
     */
    @Resource
    private UserQueryService userQueryService;

    /*'张三下了一个订单,购买商品「超值优惠红烧肉套餐」,下单结果:true' */
    @Override
    @LogRecordAnnotation(
            fail = "创建订单失败,失败原因:「」",
            category = "MANAGER_VIEW",
            detail = "",
            operator = "",
            success = "下了一个订单,购买商品「」,测试变量「」,下单结果:",
            prefix = BizLogConstants.BizLogTypeConstant.ORDER_TYPE,
            bizNo = "")
    public boolean createOrder(Order order) {
        log.info("【创建订单】orderNo: {}", order.getOrderNo());
        // db insert order
        Order order1 = new Order();
        order1.setProductName("内部变量测试");
        LogTheadLocal.putVariable("innerOrder", order1);
        LogTheadLocal.putVariable("currentUser", "healerJean");
        return true;
    }

    @Override
    @LogRecordAnnotation(success = "更新了订单{orderParse{#order.orderId}},更新内容为...",
            prefix = BizLogConstants.BizLogTypeConstant.ORDER_TYPE,
            bizNo = "",
            detail = "")
    public boolean update(Long orderId, Order order) {
        order.setOrderId(10000L);
        return false;
    }

    @Override
    @LogRecordAnnotation(success = "更新了订单{orderParse{#orderId}},更新内容为...",
            prefix = BizLogConstants.BizLogTypeConstant.ORDER_TYPE,
            bizNo = "",
            condition = "")
    public boolean testCondition(Long orderId, Order order, String condition) {
        return false;
    }

    @Override
    @LogRecordAnnotation(success = "更新了订单{orderParse{#orderId}},更新内容为..}",
            prefix = BizLogConstants.BizLogTypeConstant.ORDER_TYPE,
            bizNo = "")
    public boolean testContextCallContext(Long orderId, Order order) {
        LogTheadLocal.putVariable("title", "外层调用");
        userQueryService.getUser(order.getUserId());
        return false;
    }
}

3.5.3.3、OrderQueryService

package com.healerjean.proj.service;


import com.healerjean.proj.service.bizlog.data.po.Order;

/**
 * 订单 OrderQueryService
 */
public interface OrderQueryService {

    /**
     * 查询订单
     *
     * @param orderId orderId
     * @return Order
     */
    Order queryOrder(long orderId);
}

package com.healerjean.proj.service.impl;

import com.healerjean.proj.service.bizlog.data.po.Order;
import com.healerjean.proj.service.OrderQueryService;
import com.healerjean.proj.utils.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

/**
 * OrderQueryServiceImpl
 *
 * @author zhangyujin
 * @date 2023/5/31  15:55
 */
@Slf4j
@Service
public class OrderQueryServiceImpl implements OrderQueryService {

    /**
     * 查询订单信息
     *
     * @param orderId orderId
     * @return Order
     */
    @Override
    public Order queryOrder(long orderId) {
        Order order = new Order();
        order.setProductName("大疆飞机");

        log.info("[OrderQueryService#queryOrder] orderId:{}, order:{}", orderId, JsonUtils.toJsonString(order));
        return order;
    }
}

3.6、工具

3.6.1、LogTheadLocal

package com.healerjean.proj.service.bizlog.utils;

import com.google.common.collect.Maps;

import java.util.HashMap;
import java.util.Map;
import java.util.Stack;


/**
 * 日志上线文
 * 使用了 InheritableThreadLocal,所以在线程池的场景下使用 LogRecordContext 会出现问题,如果支持线程池可以使用阿里巴巴开源的 TTL 框架
 */
public class LogTheadLocal {

    /**
     * VARIABLE_MAP_STACK
     */
    private static final InheritableThreadLocal<Stack<Map<String, Object>>> VARIABLE_MAP_STACK = new InheritableThreadLocal<>();

    /**
     * putVariable
     *
     * @param name  name
     * @param value value
     */
    public static void putVariable(String name, Object value) {
        if (VARIABLE_MAP_STACK.get() == null) {
            Stack<Map<String, Object>> stack = new Stack<>();
            VARIABLE_MAP_STACK.set(stack);
        }
        Stack<Map<String, Object>> mapStack = VARIABLE_MAP_STACK.get();
        if (mapStack.size() == 0) {
            VARIABLE_MAP_STACK.get().push(new HashMap<>());
        }
        VARIABLE_MAP_STACK.get().peek().put(name, value);
    }

    /**
     * getVariable
     *
     * @param key key
     * @return Object
     */
    public static Object getVariable(String key) {
        Map<String, Object> variableMap = VARIABLE_MAP_STACK.get().peek();
        return variableMap.get(key);
    }

    /**
     * getVariables
     *
     * @return Map<String, Object>
     */
    public static Map<String, Object> getVariables() {
        Stack<Map<String, Object>> mapStack = VARIABLE_MAP_STACK.get();
        return mapStack.peek();
    }

    /**
     * clear
     */
    public static void clear() {
        if (VARIABLE_MAP_STACK.get() != null) {
            VARIABLE_MAP_STACK.get().pop();
        }
    }

    /**
     * 日志使用方不需要使用到这个方法
     * 每进入一个方法初始化一个 span 放入到 stack 中,方法执行完后 pop 掉这个span
     */
    public static void putEmptySpan() {
        Stack<Map<String, Object>> mapStack = VARIABLE_MAP_STACK.get();
        if (mapStack == null) {
            Stack<Map<String, Object>> stack = new Stack<>();
            VARIABLE_MAP_STACK.set(stack);
        }
        VARIABLE_MAP_STACK.get().push(Maps.newHashMap());

    }
}

3.7、SPEL 解析

3.7.1、LogEvaluationContext

package com.healerjean.proj.service.bizlog.service.parse;

import com.healerjean.proj.service.bizlog.data.BizLogContext;
import com.healerjean.proj.service.bizlog.utils.LogTheadLocal;
import org.springframework.context.expression.MethodBasedEvaluationContext;
import org.springframework.core.ParameterNameDiscoverer;

import java.util.Map;
import java.util.Objects;

/**
 * @author zhangyujin
 * @date 2023/5/30  19:42.
 */
public class LogEvaluationContext extends MethodBasedEvaluationContext {


    /**
     * 1、初始化 EvaluationContext
     * 2、获取本地内存变量数据放到上下文
     * 3、获取被切面的方法执行结果,放入上下文
     * LogEvaluationContext
     *
     * @param parameterNameDiscoverer parameterNameDiscoverer
     * @param logContext              logContext
     */
    public LogEvaluationContext(ParameterNameDiscoverer parameterNameDiscoverer, BizLogContext logContext) {
        // 1、初始化 EvaluationContext
        super(null, logContext.getMethod(), logContext.getArgs(), parameterNameDiscoverer);

        // 2、获取本地内存变量数据放到上下文
        Map<String, Object> variables = LogTheadLocal.getVariables();
        if (variables != null && variables.size() > 0) {
            for (Map.Entry<String, Object> entry : variables.entrySet()) {
                setVariable(entry.getKey(), entry.getValue());
            }
        }

        // 3、获取被切面的方法执行结果,放入上下文
        BizLogContext.MethodExecuteResult methodExecuteResult = logContext.getMethodExecuteResult();
        Object result = null;
        String errorMsg = null;
        if (Objects.nonNull(methodExecuteResult)) {
            result = methodExecuteResult.getResult();
            errorMsg = methodExecuteResult.getErrorMsg();
        }
        setVariable("_ret", result);
        setVariable("_errorMsg", errorMsg);
    }
}

3.7.2、LogExpressionEvaluator

package com.healerjean.proj.service.bizlog.service.parse;

import com.healerjean.proj.service.bizlog.data.BizLogContext;
import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.context.expression.BeanFactoryResolver;
import org.springframework.context.expression.CachedExpressionEvaluator;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;

import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 日志表达式解析器
 *
 * @author zhangyujin
 * @date 2023/5/30  19:35.
 */

public class LogExpressionEvaluator extends CachedExpressionEvaluator {

    /**
     * 表达式缓存
     */
    private Map<ExpressionKey, Expression> expressionCache = new ConcurrentHashMap<>(64);

    /**
     * 方法缓存
     */
    private final Map<AnnotatedElementKey, Method> targetMethodCache = new ConcurrentHashMap<>(64);


    /**
     * 创建解析上下文 EvaluationContext
     *
     * @param bizLogContext bizLogContext
     * @return createEvaluationContext
     */
    public EvaluationContext createEvaluationContext(BizLogContext bizLogContext) {
        LogEvaluationContext evaluationContext = new LogEvaluationContext(getParameterNameDiscoverer(), bizLogContext);
        BeanFactory beanFactory = bizLogContext.getBeanFactory();
        evaluationContext.setBeanResolver(new BeanFactoryResolver(beanFactory));
        return evaluationContext;
    }


    /***
     * 表达式解析
     * @param conditionExpression conditionExpression
     * @param methodKey methodKey
     * @param evalContext evalContext
     * @return String
     */
    public String parseExpression(String conditionExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) {
        Object value = getExpression(this.expressionCache, methodKey, conditionExpression).getValue(evalContext, Object.class);
        return value == null ? "" : value.toString();
    }


    /**
     * getTargetMethod
     *
     * @param targetClass targetClass
     * @param method      method
     * @return
     */
    private Method getTargetMethod(Class<?> targetClass, Method method) {
        AnnotatedElementKey methodKey = new AnnotatedElementKey(method, targetClass);
        Method targetMethod = this.targetMethodCache.get(methodKey);
        if (targetMethod == null) {
            targetMethod = AopUtils.getMostSpecificMethod(method, targetClass);
            this.targetMethodCache.put(methodKey, targetMethod);
        }
        return targetMethod;
    }
}

3.7.3、LogValueParser

package com.healerjean.proj.service.bizlog.service.parse;

import com.google.common.base.Strings;
import com.healerjean.proj.service.bizlog.data.BizLogContext;
import com.healerjean.proj.service.bizlog.service.IFunctionService;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.context.expression.AnnotatedElementKey;
import org.springframework.expression.EvaluationContext;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author zhangyujin
 * @date 2023/5/30  19:47.
 */
@Service
public class LogValueParser {

    /**
     * expressionEvaluator
     */
    private final LogExpressionEvaluator expressionEvaluator = new LogExpressionEvaluator();

    /**
     * pattern
     */
    private static final Pattern pattern = Pattern.compile("\\{\\s*(\\w*)\\s*\\{(.*?)}}");


    /**
     * functionService
     */
    @Resource
    private IFunctionService functionService;

    /**
     * beanFactory
     */
    @Resource
    private BeanFactory beanFactory;


    /**
     * 函数解析结果Map
     * 1、获取解析上下文 EvaluationContext
     * 2、根据模版解析SPEL内置函数
     *
     * @param bizLogContext bizLogContext
     * @return Map<String, String>
     */
    public Map<String, String> buildFunctionResult(BizLogContext bizLogContext) {
        bizLogContext.setBeanFactory(beanFactory);
        Map<String, String> functionReturnMap = new HashMap<>();
        EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(bizLogContext);

        List<String> templates = bizLogContext.getSpElTemplates();
        Method method = bizLogContext.getMethod();
        Class<?> targetClass = bizLogContext.getTargetClass();
        for (String expressionTemplate : templates) {
            if (expressionTemplate.contains("{")) {
                Matcher matcher = pattern.matcher(expressionTemplate);
                while (matcher.find()) {
                    // 解析spel表达式
                    String expression = matcher.group(2);
                    if (expression.contains("#_ret") || expression.contains("#_errorMsg")) {
                        continue;
                    }
                    AnnotatedElementKey annotatedElementKey = new AnnotatedElementKey(method, 
                                                                                      targetClass);
                    //解析函数名
                    String functionName = matcher.group(1);
                    if (functionService.check(functionName)) {
                        String value = expressionEvaluator.parseExpression(expression, 
                                                                           annotatedElementKey, 
                                                                           evaluationContext);
                        String functionReturnValue = getFunctionReturnValue(null, value, functionName);
                        functionReturnMap.put(functionName, functionReturnValue);
                    }
                }
            }
        }
        return functionReturnMap;

    }


    /**
     * 获取函数执行结果
     *
     * @param functionReturnMap 函数返回结果
     * @param value             value
     * @param functionName      functionName
     * @return
     */
    private String getFunctionReturnValue(Map<String, String> functionReturnMap, String value, String functionName) {
        String functionReturnValue = "";
        if (functionReturnMap != null) {
            functionReturnValue = functionReturnMap.get(functionName);
        }
        if (StringUtils.isEmpty(functionReturnValue)) {
            functionReturnValue = functionService.apply(functionName, value);
        }
        return functionReturnValue;
    }


    /**
     * 解析SPEL模版
     * 1、获取解析上下文 EvaluationContext
     * 2、解析模版
     * @param bizLogContext bizLogContext
     * @return Map<String, String>
     */
    public Map<String, String> processTemplate(BizLogContext bizLogContext) {
        Map<String, String> expressionValues = new HashMap<>();

        // 1、获取解析上下文 EvaluationContext
        EvaluationContext evaluationContext = expressionEvaluator.createEvaluationContext(bizLogContext);


        // 2、解析SPEL模版
        List<String> templates = bizLogContext.getSpElTemplates();
        for (String expressionTemplate : templates) {
            if (expressionTemplate.contains("{")) {
                Matcher matcher = pattern.matcher(expressionTemplate);
                StringBuffer parsedStr = new StringBuffer();
                while (matcher.find()) {
                    // 1、依次获取Spel表达式
                    String expression = matcher.group(2);
                    AnnotatedElementKey annotatedElementKey = new AnnotatedElementKey(
                      bizLogContext.getMethod(), 
                      bizLogContext.getTargetClass());

                    // 2、解析spel表达式的数据
                    String value = expressionEvaluator.parseExpression(expression, 
                                                                       annotatedElementKey, 
                                                                       evaluationContext);

                    // 3、获取表达式的函数名进行函数编辑
                    String functionName = matcher.group(1);
                    String functionReturnValue = getFunctionReturnValue(bizLogContext.
                                                                        getFunctionReturnMap(), 
                                                                        value, 
                                                                        functionName);

                    // 4、将输入字符序列首次与正在表达式匹配的部分进行更改为replaceMent并且把结果添加到一个sb结果集中
                    matcher.appendReplacement(parsedStr, Strings.nullToEmpty(functionReturnValue));
                }
                matcher.appendTail(parsedStr);
                expressionValues.put(expressionTemplate, parsedStr.toString());
            } else {
                expressionValues.put(expressionTemplate, expressionTemplate);
            }

        }
        return expressionValues;
    }

}

3.8、切面加日志服务

3.8.1、BizLogAspect

package com.healerjean.proj.service.bizlog.aspect;

import com.healerjean.proj.service.bizlog.anno.LogRecordAnnotation;
import com.healerjean.proj.service.bizlog.data.BizLogContext;
import com.healerjean.proj.service.bizlog.utils.LogTheadLocal;
import com.healerjean.proj.service.bizlog.service.BizLogService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.lang.reflect.Method;

/**
 * 业务日志切面
 *
 * @author zhangyujin
 * @date 2023/5/30  18:54.
 */
//todo
@Slf4j
@Component
@Aspect
@Order(2)
public class BizLogAspect {


    @Resource
    private BizLogService bizLogService;

    /**
     * 1、切面方法执行
     * 2、切面日志记录
     * 3、结果返回
     *
     * @param pjp                 pjp
     * @param logRecordAnnotation logRecordAnnotation
     * @return Object
     * @throws Throwable
     */
    @Around(value = "(execution(* *(..)) && @annotation(logRecordAnnotation))", argNames = "pjp,logRecordAnnotation")
    public Object around(final ProceedingJoinPoint pjp, 
                         LogRecordAnnotation logRecordAnnotation) throws Throwable {
        Signature sig = pjp.getSignature();
        Method method = null;
        Class<?> clazz = null;
        Object[] args = pjp.getArgs();
        if ((sig instanceof MethodSignature)) {
            MethodSignature signature = (MethodSignature) sig;
            Object target = pjp.getTarget();
            clazz = pjp.getTarget().getClass();
            method = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
        } else {
            log.error("signature is not instanceof MethodSignature!");
        }
        if (method == null) {
            return pjp.proceed();
        }


        // 1、切面方法执行
        Object result = null;
        BizLogContext.MethodExecuteResult methodExecuteResult = new BizLogContext
                .MethodExecuteResult()
                .setSuccess(true);

        LogTheadLocal.putEmptySpan();
        try {
            result = pjp.proceed();
        } catch (Exception e) {
            methodExecuteResult.setSuccess(false).setErrorMsg(e.getMessage()).setThrowable(e);
        }

        // 2、切面日志记录
        BizLogContext bizLogContext = new BizLogContext();
        bizLogContext.setMethod(method);
        bizLogContext.setTargetClass(clazz);
        bizLogContext.setArgs(args);
        bizLogContext.setLogRecordAnnotation(logRecordAnnotation);
        bizLogContext.setMethodExecuteResult(methodExecuteResult);
        try {
            bizLogService.bizLogExecute(bizLogContext);
        }catch (Exception e){
            log.error("[BizLogAspect#around] 操作日志记录异常", e);
        }finally {
            LogTheadLocal.clear();
        }

        // 3、结果返回
        if (methodExecuteResult.getThrowable() != null) {
            throw methodExecuteResult.getThrowable();
        }
        return result;
    }

}

3.8.2、BizLogService

package com.healerjean.proj.service.bizlog.service;

import com.healerjean.proj.service.bizlog.data.BizLogContext;

/**
 * BizLogService
 * @author zhangyujin
 * @date 2023/5/30  18:59.
 */
public interface BizLogService {

    /**
     * 业务日志执行
     */
     void bizLogExecute(BizLogContext bizLogContext);
}

package com.healerjean.proj.service.bizlog.service.impl;

import com.healerjean.proj.service.bizlog.anno.LogRecordAnnotation;
import com.healerjean.proj.service.bizlog.data.BizLogContext;
import com.healerjean.proj.service.bizlog.data.bo.LogRecordBO;
import com.healerjean.proj.service.bizlog.data.po.LogRecord;
import com.healerjean.proj.service.bizlog.service.parse.LogValueParser;
import com.healerjean.proj.service.bizlog.service.BizLogService;
import com.healerjean.proj.service.bizlog.service.ILogRecordService;
import com.healerjean.proj.service.bizlog.service.IOperatorGetService;
import com.healerjean.proj.utils.JsonUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;

/**
* @author zhangyujin
* @date 2023/5/30  18:59.
*/
@Slf4j
@Service
public class BizLogServiceImpl implements BizLogService {

  /**
   * logValueParser
   */
  @Resource
  private LogValueParser logValueParser;

  /**
   * operatorGetService
   */
  @Resource
  private IOperatorGetService operatorGetService;

  /**
   * bizLogService
   */
  @Resource
  private ILogRecordService bizLogService;

  /**
   * 业务日志执行
   * 1、获取日志对象
   * 2、获取所有的SPEL模版
   * 3、获取所有的SPEL函数结果
   * 4、日志处理
   */
  @Override
  public void bizLogExecute(BizLogContext bizLogContext) {
      // 1、获取日志对象
      LogRecordBO logRecordBo = buildLogRecordBO(bizLogContext);

      // 2、获取所有的SPEL模版
      List<String> spElTemplates = buildBeforeExecuteFunctionTemplate(logRecordBo);
      bizLogContext.setSpElTemplates(spElTemplates);

      // 3、获取所有的SPEL函数结果
      Map<String, String> functionReturnMap = logValueParser.buildFunctionResult(bizLogContext);
      bizLogContext.setFunctionReturnMap(functionReturnMap);

      // 4、日志处理
      logExecute(logRecordBo, bizLogContext);
  }


  /**
   * 记录结果
   * 1、根据切面日志结果获取成功或者失败标识模版
   * 2、获取需要解析的表达式
   * 3、解析表达式
   * 4、日志数据处理
   *
   * @param bizLogContext bizLogContext
   */
  private void logExecute(LogRecordBO logRecordBo, BizLogContext bizLogContext) {
      // 1、根据切面日志结果获取成功或者失败标识模版
      BizLogContext.MethodExecuteResult executeResult = bizLogContext.getMethodExecuteResult();
      String bizResult;
      if (executeResult.isSuccess()) {
          bizResult = logRecordBo.getSuccessLogTemplate();
      } else {
          bizResult = logRecordBo.getFailLogTemplate();
      }
      if (StringUtils.isEmpty(bizResult)) {
          return;
      }

      // 2、获取需要解析的表达式
      List<String> spElTemplates = new ArrayList<>();
      spElTemplates.add(bizResult);
      spElTemplates.add(logRecordBo.getBizKey());
      spElTemplates.add(logRecordBo.getBizNo());
      spElTemplates.add(logRecordBo.getDetail());
      if (StringUtils.isNotBlank(logRecordBo.getCondition())) {
          spElTemplates.add(logRecordBo.getCondition());
      }
      if (StringUtils.isNotBlank(logRecordBo.getOperatorId())) {
          spElTemplates.add(logRecordBo.getOperatorId());
      }
      bizLogContext.setSpElTemplates(spElTemplates);
      String runOperator = "";
      if (StringUtils.isBlank(logRecordBo.getOperatorId())) {
          runOperator = operatorGetService.getUser().getOperatorId();
          if (StringUtils.isEmpty(runOperator)) {
              throw new IllegalArgumentException("操作人获取失败");
          }
      }

      // 3、解析表达式
      Map<String, String> expressionValues = logValueParser.processTemplate(bizLogContext);


      // 4、日志数据处理
      boolean saveLogFlag = false;
      if (StringUtils.isBlank(logRecordBo.getCondition()) 
          || StringUtils.endsWithIgnoreCase(expressionValues.get(logRecordBo.getCondition()), "true")) {
          saveLogFlag = true;
      }

      if (Boolean.FALSE.equals(saveLogFlag)) {
          log.info(" 不存储日志 logRecord:{} ", JsonUtils.toJsonString(logRecordBo));
          return;
      }
      LogRecord logRecord = LogRecord.builder()
              .bizKey(expressionValues.get(logRecordBo.getBizKey()))
              .bizNo(expressionValues.get(logRecordBo.getBizNo()))
              .operator(getRealOperatorId(logRecordBo, runOperator, expressionValues))
              .category(logRecordBo.getCategory())
              .detail(expressionValues.get(logRecordBo.getDetail()))
              .action(expressionValues.get(bizResult))
              .createTime(new Date())
              .build();

      //如果 action 为空,不记录日志
      if (StringUtils.isEmpty(logRecord.getAction())) {
          return;
      }
      bizLogService.record(logRecord);

  }


  /**
   * 获取日志对象集合
   *
   * @param bizLogContext bizLogContext
   * @return buildLogRecordBO
   */
  private LogRecordBO buildLogRecordBO(BizLogContext bizLogContext) {
      LogRecordAnnotation recordAnnotation = bizLogContext.getLogRecordAnnotation();
      if (StringUtils.isBlank(recordAnnotation.success()) && StringUtils.isBlank(recordAnnotation.fail())) {
          throw new IllegalStateException("成功或者失败模版必须有一个");
      }
      return LogRecordBO.builder()
              .successLogTemplate(recordAnnotation.success())
              .failLogTemplate(recordAnnotation.fail())
              .bizKey(recordAnnotation.prefix().concat("_").concat(recordAnnotation.bizNo()))
              .bizNo(recordAnnotation.bizNo())
              .operatorId(recordAnnotation.operator())
              .category(StringUtils.isEmpty(recordAnnotation.category()) ? 
                        recordAnnotation.prefix() : recordAnnotation.category())
              .detail(recordAnnotation.detail())
              .condition(recordAnnotation.condition())
              .build();
  }

  /**
   * getBeforeExecuteFunctionTemplate
   *
   * @param logRecordBo logRecordBo
   * @return List<String>
   */
  private List<String> buildBeforeExecuteFunctionTemplate(LogRecordBO logRecordBo) {
      List<String> templates = new ArrayList<>();
      templates.add(logRecordBo.getBizKey());
      templates.add(logRecordBo.getBizNo());
      templates.add(logRecordBo.getDetail());
      if (StringUtils.isNotBlank(logRecordBo.getCondition())) {
          templates.add(logRecordBo.getCondition());
      }
      templates.add(logRecordBo.getSuccessLogTemplate());
      return templates;
  }

  private String getRealOperatorId(LogRecordBO operation, 
                                   String operatorIdFromService, 
                                   Map<String, String> expressionValues) {
      return !StringUtils.isEmpty(operatorIdFromService) ? 
        operatorIdFromService : expressionValues.get(operation.getOperatorId());
  }
}

3.9、测试

3.9.1、createOrder



@Test
public void createOrder() {
Order order = new Order();
order.setOrderNo("MT0000011");
order.setProductName("超值优惠红烧肉套餐");
order.setPurchaseName("张三");
boolean ret = orderService.createOrder(order);
Assert.assertTrue(ret);
}


@LogRecordAnnotation(
fail = "创建订单失败,失败原因:「」",
category = "MANAGER_VIEW",
detail = "",
operator = "",
success = "下了一个订单,购买商品「」,测试变量「」,下单结果:",
prefix = BizLogConstants.BizLogTypeConstant.ORDER_TYPE,
bizNo = "")


{"bizKey":"OrderType_MT0000011","bizNo":"MT0000011","operator":"healerJean","action":"张三下了一个订单,购买商品「超值优惠红烧肉套餐」,测试变量「内部变量测试」,下单结果:","category":"MANAGER_VIEW","createTime":1685540558475,"detail":"Order(orderId=null, orderNo=MT0000011, purchaseName=张三, productName=超值优惠红烧肉套餐, createTime=null, userId=null)"}

3.9.2、updateOrder

@Test
public void updateOrder() {
Order order = new Order();
order.setOrderId(99L);
order.setOrderNo("MT0000011");
order.setProductName("超值优惠红烧肉套餐");
order.setPurchaseName("张三");
boolean ret = orderService.update(1L, order);
}


@LogRecordAnnotation(
success = "更新了订单{orderParse{#order.orderId}},更新内容为...",
prefix = BizLogConstants.BizLogTypeConstant.ORDER_TYPE,
bizNo = "",
detail = "")


{"bizKey":"OrderType_MT0000011","bizNo":"MT0000011","operator":"2222","action":"更新了订单【产品名称:[大疆飞机]-订单号:[10000]】,更新内容为...","category":"OrderType","createTime":1685540659902,"detail":"Order(orderId=10000, orderNo=MT0000011, purchaseName=张三, productName=超值优惠红烧肉套餐, createTime=null, userId=null)"}

3.9.3、testCondition_打印日志

@Test
public void testCondition_打印日志() {
Order order = new Order();
order.setOrderNo("MT0000011");
order.setProductName("超值优惠红烧肉套餐");
order.setPurchaseName("张三");
boolean ret = orderService.testCondition(1L, order, null);

}


@LogRecordAnnotation(
success = "更新了订单{orderParse{#orderId}},更新内容为...",
prefix = BizLogConstants.BizLogTypeConstant.ORDER_TYPE,
bizNo = "",
condition = "")



2023-05-31 21:45:36 [main] INFO  -[                                ] -  com.healerjean.proj.service.bizlog.service.impl.DefaultLogRecordServiceImpl.record[22] - logRecordlog={"bizKey":"OrderType_MT0000011","bizNo":"MT0000011","operator":"2222","action":"更新了订单【产品名称:[大疆飞机]-订单号:[1]】,更新内容为...","category":"OrderType","createTime":1685540736780,"detail":""}

3.9.4、testCondition_不打印日志

@Test
public void testCondition_不打印日志() {
Order order = new Order();
order.setOrderNo("MT0000011");
order.setProductName("超值优惠红烧肉套餐");
order.setPurchaseName("张三");
boolean ret = orderService.testCondition(1L, order, "sss");
// 打印日志
}

3.9.5、testContextCallContext

@Test
public void testContextCallContext() {
Order order = new Order();
order.setOrderNo("MT0000011");
order.setProductName("超值优惠红烧肉套餐");
order.setPurchaseName("张三");
boolean ret = orderService.testContextCallContext(1L, order);
// 打印两条日志
}

@LogRecordAnnotation(
success = "更新了订单{orderParse{#orderId}},更新内容为..}",
prefix = BizLogConstants.BizLogTypeConstant.ORDER_TYPE,
bizNo = "")


{"bizKey":"OrderType_","bizNo":"","operator":"2222","action":"获取用户列表,内层方法调用人","category":"OrderType","createTime":1685540808942,"detail":""}

{"bizKey":"OrderType_MT0000011","bizNo":"MT0000011","operator":"2222","action":"更新了订单【产品名称:[大疆飞机]-订单号:[1]】,更新内容为..外层调用}","category":"OrderType","createTime":1685540808974,"detail":""}

ContactAuthor