通用操作日志处理方案

news2024/9/23 21:29:45

why(目的理念):操作日志是什么需要做哪些事情?

摘自美团博客的操作日志的介绍

操作日志的记录格式大概分为下面几种:

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

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

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

简单总结

总结简单来说某些业务的关键操作为了流程的展示/安全/追溯详细操作记录 的需要, 记录每次操作(操作也可能是批量的)变更的值(可能不是单表)。 实际的操作类型的话常见的大致有:新增、更新、删除、导入、上传等。

希望要实现的效果

例如系统中需要实现的是这样的效果:

how(方法措施):记录操作日志的常见方案有哪些?

  • 监听数据库binlog记录操作日志

通过采用监听数据库 Binlog 的方式,这样可以从底层知道是哪些数据做了修改,然后根据更改的数据记录操作日志(Canal 是一款基于 MySQL 数据库增量日志解析,提供增量数据订阅和消费的开源组件。)。

这种方式的优点是和业务逻辑完全分离。缺点也很明显,局限性太高,只能针对数据库的更改做操作日志记录,如果修改涉及到其他团队的 RPC 的调用,就没办法监听数据库了,举个例子:给用户发送通知,通知服务一般都是公司内部的公共组件,这时候只能在调用 RPC 的时候手工记录发送通知的操作日志了。并且也不支持跨表,并且需要处理记录的数据库字段注释名称。这种比较适合单表单纯记录数据库字段变更。

  • 打印日志的方式记录

这个意思就是直接在方法中打印日志,每个不同的操作日志的话设定不同的模板,然后在每个需要打印操作日志打印前,比对操作前和操作后的值,然后这种操作日志特殊处理到某个指定文件中,然后通过日志收集处理可以把日志保存在 Elasticsearch 或者数据库中,生成可读的操作日志。可能需要大数据或者专门处理日志文件的人员统一处理这个操作日志的开发人员。

  • 直接在代码中记录操作日志

这个和上面的操作类似,只不过就是直接比对并直接记录到数据库或者其他地方而不是打印到日志里面。 无需其他人员介入和学习成本。

  • 方法注解实现操作日志

通过方法注解,通过AOP拦截的方式记录日志,让操作日志和业务逻辑解耦。我们可以在注解的操作日志上记录固定文案,这样业务逻辑和业务代码可以做到解耦,让我们的业务代码变得纯净起来。该种方式可以自定义处理支持跨表、批量和字段注释。不过实现起来较为复杂。要考虑各种场景是否要进行处理和如何处理,比如比对两个实体时候,要不要支持实现比对实体中嵌套的实体。对象类型的不同处理。

方案对比,自己整理的有些地方可能存疑或者问题:

支持/方案

监听数据库binlog(Canal)

通过打印日志的方式

代码中记录操作日志

方法注解实现操作日志

字段注释支持(应该只能数据库字段的注释),不能自定义配置不友好支持支持支持
跨表不支持支持支持支持
批量支持支持支持支持
rpc调用的操作日志不支持支持支持支持
解耦业务支持不支持不支持支持
开发的实现复杂度/可扩展性***********
优点完全对业务系统无侵入。实现方式完全可控,可以由大数据人员处理收集生成的操作日志,也可做到弱通用性。实现方式完全可控,由需要打印日志的地方控制并记录。无需其他人员介入和学习成本。快速开发。和业务解耦无侵入。通用性和自定义较好,可以做到一次开发,其他项目也可使用。
使用场景单纯数据库单表字段变更的操作日志记录有大数据的处理开发人员记录操作日志地方较少,快速开发,记录类型单一或者业务系统有特殊规则的记录要求。记录日志较多,系统对操作日志有较强要求,类型单一或者多个都可以较好支持。

what(实践结果):操作日志的方案实践

操作日志基于方法注解方式 + SPEL 表达式 实现,

SPEL概述

Spring表达式语言全称为“Spring Expression Language”,缩写为“SpEL”,类似于Struts2x中使用的OGNL表达式语言,能在运行时构建复杂表达式、存取对象图属性、对象方法调用等等,并且能与Spring功能完美整合,如能用来配置Bean定义。

本次使用到的特性为:

Expression = "#{@CompanyManger.getCompanyOtherByCompanyId(#companyId)}"

CompanyManger.getCompanyOtherByCompanyId 是一个类的方法。 

companyId 是这个方法的方法参数。

通过表达式解析后拿到的返回值就是 这个方法的返回值。

部分示例代码思路如下: 

/**
 * @description: 操作注解,在需要记录操作日志的方法上面添加
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogRecordAnno {

    /**
     * 旧值的表达式 oldExpression和newExpression表达式返回值类型必须一致
     * @return
     */
    String oldExpression() default "";

    /**
     * 旧值的表达式 oldExpression 执行解析是否在业务方法执行之前
     * @return
     */
    boolean oldExpressionExecBeforeFlag() default false;

    /**
     * 新值的表达式 oldExpression和newExpression表达式返回值类型必须一致
     * @return
     */
    String newExpression() default "";

    /**
     * 唯一业务标识表达式, 只限于是 旧值的表达式 或者 新值的表达式 的值是基本类型的话或者是list类型的基本类型,基础类型,比如根据id删除的场景
     * @return
     */
    String bizNoExpression() default "";


    /**
     * 操作模块细项分类枚举 如果 LogRecordParamAnno 字段注解中标识了 moduleClassify  则以 LogRecordParamAnno 字段标识的为准
     * @return
     */
    LogRecordAnnoModuleClassifyEnum moduleClassify() default LogRecordAnnoModuleClassifyEnum.NONE;

    /**
     * 操作模块细项分类表达式
     * @return
     */
    String moduleClassifyExpression() default "";

    /**
     * 操作日志所属模块 具体业务自定义
     * @return
     */
    LogRecordAnnoModuleEnum module();

    /**
     * 操作日志类型 增 删 改 查 等,具体业务自定义
     * @return
     */
    LogOperaTypeEnum type() default LogOperaTypeEnum.NONE;

    /**
     * 如果是导入、导出、文件格式的,在此处放入文件名称表达式
     * @return
     */
    String fileNameExpression() default "";

    /**
     * 集合类型排序字段
     */
    String sortFiledName() default "";
}

/**
 * @description: 字段注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogRecordParamAnno {

    /**
     * 字段自定义描述
     * @return
     */
    String value();

    /**
     * 是否为操作日志绑定的业务对象标识,一个实体类中必须有一个
     * @return
     */
    boolean bizNoFlag() default false;

    /**
     * 操作模块细项分类
     * @return
     */
    LogRecordAnnoModuleClassifyEnum moduleClassify() default LogRecordAnnoModuleClassifyEnum.NONE;

    /**
     * 字段值是否需要映射 例如字段为:状态0、1 需要映射为 草稿、生效
     * @return
     */
    boolean fieldMappingFlag() default false;

    /**
     * 字段值映射 {"草稿","生效"} 下标对应字段值
     * 如果是Boolean类型的,{"false对应的映射","true 对应的映射"}, 如果不填默认为{false:否、true:是}
     * @return
     */
    String[] fieldMapping() default {};
}
/**
 * @description: 字段注解
 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogRecordParamAnno {

    /**
     * 字段自定义描述
     * @return
     */
    String value();

    /**
     * 是否为操作日志绑定的业务对象标识,一个实体类中必须有一个
     * @return
     */
    boolean bizNoFlag() default false;

    /**
     * 操作模块细项分类
     * @return
     */
    LogRecordAnnoModuleClassifyEnum moduleClassify() default LogRecordAnnoModuleClassifyEnum.NONE;

    /**
     * 字段值是否需要映射 例如字段为:状态0、1 需要映射为 草稿、生效
     * @return
     */
    boolean fieldMappingFlag() default false;

    /**
     * 字段值映射 {"草稿","生效"} 下标对应字段值
     * 如果是Boolean类型的,{"false对应的映射","true 对应的映射"}, 如果不填默认为{false:否、true:是}
     * @return
     */
    String[] fieldMapping() default {};
}
    // logRecordAnno 注解拦截方法伪代码(只有关键部分)
    @Around(value = "annotationPoinCut(logRecordAnno);")
    public Object around(ProceedingJoinPoint joinPoint, LogRecordAnno logRecordAnno) throws Throwable {

        // 前置操作: 放入入参到上下文变量、获取旧值
        before(joinPoint, logRecordAnno, null);
        // 执行业务被拦截方法
        Object proceed = joinPoint.proceed();
        // 记录操作日志
        saveOperateLog(logRecordAnno, oldObject, newObject);
        return proceed;
    }


private void saveOperateLog(LogRecordAnno logRecordAnno, Object oldObject, Object newObject) {
        // 把LogRecordContext 中的变量都放到 RootObject 中
        Map<String, Object> variables = LogRecordContext.getVariables();
        if (variables != null && variables.size() > 0) {
            for (Map.Entry<String, Object> entry : variables.entrySet()) {
                evaluationContext.setVariable(entry.getKey(), entry.getValue());
            }
        }
      
        // 解析自定义表达式并赋值
        parseCustomExpression(logRecordAnno, logRecordBO);

      
        // 根据不同的操作类型(修改、删除、新增、上传等)比对转换为最终存储用的操作日志对象
        LogRecordResultBO logRecordResultBO = logRecordExecuteHelper.getLogRecordResultBO(logRecordBO);
        logRecordSaveService.saveLog(logRecordResultBO);
    }

// 最终比对的有变化的对象都会存到此对象中
public static class ChangeObject {
  /**
   * 字段名称
   */
  private String fieldName;
  /**
   * 字段描述
   */
  private String fieldDesc;
  /**
   * 操作类型
   */
  private Integer type;
  /**
   * 操作模块
   */
  private Integer moudle;
  /**
   * 模块细项
   */
  private Integer moudleClassify;
  /**
   * 操作前旧值
   */
  private Object fieldOldO;
  /**
   * 操作前新值
   */
  private Object fieldNewO;
  /**
   * 业务唯一标识id
   */
  private String bizNo;
}

//删除的操作日志记录
@LogRecordAnno(oldExpression = "#{@CompanyManger.getCompanyOtherByCompanyId(#companyId)}", module = LogRecordAnnoModuleEnum.COMPANY_OTHER_INFO, type = LogOperaTypeEnum.DELETE)
private void deleteCompanyOther(String companyId);

//批量插入操作日志示例
@LogRecordAnno(newExpression = "#{#insertCompanyOtherInfoList}", module = LogRecordAnnoModuleEnum.COMPANY_OTHER_INFO, moduleClassifyExpression = "4#{#type}")
    public void insertBatchOther(List<CompanyOtherInfo> insertCompanyOtherInfoList, Integer type);


// 更新示例
@LogRecordAnno(oldExpression = "#{#oldUpdateCompanyShareholderList}", newExpression = "#{#updateCompanyShareholderList}", module = LogRecordAnnoModuleEnum.COMPANY_SHAREHOLDER_INFO, sortFiledName = "id")
    public void batchUpdate(List<CompanyShareholder> updateCompanyShareholderList);
		// 查询原值
        List<CompanyShareholder> companyShareholderByIdList = companyShareholderManager.selectByIdList(idList);
        LogRecordContext.putVariable("oldUpdateCompanyShareholderList", companyShareholderByIdList);


if (StringUtils.hasText(logRecordAnno.oldExpression()) && !logRecordAnno.oldExpressionExecBeforeFlag()) {
  // 解析旧值表达式
  oldObject = PARSER.parseExpression(logRecordAnno.oldExpression(), PARSER_CONTEXT)
    .getValue(this.evaluationContext, evaluationContext.getRootObject());
  logRecordBO.setOldObject(oldObject);
}

if (StringUtils.hasText(logRecordAnno.newExpression())) {
  // 新值表达式执行返回的对象
  newObject = PARSER.parseExpression(logRecordAnno.newExpression(), PARSER_CONTEXT)
    .getValue(this.evaluationContext, evaluationContext.getRootObject());
  logRecordBO.setNewObject(newObject);
}
// 更改值的对象
public static class ChangeObject {
  /**
   * 字段名称
   */
  private String fieldName;
  /**
   * 字段描述
   */
  private String fieldDesc;
  /**
   * 操作类型
   */
  private Integer type;
  /**
   * 操作模块
   */
  private Integer moudle;
  /**
   * 模块细项
   */
  private Integer moudleClassify;
  /**
   * 操作前旧值
   */
  private Object fieldOldO;
  /**
   * 操作前新值
   */
  private Object fieldNewO;
  /**
   * 业务唯一标识id
   */
  private String bizNo;
}

大致流程图:

参考资料

SPEL表达式相关文章:玩转Spring中强大的spel表达式! - 知乎 (zhihu.com)

可参考美团的实现方案,当时开发操作日志之前也是看到美团的文章了解到使用SPEL表达式来实现更具扩展性的实现思路,文章中实现给出的是大致思想,复杂度和实现毕竟他们也考虑了自身的业务和需求:如何优雅地记录操作日志?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/488579.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

计算机网络中的路由选择背后的原理到底是什么样的?

路由选择是计算机网络中一个重要的概念&#xff0c;它指的是当一个数据包在网络中传输时&#xff0c;如何选择最优的路径以达到目的地。路由选择涉及到网络中的路由器和交换机&#xff0c;以及它们之间的通信协议。 本文将介绍路由选择的基本概念、算法和协议&#xff0c;以及…

虹科教您 | 基于Windows系统操作使用RELY-TSN-KIT评估套件

我们曾通过3篇文章详细介绍了如何在Linux系统中使用RELY-TSN-KIT并进行TSN协议测试&#xff0c;而本篇文章则将基于Windows系统来进行介绍。与Linux系统的操作类似的部分&#xff0c;本篇文章不再赘述&#xff0c;欢迎通过下方链接阅读前文&#xff1a; 虹科教您 | 基于Linux系…

UG NX二次开发(C#)-建模-一键获取直线与坐标轴的夹角

文章目录 1、前言2、在UG NX中随便创建几条直线3、在UG NX中的分析操作4、UG NX二次开发实现4.1 基本思路4.2 创建基准坐标系4.3 计算直线与轴向的夹角方法4.4 添加调用方法4.5 代码说明5、测试效果1、前言 在UG NX新版本中,测量的操作都集成在一起了,有读者私下问我,想开发…

OceanBase 4.0(小鱼)入选2023数字中国建设峰会“十大硬核科技”

4 月 27 日 &#xff0c;第六届数字中国建设峰会发布“十大硬核科技”&#xff0c;原生分布式数据库 OceanBase 4.0&#xff08;小鱼&#xff09;入选&#xff0c;这是对 OceanBase 在技术突破上的权威肯定。OceanBase 4.0 是业内首个单机分布式一体化数据库&#xff0c;突破了…

【深度学习】计算机视觉(13)——tensorboard

因为意识到tensorboard的使用远不止画个图放个图片那么简单&#xff0c;所以这里总结一些关键知识的笔记。由于时间问题&#xff0c;我先学习目前使用最多的功能&#xff0c;大部分源码都包含summary的具体使用&#xff0c;基本不需要自己修改&#xff0c;因此tensorboard的解读…

选择云原生是企业进行技术变革的必经之路

前言 众所周知&#xff0c;云计算领域的蓬勃发展&#xff0c;让越来越多的企业将自己的业务搬到云上&#xff0c;上云已经成为大部分企业的首选操作。无论是头部的中大型企业&#xff0c;还是普通的微小企业&#xff0c;企业业务是亘古不变的核心&#xff0c;这关系着企业的命脉…

赋值带随机指针的链表

给你一个长度为 n 的链表&#xff0c;每个节点包含一个额外增加的随机指针 random &#xff0c;该指针可以指向链表中的任何节点或空节点。 构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成&#xff0c;其中每个新节点的值都设为其对应的原节点的值。新节点的 n…

云主机如何恢复备份和挂载备份

一、挂载备份&#xff08;优先推荐&#xff09; 1、windows系统: 在主机管理界面&#xff0c;点击【挂载备份盘】 挂载完成以后远程连接到服务器&#xff0c;右键 我的电脑--管理--磁盘管理 可以看到未分配的磁盘 右键未分配的磁盘--更改驱动路径--添加 给磁盘指定一个盘符。…

C++学习day--02 C++语法规则

1、回顾 通过第一天的学习&#xff0c;我们搭建好了开发环境&#xff0c;并打印了你好&#xff0c;现在做一个案例&#xff1a;自己跟着敲写出来&#xff1a; #include <iostream> #include <Windows.h> int main(void) { std::cout << "1.网站 404 攻…

SPSS如何进行相关分析之案例实训?

文章目录 0.引言1.双变量相关分析2.偏相关分析3.距离分析 0.引言 因科研等多场景需要进行绘图处理&#xff0c;笔者对SPSS进行了学习&#xff0c;本文通过《SPSS统计分析从入门到精通》及其配套素材结合网上相关资料进行学习笔记总结&#xff0c;本文对相关分析进行阐述。 1.双…

ubuntu(22.04)--常用命令(2)-awk-sed-find-cut-xargs-grep-curl-tee-wc-sort

1.awk 它逐行处理数据&#xff0c;特别适用于从文件中提取特定的数据。例如&#xff0c;您可以使用以下命令从CSV文件中提取数据的第一列&#xff1a; #1.awk awk -F , {print $1} linuxmi.csv#指定分割符#第一列# 待处理文件$ awk [options] [file] #1.打印第2列awk ‘{pr…

IPC<进程间通信>之共享内存-源代码在结尾处

一&#xff0c;什么是共享内存 共享内存是进程间通信的一种方式&#xff0c;相较于传统的管道和命名文件的通信方式&#xff0c;shared memory是最快的一种方式&#xff0c;但是他也有一定的缺陷&#xff0c;下面再谈。 共享内存区是最快的IPC形式。一旦这样的内存映射到共享…

从C语言到C++⑤(第二章_类和对象_中篇)(6个默认成员函数+运算符重载+const成员)

目录 0. 引入6个默认成员函数 1. 构造函数&#xff08;默认成员函数&#xff09; 1.1 构造函数的概念 1.2 构造函数的特性和用法 1.3 默认构造函数 2. 析构函数&#xff08;默认成员函数&#xff09; 2.1 析构函数概念 2.2 析构函数特性 3. 拷贝构造函数&#xff08;默认成员函…

附录2-购物车案例

目录 1 效果 2 接口数据 3 App.vue 4 HEADER.vue 5 COUNT.vue 6 GOODS.vue 7 FOOTER.vue 1 效果 由四种子组件和一个App.vue构成 2 接口数据 返回结果如下 {"status": 200,"message": "获取购物车列表数据成功&#xff01;",&q…

idea中的debug操作详解

行断点 默认模式 方法断点 菱形&#xff0c;加在方法前&#xff0c;用的比较多的地方&#xff1a;加在接口前会进入这个接口的实现类。 异常断点 如果说你的程序抛了某个异常&#xff0c;你需要知道在哪里抛出的&#xff0c;可以直接设置异常断点&#xff0c;设置后程序会…

Shell脚本之循环语句(for、while、until)

目录 一、echo命令二 for循环语句三 while循环语句结构(迭代&#xff09;四. until 循环语句结构五.continue和break和exit 一、echo命令 ?echo -n 表示不换行输出 echo -e 输出转义字符&#xff0c;将转义后的内容输出到屏幕上 常见转义字符&#xff1a; 二 for循环语句 用法…

Baumer工业相机堡盟工业相机如何通过BGAPI SDK设置自动亮度调整BrightnessAuto(自动曝光自动增益)(C++)

自动亮度调整项目场景 Baumer工业相机堡盟相机是一种高性能、高质量的工业相机&#xff0c;可用于各种应用场景&#xff0c;如物体检测、计数和识别、运动分析和图像处理。 Baumer的万兆网相机拥有出色的图像处理性能&#xff0c;可以实时传输高分辨率图像。此外&#xff0…

北斗哨兵北斗短报文远程监控系统解决方案

一、项目背景 随着社会发展各行各业都会遇到各种各样的安全问题&#xff0c;监控系统作为安防的第一线安防设备也已广泛部署&#xff0c;然而地处偏僻的监控区域往往面临着难以提供电力供应以及网络供应的问题&#xff0c;类似于山区环境监测&#xff0c;工地监测等复杂的环境布…

Web自动化测试流程:从入门到精通,帮你成为测试专家

B站首推&#xff01;2023最详细自动化测试合集&#xff0c;小白皆可掌握&#xff0c;让测试变得简单、快捷、可靠https://www.bilibili.com/video/BV1ua4y1V7Db 目录 摘要&#xff1a; 步骤一&#xff1a;选取测试工具 步骤二&#xff1a;编写测试用例 步骤三&#xff1a;编…

5月4号软件资讯更新合集.....

&#x1f680; Layui 2.8.2 发布 更新日志 table 修复 autoSort: true 时&#xff0c;更改 table.cache 未同步到 data 属性的问题 修复 多级表头存在 hide 表头属性时&#xff0c;执行完整重载可能出现的错位问题 修复 未开启 page 属性时底边框缺失问题 优化 打印内容中…