当前位置: 代码网 > it编程>编程语言>Java > 基于MyBatis拦截器实现数据变更记录的技术方案

基于MyBatis拦截器实现数据变更记录的技术方案

2026年04月20日 Java 我要评论
一、背景与需求在日常应用开发中,数据变更记录是一个常见需求。我们需要记录业务数据的每一次变更,包括:字段级别的变更记录变更前后的值变更人、变更时间支持insert/update/delete操作支持单

一、背景与需求

在日常应用开发中,数据变更记录是一个常见需求。我们需要记录业务数据的每一次变更,包括:

  • 字段级别的变更记录
  • 变更前后的值
  • 变更人、变更时间
  • 支持insert/update/delete操作
  • 支持单条和批量操作
  • 确保数据一致性和准确性

二、整体架构设计

2.1 核心组件

┌─────────────────────────────────────┐
│         controller/service层         │
└───────────────┬─────────────────────┘
                │
                ▼
┌─────────────────────────────────────┐
│         mapper接口(带注解)          │
│    @datachangelog(businesstype)     │
└───────────────┬─────────────────────┘
                │
                ▼
┌─────────────────────────────────────┐
│      mybatis executor.update()       │
│          拦截器拦截点                 │
└───────────────┬─────────────────────┘
                │
                ▼
┌─────────────────────────────────────┐
│     improveddatachangeinterceptor    │
│  ┌─────────────────────────────┐   │
│  │ 1. 获取注解/判断是否需要记录 │   │
│  │ 2. 查询原始数据              │   │
│  │ 3. 执行sql操作               │   │
│  │ 4. 事务提交后查询新数据       │   │
│  │ 5. 对比差异并保存记录         │   │
│  └─────────────────────────────┘   │
└───────────────┬─────────────────────┘
                │
                ▼
┌─────────────────────────────────────┐
│         sys_data_change_log          │
│          数据变更记录表               │
└─────────────────────────────────────┘

2.2 数据表设计

create table sys_data_change_log (
    id bigint(20) not null auto_increment comment '主键id',
    business_type varchar(50) not null comment '业务类型',
    business_id varchar(50) not null comment '业务数据id',
    field_name varchar(50) not null comment '字段名称',
    field_comment varchar(100) default null comment '字段注释',
    old_value text comment '变更前值',
    new_value text comment '变更后值',
    change_type varchar(20) default 'update' comment '变更类型',
    change_by varchar(50) not null comment '变更人',
    change_time datetime not null comment '变更时间',
    primary key (id),
    key idx_business (business_type, business_id),
    key idx_change_time (change_time)
) engine=innodb default charset=utf8mb4 comment='数据变更记录表';

三、核心代码实现

3.1 自定义注解

package com.demo.framework.aspectj.lang.annotation;
import java.lang.annotation.*;
/**
 * 数据变更记录注解
 * 标记在mapper方法上,表示该方法需要记录数据变更
 */
@target({elementtype.method})
@retention(retentionpolicy.runtime)
@documented
public @interface datachangelog {
    /**
     * 业务类型
     */
    string businesstype() default "";
    /**
     * 是否记录详情(字段级别)
     */
    boolean detail() default true;
    /**
     * 忽略的字段
     */
    string[] ignorefields() default {"createtime", "updatetime", "createby", "updateby"};
}

3.2 数据变更记录实体

package com.demo.system.domain;

import com.demo.common.annotation.excel;
import java.util.date;

/**
 * 数据变更记录对象 sys_data_change_log
 */
public class sysdatachangelog {
    private static final long serialversionuid = 1l;
    
    /** 主键id */
    private long id;
    
    /** 业务类型 */
    @excel(name = "业务类型")
    private string businesstype;
    
    /** 业务数据id */
    @excel(name = "业务数据id")
    private string businessid;
    
    /** 字段名称 */
    @excel(name = "字段名称")
    private string fieldname;
    
    /** 字段注释 */
    @excel(name = "字段注释")
    private string fieldcomment;
    
    /** 变更前值 */
    @excel(name = "变更前值")
    private string oldvalue;
    
    /** 变更后值 */
    @excel(name = "变更后值")
    private string newvalue;
    
    /** 变更类型 */
    @excel(name = "变更类型")
    private string changetype;
    
    /** 变更人 */
    @excel(name = "变更人")
    private string changeby;
    
    /** 变更时间 */
    @excel(name = "变更时间", dateformat = "yyyy-mm-dd hh:mm:ss")
    private date changetime;
    
    // getters and setters...
}

四、数据提交后再记录日志的方案

4.1 事务同步机制

使用spring的transactionsynchronization实现事务提交后执行记录操作:

/**
 * 注册事务提交后的任务
 * 核心:确保数据真正写入数据库后才记录变更日志
 */
private void registeraftercommittask(runnable task) {
    if (transactionsynchronizationmanager.issynchronizationactive()) {
        // 事务中:注册事务同步器,事务提交后执行
        transactionsynchronizationmanager.registersynchronization(
            new transactionsynchronization() {
                @override
                public void aftercommit() {
                    task.run();
                }
                
                @override
                public void aftercompletion(int status) {
                    if (status == status_rolled_back) {
                        log.debug("事务回滚,不记录变更");
                    }
                }
            }
        );
    } else {
        // 无事务:直接执行
        log.debug("无事务环境,直接执行");
        task.run();
    }
}

4.2 更新操作处理流程

/**
 * 处理更新操作
 * 三步走策略:
 * 1. 更新前查询原始数据
 * 2. 执行更新操作
 * 3. 事务提交后查询新数据并对比记录
 */
private object handleupdate(invocation invocation, mappedstatement ms, 
                            object parameter, datachangelog datachangelog) throws throwable {
    log.debug("处理更新操作,业务类型:{}", datachangelog.businesstype());
    
    // 1. 更新前查询原始数据
    object beforedata = querydatabyid(parameter, ms);
    log.debug("更新前数据:{}", json.tojsonstring(beforedata));
    
    // 2. 缓存操作信息
    operationinfo opinfo = new operationinfo();
    opinfo.setbusinesstype(datachangelog.businesstype());
    opinfo.setignorefields(datachangelog.ignorefields());
    opinfo.setparameter(parameter);
    opinfo.setsqlcommandtype(sqlcommandtype.update);
    opinfo.setmapperid(ms.getid());
    opinfo.setbeforedata(beforedata);
    operation_info_thread_local.set(opinfo);
    
    // 3. 执行更新操作
    object result = invocation.proceed();
    
    // 4. 如果更新成功,在事务提交后处理变更记录
    if (integer.parseint(result.tostring()) > 0) {
        registeraftercommittask(() -> processupdateaftercommit(opinfo));
    }
    
    return result;
}

/**
 * 事务提交后处理更新操作
 */
private void processupdateaftercommit(operationinfo opinfo) {
    try {
        // 1. 更新后查询最新数据
        object afterdata = querydatabyid(opinfo.getparameter(), opinfo.getmapperid());
        log.debug("更新后数据:{}", json.tojsonstring(afterdata));
        
        if (opinfo.getbeforedata() != null && afterdata != null) {
            // 2. 对比数据差异
            list<sysdatachangelog> changelogs = comparedata(
                opinfo.getbeforedata(), 
                afterdata, 
                opinfo.getbusinesstype(),
                opinfo.getignorefields()
            );
            
            // 3. 批量保存变更记录
            if (!changelogs.isempty()) {
                datachangelogservice.insertbatch(changelogs);
                log.info("保存{}条更新变更记录", changelogs.size());
            }
        }
    } catch (exception e) {
        log.error("处理更新后变更记录失败", e);
    }
}

4.3 数据对比工具方法

/**
 * 对比数据差异
 * 支持日期、数字类型的精确比较
 */
private list<sysdatachangelog> comparedata(object beforedata, object afterdata, 
                                           string businesstype, string[] ignorefields) {
    list<sysdatachangelog> changelogs = new arraylist<>();
    
    try {
        map<string, object> beforemap = converttomap(beforedata);
        map<string, object> aftermap = converttomap(afterdata);
        
        string businessid = extractbusinessid(aftermap);
        
        // 遍历更新后的数据
        for (map.entry<string, object> entry : aftermap.entryset()) {
            string fieldname = entry.getkey();
            
            // 忽略指定字段
            if (shouldignore(fieldname, ignorefields)) {
                continue;
            }
            
            object beforevalue = beforemap.get(fieldname);
            object aftervalue = entry.getvalue();
            
            // 比较值是否变化
            if (!isvalueequals(beforevalue, aftervalue)) {
                sysdatachangelog changelog = createchangelog(
                    businesstype, businessid, fieldname,
                    getfieldcomment(afterdata, fieldname),
                    beforevalue, aftervalue, "update"
                );
                changelogs.add(changelog);
            }
        }
        
    } catch (exception e) {
        log.error("对比数据差异失败", e);
    }
    
    return changelogs;
}

/**
 * 判断两个值是否相等(处理各种数据类型)
 */
private boolean isvalueequals(object v1, object v2) {
    if (v1 == null && v2 == null) return true;
    if (v1 == null || v2 == null) return false;
    
    // 处理日期类型
    if (v1 instanceof date && v2 instanceof date) {
        return ((date) v1).gettime() == ((date) v2).gettime();
    }
    
    // 处理数字类型(避免精度问题)
    if (v1 instanceof number && v2 instanceof number) {
        return new bigdecimal(v1.tostring())
            .compareto(new bigdecimal(v2.tostring())) == 0;
    }
    
    return v1.equals(v2);
}

五、拦截器未注册到spring容器的解决方案

5.1 问题分析

在实际项目中,经常遇到拦截器配置了@component注解,但mybatis并没有自动注册拦截器的情况。主要原因:

  1. mybatis自动配置顺序问题:拦截器注册时机过早
  2. 多数据源配置:需要为每个sqlsessionfactory手动注册
  3. 自定义mybatis配置:覆盖了默认配置

5.2 解决方案一:手动注册拦截器(推荐)

package com.demo.framework.config;

import com.demo.framework.interceptor.improveddatachangeinterceptor;
import org.apache.ibatis.plugin.interceptor;
import org.apache.ibatis.session.sqlsessionfactory;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.context.annotation.configuration;
import org.springframework.context.annotation.lazy;
import javax.annotation.postconstruct;
import java.util.list;

/**
 * 拦截器手动注册配置
 * 解决拦截器未自动注册到mybatis的问题
 */
@configuration
public class interceptormanualconfig {
    
    private static final org.slf4j.logger log = 
        org.slf4j.loggerfactory.getlogger(interceptormanualconfig.class);
    
    @autowired
    private list<sqlsessionfactory> sqlsessionfactories;
    
    @autowired
    @lazy // 避免循环依赖
    private improveddatachangeinterceptor datachangeinterceptor;
    
    @postconstruct
    public void manualregisterinterceptor() {
        log.info("========== 开始手动注册数据变更拦截器 ==========");
        log.info("发现 {} 个sqlsessionfactory", sqlsessionfactories.size());
        
        for (int i = 0; i < sqlsessionfactories.size(); i++) {
            sqlsessionfactory sqlsessionfactory = sqlsessionfactories.get(i);
            org.apache.ibatis.session.configuration configuration = 
                sqlsessionfactory.getconfiguration();
            
            // 检查是否已经注册
            boolean alreadyregistered = false;
            for (interceptor interceptor : configuration.getinterceptors()) {
                if (interceptor instanceof improveddatachangeinterceptor) {
                    alreadyregistered = true;
                    log.info("sqlsessionfactory[{}] 拦截器已存在,跳过注册", i);
                    break;
                }
            }
            
            if (!alreadyregistered) {
                log.info("注册拦截器到 sqlsessionfactory[{}]: {}", 
                    i, sqlsessionfactory);
                configuration.addinterceptor(datachangeinterceptor);
            }
        }
        
        // 验证注册结果
        verifyinterceptorregistration();
        log.info("========== 拦截器手动注册完成 ==========");
    }
    
    /**
     * 验证拦截器注册结果
     */
    private void verifyinterceptorregistration() {
        for (int i = 0; i < sqlsessionfactories.size(); i++) {
            sqlsessionfactory sqlsessionfactory = sqlsessionfactories.get(i);
            list<interceptor> interceptors = 
                sqlsessionfactory.getconfiguration().getinterceptors();
            
            log.info("sqlsessionfactory[{}] 已注册拦截器数量: {}", i, interceptors.size());
            for (interceptor interceptor : interceptors) {
                log.info("  - {}", interceptor.getclass().getsimplename());
            }
        }
    }
}

5.3 解决方案二:使用mybatis配置类

package com.demo.framework.config;

import com.demo.framework.interceptor.improveddatachangeinterceptor;
import org.apache.ibatis.plugin.interceptor;
import org.apache.ibatis.session.sqlsessionfactory;
import org.mybatis.spring.sqlsessionfactorybean;
import org.mybatis.spring.boot.autoconfigure.mybatisautoconfiguration;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.boot.autoconfigure.autoconfigureafter;
import org.springframework.context.applicationcontext;
import org.springframework.context.annotation.configuration;

import javax.annotation.postconstruct;

/**
 * 通过mybatis配置类注册拦截器
 */
@configuration
@autoconfigureafter(mybatisautoconfiguration.class)
public class mybatisinterceptorconfig {
    
    @autowired
    private applicationcontext applicationcontext;
    
    @autowired
    private improveddatachangeinterceptor datachangeinterceptor;
    
    @postconstruct
    public void init() {
        // 获取所有sqlsessionfactory
        string[] sqlsessionfactorynames = 
            applicationcontext.getbeannamesfortype(sqlsessionfactory.class);
        
        for (string name : sqlsessionfactorynames) {
            sqlsessionfactory sqlsessionfactory = 
                applicationcontext.getbean(name, sqlsessionfactory.class);
            
            // 获取当前的拦截器列表
            org.apache.ibatis.session.configuration configuration = 
                sqlsessionfactory.getconfiguration();
            
            // 添加拦截器(如果不存在)
            boolean exists = false;
            for (interceptor interceptor : configuration.getinterceptors()) {
                if (interceptor instanceof improveddatachangeinterceptor) {
                    exists = true;
                    break;
                }
            }
            
            if (!exists) {
                configuration.addinterceptor(datachangeinterceptor);
            }
        }
    }
}

5.4 解决方案三:验证拦截器是否注册

package com.demo.framework.config;

import org.apache.ibatis.plugin.interceptor;
import org.apache.ibatis.session.sqlsessionfactory;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.boot.commandlinerunner;
import org.springframework.stereotype.component;
import java.util.list;

/**
 * 启动后验证拦截器注册情况
 */
@component
public class interceptorverifyrunner implements commandlinerunner {
    
    private static final org.slf4j.logger log = 
        org.slf4j.loggerfactory.getlogger(interceptorverifyrunner.class);
    
    @autowired
    private list<sqlsessionfactory> sqlsessionfactories;
    
    @override
    public void run(string... args) throws exception {
        log.info("========== 数据变更拦截器注册验证 ==========");
        
        for (int i = 0; i < sqlsessionfactories.size(); i++) {
            sqlsessionfactory factory = sqlsessionfactories.get(i);
            list<interceptor> interceptors = factory.getconfiguration().getinterceptors();
            
            boolean hasinterceptor = false;
            for (interceptor interceptor : interceptors) {
                string classname = interceptor.getclass().getname();
                if (classname.contains("datachangeinterceptor")) {
                    hasinterceptor = true;
                    log.info("✅ sqlsessionfactory[{}] 已注册拦截器: {}", 
                        i, classname);
                }
            }
            
            if (!hasinterceptor) {
                log.error("❌ sqlsessionfactory[{}] 未找到数据变更拦截器!", i);
            }
        }
        
        log.info("==========================================");
    }
}

5.5 调试用rest接口

package com.demo.web.controller.monitor;

import com.demo.common.core.controller.basecontroller;
import com.demo.common.core.domain.ajaxresult;
import org.apache.ibatis.plugin.interceptor;
import org.apache.ibatis.session.sqlsessionfactory;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.web.bind.annotation.getmapping;
import org.springframework.web.bind.annotation.requestmapping;
import org.springframework.web.bind.annotation.restcontroller;
import java.util.arraylist;
import java.util.hashmap;
import java.util.list;
import java.util.map;

/**
 * 拦截器调试控制器
 */
@restcontroller
@requestmapping("/monitor/interceptor")
public class interceptordebugcontroller extends basecontroller {
    
    @autowired
    private list<sqlsessionfactory> sqlsessionfactories;
    
    /**
     * 查看拦截器注册状态
     */
    @getmapping("/status")
    public ajaxresult status() {
        list<map<string, object>> result = new arraylist<>();
        
        for (int i = 0; i < sqlsessionfactories.size(); i++) {
            sqlsessionfactory factory = sqlsessionfactories.get(i);
            map<string, object> factoryinfo = new hashmap<>();
            factoryinfo.put("factoryindex", i);
            factoryinfo.put("factoryname", factory.getclass().getsimplename());
            
            list<interceptor> interceptors = factory.getconfiguration().getinterceptors();
            list<string> interceptornames = new arraylist<>();
            for (interceptor interceptor : interceptors) {
                interceptornames.add(interceptor.getclass().getsimplename());
            }
            
            factoryinfo.put("interceptorcount", interceptors.size());
            factoryinfo.put("interceptors", interceptornames);
            factoryinfo.put("hasdatachangeinterceptor", 
                interceptornames.stream().anymatch(name -> name.contains("datachange")));
            
            result.add(factoryinfo);
        }
        
        return success(result);
    }
    
    /**
     * 测试拦截器是否工作
     */
    @getmapping("/test")
    public ajaxresult test() {
        // 返回提示信息,实际测试需要调用mapper方法
        return success("请调用实际的mapper方法测试拦截器,如:/system/user/update");
    }
}

六、使用示例

6.1 mapper接口配置

package com.demo.system.mapper;

import com.demo.framework.aspectj.lang.annotation.datachangelog;
import com.demo.system.domain.sysuser;
import org.apache.ibatis.annotations.param;
import java.util.list;

public interface sysusermapper {
    
    /**
     * 修改用户信息
     */
    @datachangelog(
        businesstype = "user.update",
        ignorefields = {"createtime", "updatetime", "password"}
    )
    public int updateuser(sysuser user);
    
    /**
     * 新增用户
     */
    @datachangelog(businesstype = "user.insert")
    public int insertuser(sysuser user);
    
    /**
     * 删除用户
     */
    @datachangelog(businesstype = "user.delete")
    public int deleteuserbyid(long userid);
    
    /**
     * 批量删除用户
     */
    @datachangelog(businesstype = "user.batchdelete")
    public int deleteuserbyids(@param("userids") list<long> userids);
    
    /**
     * 查询用户(用于获取变更前后的数据)
     */
    public sysuser selectuserbyid(long userid);
}

6.2 service层调用

@service
public class sysuserserviceimpl implements isysuserservice {
    
    @autowired
    private sysusermapper usermapper;
    
    @override
    @transactional(rollbackfor = exception.class)
    public int updateuser(sysuser user) {
        // 直接调用mapper,拦截器会自动处理数据变更记录
        // 记录会在事务提交后自动写入
        return usermapper.updateuser(user);
    }
}

七、常见问题与解决方案

7.1 拦截器未生效排查清单

问题检查点解决方案
拦截器类未扫描@component注解是否存在添加注解或手动注册bean
mybatis未注册查看configuration.getinterceptors()手动注册拦截器
多数据源问题所有sqlsessionfactory都检查为每个数据源注册拦截器
事务问题事务提交前查询不到新数据使用aftercommit回调
注解未识别mapper方法上是否有@datachangelog检查注解是否存在

7.2 性能优化建议

/**
 * 批量操作优化
 */
private static final int batch_threshold = 100; // 批量阈值

// 分批保存变更记录
if (allchangelogs.size() > batch_threshold) {
    list<list<sysdatachangelog>> partitions = partitionlist(allchangelogs, batch_threshold);
    for (list<sysdatachangelog> partition : partitions) {
        datachangelogservice.insertbatch(partition);
    }
} else {
    datachangelogservice.insertbatch(allchangelogs);
}

7.3 日志配置

# application.yml
logging:
  level:
    com.demo.framework.interceptor: debug
    org.apache.ibatis: info

八、总结

8.1 方案优势

  1. 数据一致性:事务提交后才记录日志,确保数据准确性
  2. 性能优化:异步处理变更记录,不影响主业务流程
  3. 字段级对比:精确记录每个字段的变化
  4. 易于集成:通过注解配置,对业务代码无侵入
  5. 支持批量:完善的批量操作处理机制

8.2 注意事项

  1. 查询方法必须存在:需要提供根据id查询数据的方法
  2. 事务管理:确保在事务环境中使用
  3. 大字段处理:考虑对blob等大字段的特殊处理
  4. 数据清理:定期清理历史变更记录

8.3 扩展建议

  • 支持异步记录(使用消息队列)
  • 支持变更记录查询和导出
  • 支持自定义字段转换器
  • 支持敏感数据脱敏

以上就是基于mybatis拦截器实现数据变更记录的技术方案的详细内容,更多关于mybatis拦截器数据变更记录的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com