数据变更追踪的痛点
在我们的日常开发工作中,经常会遇到这样的场景:
- 产品经理问:“这条数据是谁什么时候修改的?”
- 运维人员说:“系统出了问题,需要知道哪些数据发生了变更”
- 审计要求:“需要记录所有的数据变更历史,以便合规检查”
- 业务人员想知道:“这个订单的状态是怎么一步步变过来的”
传统的做法往往是手动在每个业务方法中添加日志记录,不仅代码冗余,还容易遗漏。今天我们就用springboot + aop + 注解的方式来解决这个问题。
解决方案思路
今天我们要解决的,就是如何用aop实现自动化的数据变更追踪。
核心思路是:
- 自定义注解:标记需要追踪的方法
- aop切面:拦截被标记的方法
- 数据对比:比较变更前后的数据差异
- 变更记录:自动记录变更信息
技术选型
- springboot:快速搭建应用
- spring aop:面向切面编程
- jackson:json序列化/反序列化
- jpa/hibernate:orm框架
- mysql:数据存储
核心实现思路
1. 自定义注解定义
首先定义追踪注解:
/**
* 数据变更追踪注解
*/
@target(elementtype.method)
@retention(retentionpolicy.runtime)
@documented
public @interface datachangetrack {
/**
* 业务类型
*/
string businesstype() default "";
/**
* 业务id字段名
*/
string businessidfield() default "id";
/**
* 实体类类型
*/
class<?> entityclass();
/**
* 是否记录详细变更内容
*/
boolean trackdetail() default true;
/**
* 忽略的字段列表
*/
string[] ignorefields() default {};
}
2. 变更记录实体
定义变更记录的存储实体:
@entity
@table(name = "data_change_log")
@data
public class datachangelog {
@id
@generatedvalue(strategy = generationtype.identity)
private long id;
@column(name = "business_type")
private string businesstype;
@column(name = "business_id")
private string businessid;
@column(name = "operation_type")
private string operationtype; // create, update, delete
@column(name = "table_name")
private string tablename;
@column(name = "before_value", columndefinition = "text")
private string beforevalue; // 变更前的值
@column(name = "after_value", columndefinition = "text")
private string aftervalue; // 变更后的值
@column(name = "changed_fields", columndefinition = "text")
private string changedfields; // 变更的字段列表
@column(name = "changer_id")
private string changerid;
@column(name = "changer_name")
private string changername;
@column(name = "change_time")
private localdatetime changetime;
@column(name = "remark", columndefinition = "text")
private string remark;
}
3. aop切面实现
创建核心的aop切面:
@aspect
@component
@slf4j
public class datachangetrackeraspect {
@autowired
private datachangelogservice logservice;
@autowired
private objectmapper objectmapper;
/**
* 环绕通知,拦截被@datachangetrack注解标记的方法
*/
@around("@annotation(datachangetrack)")
public object around(proceedingjoinpoint joinpoint, datachangetrack datachangetrack) throws throwable {
string methodname = joinpoint.getsignature().getname();
object[] args = joinpoint.getargs();
log.debug("开始追踪方法: {}", methodname);
// 获取操作前的数据
map<string, object> beforedata = getbeforedata(joinpoint, datachangetrack);
try {
// 执行原方法
object result = joinpoint.proceed();
// 获取操作后的数据
map<string, object> afterdata = getafterdata(joinpoint, result, datachangetrack);
// 记录变更
recordchange(datachangetrack, beforedata, afterdata, result);
return result;
} catch (exception e) {
log.error("数据变更追踪发生异常", e);
throw e;
}
}
/**
* 获取变更前的数据
*/
private map<string, object> getbeforedata(proceedingjoinpoint joinpoint, datachangetrack annotation) {
try {
object[] args = joinpoint.getargs();
if (args.length == 0) {
return collections.emptymap();
}
// 假设第一个参数是要操作的实体对象
object entity = args[0];
if (entity == null) {
return collections.emptymap();
}
// 序列化为map
return objectmapper.convertvalue(entity, map.class);
} catch (exception e) {
log.error("获取变更前数据失败", e);
return collections.emptymap();
}
}
/**
* 获取变更后的数据
*/
private map<string, object> getafterdata(proceedingjoinpoint joinpoint, object result, datachangetrack annotation) {
try {
if (result == null) {
return collections.emptymap();
}
// 如果返回值是实体对象,直接序列化
if (annotation.entityclass().isassignablefrom(result.getclass())) {
return objectmapper.convertvalue(result, map.class);
}
// 如果是更新操作,可能需要重新查询数据库获取最新数据
if (result instanceof number) { // 假设返回值是影响的行数
// 从参数中获取业务id,查询最新数据
object entity = joinpoint.getargs()[0];
string businessid = getbusinessid(entity, annotation.businessidfield());
if (businessid != null) {
// 查询数据库获取最新数据
return querylatestdata(annotation.entityclass(), businessid);
}
}
return objectmapper.convertvalue(result, map.class);
} catch (exception e) {
log.error("获取变更后数据失败", e);
return collections.emptymap();
}
}
/**
* 记录数据变更
*/
private void recordchange(datachangetrack annotation, map<string, object> beforedata,
map<string, object> afterdata, object result) {
try {
datachangelog logentry = new datachangelog();
logentry.setbusinesstype(annotation.businesstype());
logentry.setoperationtype(determineoperationtype(beforedata, afterdata));
logentry.settablename(annotation.entityclass().getsimplename());
logentry.setbeforevalue(objectmapper.writevalueasstring(beforedata));
logentry.setaftervalue(objectmapper.writevalueasstring(afterdata));
logentry.setchangedfields(getchangedfields(beforedata, afterdata, annotation.ignorefields()));
logentry.setchangerid(getcurrentuserid());
logentry.setchangername(getcurrentusername());
logentry.setchangetime(localdatetime.now());
logentry.setremark("自动追踪");
// 设置业务id
string businessid = getbusinessidfromresult(result, annotation.businessidfield());
if (businessid != null) {
logentry.setbusinessid(businessid);
}
logservice.save(logentry);
} catch (exception e) {
log.error("记录数据变更失败", e);
}
}
/**
* 确定操作类型
*/
private string determineoperationtype(map<string, object> beforedata, map<string, object> afterdata) {
if (beforedata.isempty() && !afterdata.isempty()) {
return "create";
} else if (!beforedata.isempty() && afterdata.isempty()) {
return "delete";
} else {
return "update";
}
}
/**
* 获取变更的字段
*/
private string getchangedfields(map<string, object> beforedata, map<string, object> afterdata, string[] ignorefields) {
set<string> ignored = new hashset<>(arrays.aslist(ignorefields));
list<string> changed = new arraylist<>();
set<string> allkeys = new hashset<>();
allkeys.addall(beforedata.keyset());
allkeys.addall(afterdata.keyset());
for (string key : allkeys) {
if (ignored.contains(key)) {
continue;
}
object beforevalue = beforedata.get(key);
object aftervalue = afterdata.get(key);
if (!objects.equals(beforevalue, aftervalue)) {
changed.add(key);
}
}
return string.join(",", changed);
}
/**
* 获取业务id
*/
private string getbusinessid(object entity, string idfield) {
try {
field field = entity.getclass().getdeclaredfield(idfield);
field.setaccessible(true);
object value = field.get(entity);
return value != null ? value.tostring() : null;
} catch (exception e) {
log.error("获取业务id失败", e);
return null;
}
}
/**
* 从结果中获取业务id
*/
private string getbusinessidfromresult(object result, string idfield) {
if (result != null) {
try {
return getbusinessid(result, idfield);
} catch (exception e) {
log.error("从结果中获取业务id失败", e);
}
}
return null;
}
/**
* 查询最新数据
*/
private map<string, object> querylatestdata(class<?> entityclass, string businessid) {
// 这里需要根据实际情况实现查询逻辑
// 可以通过反射调用repository方法
return collections.emptymap();
}
/**
* 获取当前用户id
*/
private string getcurrentuserid() {
// 从securitycontext或threadlocal获取当前用户信息
authentication authentication = securitycontextholder.getcontext().getauthentication();
if (authentication != null && authentication.getprincipal() instanceof userdetails) {
return ((userdetails) authentication.getprincipal()).getusername();
}
return "system";
}
/**
* 获取当前用户名
*/
private string getcurrentusername() {
// 实现获取当前用户名的逻辑
return getcurrentuserid();
}
}
4. 服务层实现
创建变更日志服务:
@service
@transactional
public class datachangelogservice {
@autowired
private datachangelogrepository repository;
/**
* 保存变更记录
*/
public void save(datachangelog log) {
repository.save(log);
}
/**
* 根据业务类型和id查询变更记录
*/
public list<datachangelog> findbybusinessid(string businesstype, string businessid) {
return repository.findbybusinesstypeandbusinessidorderbychangetimedesc(businesstype, businessid);
}
/**
* 分页查询变更记录
*/
public page<datachangelog> findlogs(pageable pageable) {
return repository.findall(pageable);
}
/**
* 查询指定时间段内的变更记录
*/
public list<datachangelog> findbytimerange(localdatetime starttime, localdatetime endtime) {
return repository.findbychangetimebetween(starttime, endtime);
}
}
5. repository接口
定义数据访问接口:
@repository
public interface datachangelogrepository extends jparepository<datachangelog, long> {
list<datachangelog> findbybusinesstypeandbusinessidorderbychangetimedesc(string businesstype, string businessid);
list<datachangelog> findbychangetimebetween(localdatetime starttime, localdatetime endtime);
@query("select d from datachangelog d where d.businesstype = :businesstype and d.operationtype = :operationtype order by d.changetime desc")
list<datachangelog> findbybusinesstypeandoperationtype(@param("businesstype") string businesstype,
@param("operationtype") string operationtype);
}
6. 使用示例
在业务方法上使用注解:
@service
public class userservice {
@autowired
private userrepository userrepository;
@datachangetrack(
businesstype = "user_update",
businessidfield = "id",
entityclass = user.class,
trackdetail = true,
ignorefields = {"lastmodifiedtime", "version"}
)
public user updateuser(user user) {
// 更新用户信息
return userrepository.save(user);
}
@datachangetrack(
businesstype = "user_create",
businessidfield = "id",
entityclass = user.class
)
public user createuser(user user) {
// 创建用户
return userrepository.save(user);
}
@datachangetrack(
businesstype = "user_delete",
businessidfield = "id",
entityclass = user.class
)
@transactional
public void deleteuser(long userid) {
// 删除用户
userrepository.deletebyid(userid);
}
}
@service
public class orderservice {
@autowired
private orderrepository orderrepository;
@datachangetrack(
businesstype = "order_status_update",
businessidfield = "orderid",
entityclass = order.class,
trackdetail = true,
ignorefields = {"updatetime", "version"}
)
public order updateorderstatus(order order) {
// 更新订单状态
return orderrepository.save(order);
}
}
7. 控制器接口
提供查询接口:
@restcontroller
@requestmapping("/api/data-change")
public class datachangecontroller {
@autowired
private datachangelogservice logservice;
/**
* 查询业务对象的变更历史
*/
@getmapping("/history")
public result<list<datachangelog>> getchangehistory(
@requestparam string businesstype,
@requestparam string businessid) {
list<datachangelog> history = logservice.findbybusinessid(businesstype, businessid);
return result.success(history);
}
/**
* 分页查询变更记录
*/
@getmapping("/logs")
public result<page<datachangelog>> getchangelogs(
@requestparam(defaultvalue = "0") int page,
@requestparam(defaultvalue = "10") int size) {
pageable pageable = pagerequest.of(page, size, sort.by(sort.direction.desc, "changetime"));
page<datachangelog> logs = logservice.findlogs(pageable);
return result.success(logs);
}
/**
* 查询时间范围内的变更记录
*/
@getmapping("/logs/time-range")
public result<list<datachangelog>> getlogsbytimerange(
@requestparam @datetimeformat(iso = datetimeformat.iso.date_time) localdatetime starttime,
@requestparam @datetimeformat(iso = datetimeformat.iso.date_time) localdatetime endtime) {
list<datachangelog> logs = logservice.findbytimerange(starttime, endtime);
return result.success(logs);
}
}
性能优化策略
1. 异步处理
为了避免影响业务性能,可以将日志记录改为异步:
@async
public void saveasync(datachangelog log) {
repository.save(log);
}
2. 批量处理
对于高频变更场景,可以采用批量处理:
@component
public class batchlogprocessor {
private final list<datachangelog> logbuffer = new arraylist<>();
private final object lock = new object();
@scheduled(fixedrate = 5000) // 每5秒批量处理一次
public void processbatch() {
synchronized (lock) {
if (!logbuffer.isempty()) {
list<datachangelog> currentbatch = new arraylist<>(logbuffer);
logbuffer.clear();
// 批量保存到数据库
logrepository.saveall(currentbatch);
}
}
}
public void addtobatch(datachangelog log) {
synchronized (lock) {
logbuffer.add(log);
}
}
}
优势分析
相比传统的手动记录方式,这种方案的优势明显:
- 无侵入性:只需添加注解,不影响业务代码
- 自动化:自动记录变更,无需手动编写日志代码
- 灵活性:可通过注解参数灵活配置
- 完整性:记录完整的变更历史
- 可追溯:支持按业务类型和id查询变更历史
注意事项
- 性能影响:aop切面会带来一定的性能开销
- 数据安全:注意敏感数据的脱敏处理
- 存储容量:变更日志会持续增长,需要定期清理
- 事务一致性:确保变更日志与业务操作在同一线程中
总结
通过springboot + aop + 注解的技术组合,我们可以轻松实现自动化的数据变更追踪。这种方式不仅减少了代码冗余,还提高了系统的可维护性和可追溯性。
在实际项目中,建议根据具体业务需求调整注解参数和切面逻辑,并考虑性能优化策略。
以上就是基于springboot+aop+注解实现自动数据变更追踪的详细内容,更多关于springboot+aop+注解数据变更追踪的资料请关注代码网其它相关文章!
发表评论