java

关注公众号 jb51net

关闭
首页 > 软件编程 > java > MyBatis拦截器数据变更记录

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

作者:蒙眼过河

本文介绍了在日常应用开发中记录业务数据变更的需求与实现方法,设计了整体架构,实现自定义注解、数据变更记录实体、事务同步机制和拦截器注册方案,并通过示例展示了使用方法及常见问题解决方案,最后总结了方案的优势和注意事项,需要的朋友可以参考下

一、背景与需求

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

二、整体架构设计

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拦截器数据变更记录的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文