SpringBoot实际项目中 如何基于切面的方式对业务操作日志进行记录

  • Post author:
  • Post category:其他


首先明确需要明确一下业务场景,对于每个接口的请求日志,我们业务是统一在网关层进行记录处理,本文描述的是指业务在执行过程中,产生的业务操作记录,一般是指事务型操作的执行,往往伴随着业务状态的转变,主要用于判断

当前业务的进行轨迹及相应的操作信息



对于我们的业务系统来说,业务是一个众包接单平台,其中各类业务状态的的转换很多,很多业务需求需要记录业务状态的转换,如果每种业务转换都单独写操作记录的拼装存储逻辑,缺点会很明显:


1.核心逻辑和记录操作逻辑耦合,不符合单一职责

2.开发代码繁琐,每次记录都需要把拼装存储逻辑写一遍,无法逻辑复用

3.十分不优雅

基于这些情况,很容易就可以想到通过切面编程也就是AOP对核心代码及非关注点的代码进行分离解耦。

那么对于一个SpringBoot项目,一般编写AOP逻辑步骤如下



1. 定义一个注解

2. 编写切面逻辑,切点、横切逻辑、织入

3. 使用注解



定义一个注解

@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OpLogRecord {
 /**
     * 业务Id
     * @return 业务Id
      */
    String bizId() default "";

    /**
     * 操作名称
     * @return 操作名称
     */
    String opName() default "";

    /**
     * 操作类型
     * @return 操作类型
     */
    EnumOperationRecordType recordType() default EnumOperationRecordType.DEAFULT;

    /**
     * 执行模式 默认异步执行
     * @return 执行模式
     */
    LogExecutionTypeEnum executionType() default LogExecutionTypeEnum.ASYNC;

    /**
     * 操作备注
     * @return 操作备注
     */
    String remark() default "";

    /**
     * 创建人ID
     * @return 创建人ID
     */
    String creatorId() default "";

    /**
     * 创建人姓名
     * @return 创建人姓名
     */
    String creatorName() default "";

    /**
     * 将请求转换为标准的日志格式
     * @return 转换器
     */
    Class<? extends LogConvert> converter() default LogConvert.class;
}

根据当前业务需要注解定义如上,其中有两个字段需要说明下


1.LogExecutionTypeEnum 执行模式

SYNC同步:需要和业务操作绑定同一事务

ASYNC异步:线程池异步执行,不影响核心业务操作

/**
 * description 操作日志记录执行模式
 * @author zhanghailang
 * @date 2023/02/15 9:01
 */
@Getter
public enum LogExecutionTypeEnum {

    /**
     * 异步执行
     */
    ASYNC("ASYNC"),

    /**
     * 同步执行
     */
    SYNC("SYNC"),
    ;

    private final String value;

    LogExecutionTypeEnum(String value) {
        this.value = value;
    }
}


2.转换模式 converter() default LogConvert.class

== 拓展点 ==:解析逻辑在

OpLogRecordAop

中,可根据接口需要,从参数或者返回值中进行操作日志实体的构建,也适用复杂逻辑的操作日志的构建。

/**
 * 操作日志记录转换器
 * 实现类以Log开头 Converter结尾
 * example
 * @see LogAddRecordConverter
 * @author hailang.zhang
 * @since 2023-02-14
 */
public interface LogConvert<T, R> {

     /**
      * 接口执行前转换
      * @param value 一般为请求参数
      * @return 操作日志
      */
     GigOperationRecordDO beforeConvert(T value);

     /**
      * 接口执行后转换
      * @param value 一般为返回结果
      * @param param 操作日志
      * @return 操作日志
      */
     GigOperationRecordDO afterConvert(R value, GigOperationRecordDO param);
}

注解的这个属性设计主要用于较为复杂的日志实体转换,如果方法的入参字段不符合日志实体字段,比如字段名称不一致,或者日志数据需要根据入参字段来查询业务数据,例如下图需要查询审核结果的状态来进行日志记录的完善



切面逻辑

OpLogRecordAop
/**
 * 操作日记记录切面
 * @author hailang.zhang
 * @since 2023-02-14
 */
@Component
@Aspect
@Slf4j
public class OpLogRecordAop {

    @Resource
    private OperationRecordGateway operationRecordGateway;

    private static final ThreadPoolExecutor EXECUTOR = new ThreadPoolExecutor(
            1,
            5,
            10,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(1000),
            new NamedThreadFactory("log_record_thread_", false));

    @Around("@annotation(opLogRecord)")
    public Object around(ProceedingJoinPoint joinPoint, OpLogRecord opLogRecord) throws Throwable {
        //实际方法执行
        Object proceed = joinPoint.proceed();

        //同步执行日志记录
        if (opLogRecord.executionType() == LogExecutionTypeEnum.SYNC) {
            this.sync(joinPoint, opLogRecord, proceed);
        }

        //异步执行日志记录
        if (opLogRecord.executionType() == LogExecutionTypeEnum.ASYNC) {
            this.async(joinPoint, opLogRecord, proceed);
        }

        return proceed;
    }

    /**
     * 同步执行
     * @param joinPoint 切面
     * @param opLogRecord 配置数据
     * @param proceed 执行结果
     * @throws Throwable 异常
     */
    private void sync(ProceedingJoinPoint joinPoint, OpLogRecord opLogRecord, Object proceed) throws Throwable {
        execute(joinPoint, opLogRecord, proceed);
        throw new RuntimeException("test1111");
    }

    /**
     * 异步执行
     * @param joinPoint 切面
     * @param opLogRecord 配置数据
     * @param proceed 执行结果
     * @throws Throwable 异常
     */
    private void async(ProceedingJoinPoint joinPoint, OpLogRecord opLogRecord, Object proceed) {
        //异步执行
        EXECUTOR.execute(() -> {
            try {
                execute(joinPoint, opLogRecord, proceed);
            } catch (Throwable e) {
                log.error("record log failed: {}", ExceptionUtil.stacktraceToString(e));
            }
        });
    }

    /**
     * 执行
     * @param joinPoint 切面
     * @param opLogRecord 配置数据
     * @param proceed 执行结果
     * @throws Throwable 异常
     */
    private void execute(ProceedingJoinPoint joinPoint, OpLogRecord opLogRecord, Object proceed) throws Throwable {
        GigOperationRecordDO opRecord = createLogRecord(joinPoint, opLogRecord, proceed);
        this.operationRecordGateway.saveOperationRecord(opRecord);
    }

    /**
     * 创建操作日志实体 两种模式
     * 1:从注解数据中解析spel表达式获取日志实体
     * 2:从LogConvert实现类中获取日志实体
     * @param joinPoint 切面
     * @param opLogRecord 配置数据
     * @param proceed 执行结果
     * @return 操作日志实体
     * @throws Throwable 异常
     */
    private GigOperationRecordDO createLogRecord(ProceedingJoinPoint joinPoint, OpLogRecord opLogRecord, Object proceed) throws Throwable {
        Class<? extends LogConvert> converter = opLogRecord.converter();
        Object arg = joinPoint.getArgs()[0];
        if (converter == LogConvert.class) {
            //模式1:从注解数据中解析spel表达式获取日志实体
            return createByParseSpelExpression(opLogRecord, arg);
        } else {
            //模式2:从LogConvert实现类中获取日志实体
            LogConvert logConvert = null;
            try {
                //如果是被Spring管理的Bean
                logConvert = SpringUtil.getBean(opLogRecord.converter());
            } catch (Exception e) {
                log.info("log bean not find");
            }
            if (logConvert == null) {
                logConvert = converter.newInstance();
            }
            GigOperationRecordDO result = logConvert.beforeConvert(arg);
            result = logConvert.afterConvert(proceed, result);
            return result;
        }
    }

    /**
     * 从注解数据中解析表达式获取日志实体
     * @param opLogRecord 注解配置数据
     * @param arg 方法请求参数
     * @return 日志实体
     */
    private GigOperationRecordDO createByParseSpelExpression(OpLogRecord opLogRecord, Object arg) {
        GigOperationRecordDO result = new GigOperationRecordDO();
        SpelExpressionParser parser = new SpelExpressionParser();
        result.setBizId((String) parser.parseExpression(opLogRecord.bizId(), ParserContext.TEMPLATE_EXPRESSION).getValue(arg));
        result.setCreatorId((Long) parser.parseExpression(opLogRecord.creatorId(), ParserContext.TEMPLATE_EXPRESSION).getValue(arg));
        result.setCreatorName((String) parser.parseExpression(opLogRecord.creatorName(), ParserContext.TEMPLATE_EXPRESSION).getValue(arg));
        result.setRecordType(opLogRecord.recordType().getValue());
        result.setRemark((String) parser.parseExpression(opLogRecord.remark(), ParserContext.TEMPLATE_EXPRESSION).getValue(arg));
        result.setOpName(opLogRecord.opName());
        result.setCreateTime(Instant.now());
        return result;
    }
}



实际业务注解使用


converter模式

比较简单直接加上注解,定义实体转换的converter即可

在这里插入图片描述


spel表达式模式

需要把构建日志需要的字段,在注解的属性上一一填写

在这里插入图片描述



版权声明:本文为zhanghailang1原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。