One-Line Config for Automatic Data Change Auditing with Spring Boot & MyBatis-Plus
The article presents a low‑intrusion, highly configurable data‑audit plugin for Spring Boot that leverages MyBatis‑Plus interceptors to automatically capture before‑and‑after snapshots of INSERT, UPDATE, and DELETE operations, offering extensible logging, masking, async handling, and monitoring for enterprise systems.
Data correctness and traceability are critical in enterprise systems because a single mis‑modified record can amplify risk. Manual logging in the Service layer is invasive, error‑prone, and costly to maintain.
Why use MyBatis‑Plus interceptors?
MyBatis‑Plus adds a mature plugin system on top of MyBatis, allowing non‑intrusive interception of SQL execution. The interceptable core components are Executor, StatementHandler, ParameterHandler, and ResultSetHandler. The Executor is the optimal entry point because every INSERT, UPDATE, and DELETE passes through it.
Overall design
The solution is decomposed into five steps:
Intercept write operations (INSERT/UPDATE/DELETE).
Parse the SQL to obtain the table name and operation type.
Capture before‑ and after‑data snapshots.
Persist an audit log (supporting masking and asynchronous writes).
Provide query, monitoring, and alert capabilities.
The flow adds almost no code intrusion to business logic.
Audit entity definition
package com.icoderoad.audit.entity;
import jakarta.persistence.*;
import lombok.Data;
@Data
@Entity
@Table(name = "data_audit_log")
public class DataAuditLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String tableName; // 被操作的表名
private String operationType; // INSERT / UPDATE / DELETE
private String recordId; // 业务主键标识
private String beforeData; // 变更前数据快照(JSON)
private String afterData; // 变更后数据快照(JSON)
private String operator; // 操作人
private String operationTime; // 操作时间
private String businessInfo; // 业务描述信息
}Core interceptor implementation
package com.icoderoad.audit.interceptor;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.session.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
@Intercepts({
@Signature(type = Executor.class, method = "update", args = {MappedStatement.class, Object.class})
})
@Component
public class DataAuditInterceptor implements Interceptor {
@Autowired
private DataAuditService dataAuditService;
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
MappedStatement ms = (MappedStatement) args[0];
Object parameter = args[1];
String operationType = ms.getSqlCommandType().name();
String sql = ms.getBoundSql(parameter).getSql();
String tableName = SqlParser.getTableName(sql);
// before snapshot for UPDATE/DELETE
String beforeData = null;
if ("UPDATE".equals(operationType) || "DELETE".equals(operationType)) {
beforeData = JSON.toJSONString(parameter);
}
// execute original SQL
Object result = invocation.proceed();
// after snapshot for INSERT/UPDATE
String afterData = null;
if ("INSERT".equals(operationType) || "UPDATE".equals(operationType)) {
afterData = JSON.toJSONString(parameter);
}
DataAuditLog auditLog = new DataAuditLog();
auditLog.setTableName(tableName);
auditLog.setOperationType(operationType);
auditLog.setBeforeData(beforeData);
auditLog.setAfterData(afterData);
auditLog.setOperator(CurrentUserUtil.getUsername());
auditLog.setOperationTime(LocalDateTime.now().toString());
dataAuditService.saveAuditLog(auditLog);
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
}SQL parsing utility
package com.icoderoad.audit.parser;
public class SqlParser {
public static String getTableName(String sql) {
sql = sql.toUpperCase().trim();
if (sql.startsWith("INSERT INTO")) {
return sql.split("\\s+")[2];
}
if (sql.startsWith("UPDATE")) {
return sql.split("\\s+")[1];
}
if (sql.startsWith("DELETE FROM")) {
return sql.split("\\s+")[2];
}
return "UNKNOWN_TABLE";
}
}In production you may replace the simple parser with professional libraries such as JSqlParser .
Additional capabilities
Data masking utility that obscures sensitive fields (phone numbers, IDs, emails).
Asynchronous log persistence using Spring's @Async to reduce latency.
Custom annotation @DataAudit to control audit scope at method level.
package com.icoderoad.audit.util;
public class DataMaskingUtil {
public static String mask(String data) {
if (data == null) return null;
data = data.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
data = data.replaceAll("(\\d{6})\\d{8}(\\w{4})", "$1********$2");
data = data.replaceAll("(\\w{2})\\w+@(\\w+)", "$1***@$2");
return data;
}
} package com.icoderoad.audit.service;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
@Service
public class AsyncDataAuditService {
@Async
public void saveAsync(DataAuditLog auditLog) {
saveAuditLog(auditLog);
}
} package com.icoderoad.audit.annotation;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface DataAudit {
String value() default "";
boolean enable() default true;
}Performance and security recommendations
Batch insert audit logs to reduce write overhead.
Use message queues (Kafka, RabbitMQ) for asynchronous decoupling.
Periodically archive historical audit data.
Optimize indexes on the audit table.
Control access permissions for audit data.
Encrypt sensitive fields before storage.
Prevent tampering of audit logs and consider meta‑auditing of the logs themselves.
Real‑world usage examples
Financial system – fund movement audit
@Service
public class AccountService {
@DataAudit("账户资金变动")
public void transfer(Long fromId, Long toId, BigDecimal amount) {
// 资金转账逻辑
}
}E‑commerce system – product info changes
@Service
public class ProductService {
@DataAudit("商品信息更新")
public void updateProduct(Product product) {
// 更新商品
}
}Monitoring and alert integration
package com.icoderoad.audit.metrics;
import io.micrometer.core.instrument.*;
import org.springframework.stereotype.Component;
@Component
public class AuditMetrics {
private final MeterRegistry registry;
public AuditMetrics(MeterRegistry registry) { this.registry = registry; }
public void record(String type) {
Counter.builder("data.audit.events")
.tag("operation", type)
.register(registry)
.increment();
}
}Conclusion
The approach achieves non‑intrusive data‑change tracking, a configurable and extensible audit capability, while balancing performance, security, and compliance requirements. Leveraging Spring Boot and MyBatis‑Plus interceptors enables developers to add this foundation to production‑grade systems with minimal effort.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
LuTiao Programming
LuTiao Programming is a friendly community offering free programming lessons. We inspire learners to explore new ideas and technologies and quickly acquire job-ready skills.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
