前言
工作中,特别是涉及到数据的记录变更时,常常会有记录变更字段日志的需求。
本文介绍了通过日志切面和自定义注解以及利用ObjectDifferBuilder来进行数据比对,实现数据变更记录日志功能开发。
@UpdateRecord
1. 自定义注解类解析
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface UpdateRecord {
/**
* 表名称
*/
String tableName() default "";
/**
* 业务类型
*/
String bizType() default "";
/**
* 日志绑定的业务标识
*/
String bizId() default "";
/**
* dao bean name
*/
String beanName();
}
2. 注解使用示例
@UpdateRecord(tableName = "deal_meeting", beanName = "dealCompanyValuationService", bizType = "项目估值", bizId = "#dealCompanyValuation.dealId")
public int save(DealCompanyValuation dealCompanyValuation) {
//是否是新建
boolean isAdd = StringUtils.isBlank(dealCompanyValuation.getId());
// 保存主表
int count = super.save(dealCompanyValuation);
// TODO 写自己对应得业务逻辑
return count;
}
3. bizId属性写法解析
业务ID这个可以取参数对象里的值。例如:#dealMeeting.dealId
这里使用的技术是Spring SpEL
一个表达式工具。
@RecordField
1. 自定义注解类解析
@Retention(RetentionPolicy.RUNTIME)
public @interface RecordField {
/**
* 记录字段类型 -> (3中所讲的注解枚举类)
* @return
*/
RecordFieldType type() default RecordFieldType.DEFAULT;
/**
* 记录字段名称
* @return
*/
String name() default "";
/**
* 字典TYPE
* type = DICT时填写
* @return
*/
String dictType() default "";
/**
* 日期格式化
* type = DATE时填写
* @return
*/
String dateFormat() default "yyyy-MM-dd";
/**
* ========== 这里可以继续延申,加上其他的格式化属性,如金额单位转换啊,部门转化啊等等。
*/
}
2. 注解使用示例
/** 主体类型 */
@Excel(name = "主体类型")
@ApiModelProperty(value = "主体类型",position=30)
@RecordField(name = "主体类型", type = RecordFieldType.DICT, dictType = "deal_investment_type")
private String investType;
RecordFieldType enum枚举类
1. 枚举类内容
public enum RecordFieldType {
DEFAULT(-1),
DICT(1),
USER(2),
DATE(3);
private final int value;
private RecordFieldType(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
默认是-1,对应type不转化。DICT、USER、DATE分别转化为字典、人员、日期。
SysUpdateRecord 日志记录表
1. 表结构sql
DROP TABLE IF EXISTS `sys_update_record`;
CREATE TABLE `sys_update_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',
`table_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '表名',
`key_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改主键ID',
`biz_id` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改记录的ID',
`biz_type` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改类型',
`record_name` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '记录字段名称',
`value_before` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '更改之前的值',
`value_after` text CHARACTER SET utf8 COLLATE utf8_general_ci NULL COMMENT '更改之后的值',
`update_by` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '修改人',
`update_time` datetime(0) NULL DEFAULT NULL COMMENT '修改时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2566 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '修改历史记录' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;
综合使用
1. 切面类开发,逻辑层编写。
@Aspect
@Component
public class UpdateRecordAspect {
@Autowired
private SysUpdateRecordService sysUpdateRecordService;
private final Logger logger = LoggerFactory.getLogger(getClass());
private final SpelExpressionParser parser = new SpelExpressionParser();
private final DefaultParameterNameDiscoverer discoverer = new DefaultParameterNameDiscoverer();
@Around("@annotation(com.anxin.sys.record.annotation.UpdateRecord)")
public Object doAround(ProceedingJoinPoint pjp) throws Throwable {
Method method = getMethod(pjp);
UpdateRecord annotation = method.getAnnotation(UpdateRecord.class);
BaseEntity<?> entity = (BaseEntity<?>) pjp.getArgs()[0];
String id = entity.getId();
Object bean = SpringBeanUtils.getBean(annotation.beanName());
Object data = null;
// 获取修改前的对象
if (bean instanceof BaseDao) {
BaseDao<?> baseDao = (BaseDao<?>) bean;
data = baseDao.getById(id);
} else if (bean instanceof BaseService) {
BaseService<?, ?> baseService = (BaseService<?, ?>) bean;
data = baseService.get(id);
}
// String bizId = parseExpression(annotation, pjp);
String bizId = "";
List<SysUpdateRecord> diff = diff(data, pjp.getArgs()[0], id, annotation, bizId);
Object result = pjp.proceed();
sysUpdateRecordService.batchInsert(diff);
return result;
}
protected Method getMethod(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
MethodSignature ms = (MethodSignature) signature;
return ms.getMethod();
}
/**
* 比较并记录变更
* @param source 修改前对象
* @param target 修改后对象
* @param id 对象id
* @param annotation updateRecord
* @param bizId 业务id
* @return 变更的记录
*/
public List<SysUpdateRecord> diff(Object source, Object target, String id, UpdateRecord annotation, String bizId) {
if (source == null || target == null) {
try {
Class<?> clazz = source == null ? target.getClass() : source.getClass();
source = source == null ? clazz.getDeclaredConstructor().newInstance() : source;
target = target == null ? clazz.getDeclaredConstructor().newInstance() : target;
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
DiffNode diff = ObjectDifferBuilder.buildDefault().compare(source, target);
List<SysUpdateRecord> result = Lists.newArrayList();
Object finalSource = source;
Object finalTarget = target;
// 存储子表字段修改前的值和修改后的值
// key 字段上@RecordField注解的name值
// value 有两个值 索引0 修改前的值 索引1 修改后的值
Map<String, String[]> childrenFieldVal = Maps.newHashMap();
diff.visit((node, visit) -> {
SysUpdateRecord updateRecord = diffField(node, finalSource, finalTarget, childrenFieldVal);
if (updateRecord != null) {
updateRecord.setTableName(annotation.tableName());
updateRecord.setBizType(annotation.bizType());
updateRecord.setKeyId(id);
updateRecord.setBizId(bizId);
updateRecord.preUpdate();
result.add(updateRecord);
}
});
childrenFieldVal.forEach((key, val) -> {
if (!Objects.equals(val[0], val[1])) {
SysUpdateRecord updateRecord = new SysUpdateRecord(val[0], val[1], key);
updateRecord.setTableName(annotation.tableName());
updateRecord.setBizType(annotation.bizType());
updateRecord.setKeyId(id);
updateRecord.setBizId(bizId);
updateRecord.preUpdate();
result.add(updateRecord);
}
});
return result;
}
SysUpdateRecord diffField(DiffNode node, Object source, Object target, Map<String, String[]> childrenFieldVal) {
if (node.isRootNode()) {
return null;
}
RecordField recordField = node.getFieldAnnotation(RecordField.class);
if (recordField == null || node.getValueTypeInfo() != null) {
return null;
}
boolean valueIsCollection = valueIsCollection(node, source, target);
DiffNode.State state = node.getState();
String name = recordField.name();
switch (state) {
case CHANGED:
if (!valueIsCollection) {
String[] values = convert(recordField, node.canonicalGet(source), node.canonicalGet(target));
return new SysUpdateRecord(values[0], values[1], recordField.name());
}
break;
case ADDED:
String[] beforeVal = childrenFieldVal.getOrDefault(name, new String[2]);
String[] beforeConvert = convert(recordField, node.canonicalGet(source), null);
beforeVal[0] = beforeConvert[0];
childrenFieldVal.put(name, beforeVal);
break;
case REMOVED:
String[] afterVal = childrenFieldVal.getOrDefault(name, new String[2]);
String[] afterConvert = convert(recordField, null, node.canonicalGet(target));
afterVal[1] = afterConvert[1];
childrenFieldVal.put(name, afterVal);
break;
default:
logger.warn("diff log not support");
break;
}
return null;
}
String[] convert(RecordField recordField, Object before, Object after) {
try {
switch (recordField.type()) {
case USER:
String beforeUserNickName = null;
String afterUserNickName = null;
if (before != null && StringUtils.isNotBlank(before.toString())){
SysUser beforeUser = UserUtils.getUser(String.valueOf(before));
if (beforeUser != null){
beforeUserNickName = beforeUser.getNickName();
}
}
if (after != null && StringUtils.isNotBlank(after.toString())){
SysUser afterUser = UserUtils.getUser(String.valueOf(after));
if (afterUser != null){
afterUserNickName = afterUser.getNickName();
}
}
return new String[] {
beforeUserNickName,
afterUserNickName
};
case DICT:
return new String[] {
DictUtils.getDictLabel(String.valueOf(before), recordField.dictType(), ""),
DictUtils.getDictLabel(String.valueOf(after), recordField.dictType(), "")
};
case DATE:
String[] result = new String[2];
if (before != null) {
result[0] = DateUtil.format(DateUtil.parse(String.valueOf(before)), recordField.dateFormat());
}
if (after != null) {
result[1] = DateUtil.format(DateUtil.parse(String.valueOf(after)), recordField.dateFormat());
}
return result;
default:
return new String[] {
String.valueOf(before),
String.valueOf(after)
};
}
}catch (Exception e){
logger.error("日志记录报错",e);
}
return new String[]{
String.valueOf(before),
String.valueOf(after)
};
}
private boolean valueIsCollection(DiffNode node, Object sourceObject, Object targetObject) {
if (sourceObject != null) {
Object sourceValue = node.canonicalGet(sourceObject);
if (sourceValue == null) {
if (targetObject != null) {
return node.canonicalGet(targetObject) instanceof Collection;
}
}
return sourceValue instanceof Collection;
}
return false;
}
}
doAround解析
- 通过@round注解监听对应包下对应注解的活动变化;
- 利用ProceedingJoinPoint参数通过getMethod()方法获取到对应的方法;
- 根据方法对象获取到对应加在方法上的注解SpringBeanUtils.getBean(UpdateRecord.Class);
- 转成我们对应继承的基础父类BaseEntity,并取出对应的id数据;
- 根据切面方法参数获取到对应的目标方法参数,我们第一个参数就是我们的业务对象;
- 获取到对应bean是属于dao层还是service层
- 利用instanceOf根据不同的结构进行不同的数据处理,最终得到我们的传入对象;
- 编写方法: 利用最终得到的数据源对象,如今传入的对象,id,注解annotation,业务id来进行最后的逻辑判断处理;
- 最后返回List集合的日志记录数据,然后进行日志插入数据库操作;
- 最终完成日志记录;
diff方法解析,一切变更判断的逻辑处理汇集地(详细看代码中注解解析)
ObjectDifferBuilder类的使用详解,详见https://blog.csdn.net/feeltouch/article/details/86683119
List<SysUpdateRecord> diff = diff(data, pjp.getArgs()[0], id, annotation, bizId);
// diff方法,传入source原始对象 target新对象,id,注解对象,业务id对象
public List<SysUpdateRecord> diff(Object source, Object target, String id, UpdateRecord annotation, String bizId) {
// 构造数据处理,两个是否其中一个为空,则进行数据构造
// 利用反射来获取到对应的新实例方法,保证任何一个都不能为null,防止后续报错。
if (source == null || target == null) {
try {
// 源数据如果为空,clazz赋值新数据target类,否则clazz及时源数据类
Class<?> clazz = source == null ? target.getClass() : source.getClass();
// 如果源数据为空,clazz构造新对象给源数据,否则直接赋值
source = source == null ? clazz.getDeclaredConstructor().newInstance() : source;
// 同上
target = target == null ? clazz.getDeclaredConstructor().newInstance() : target;
} catch (InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}
// 利用java对象比较器ObjectDifferBuilder构造出对象调用compare对象来进行字段结构进行判断
DiffNode diff = ObjectDifferBuilder.buildDefault().compare(source, target);
// 构造list来记录生成的日志信息
List<SysUpdateRecord> result = Lists.newArrayList();
// 接收处理后的源对象和新对象(此时已经都不为空)
Object finalSource = source;
Object finalTarget = target;
// 存储子表字段修改前的值和修改后的值
// key 字段上@RecordField注解的name值
// value 有两个值 索引0 修改前的值 索引1 修改后的值
Map<String, String[]> childrenFieldVal = Maps.newHashMap();
// 遍历节点进行判断处理
diff.visit((node, visit) -> {
// 根据传入的对象和节点以及返回的唯一map结构传入,构造出对应节点的变化记录信息
SysUpdateRecord updateRecord = diffField(node, finalSource, finalTarget, childrenFieldVal);
// update的属性值 前后都有值 走这里保存
if (updateRecord != null) {
updateRecord.setTableName(annotation.tableName());
updateRecord.setBizType(annotation.bizType());
updateRecord.setKeyId(id);
updateRecord.setBizId(bizId);
updateRecord.preUpdate();
result.add(updateRecord);
}
});
// 之前字段值为空 || 现在字段值为空 走这里保存
childrenFieldVal.forEach((key, val) -> {
if (!Objects.equals(val[0], val[1])) {
SysUpdateRecord updateRecord = new SysUpdateRecord(val[0], val[1], key);
updateRecord.setTableName(annotation.tableName());
updateRecord.setBizType(annotation.bizType());
updateRecord.setKeyId(id);
updateRecord.setBizId(bizId);
updateRecord.preUpdate();
result.add(updateRecord);
}
});
return result;
}
diffField方法解析
// 传入节点 源对象(非空)现对象 构造的最终全属性返回的更改变化数据map集
SysUpdateRecord diffField(DiffNode node, Object source, Object target, Map<String, String[]> childrenFieldVal) {
// 如果是根节点 则直接返回空
if (node.isRootNode()) {
return null;
}
// 获取每个node节点上对应的注解对象
RecordField recordField = node.getFieldAnnotation(RecordField.class);
// 注解对象为空或者node节点获取的值类型对象不为空,则返回null
if (recordField == null || node.getValueTypeInfo() != null) {
return null;
}
// 判断节点类型是否是子列表集合,例如:List<User> userList;
boolean valueIsCollection = valueIsCollection(node, source, target);
// 获取节点的操作状态
DiffNode.State state = node.getState();
// 获取对应节点注解标注的名称
String name = recordField.name();
switch (state) {
// changed 被改变(之前这个属性有,现在改变成为另一个了)
case CHANGED:
// 是否是子列表
if (!valueIsCollection) {
// 转换属性,比如字典值为sys_dict_sex 1 ->男
String[] values = convert(recordField, node.canonicalGet(source), node.canonicalGet(target));
// 将返回的数据字符串数组拆分成新老数据全参构造到SysUpdateRecord对象中
return new SysUpdateRecord(values[0], values[1], recordField.name());
}
break;
// addEd 新增(之前这个属性有值,现在没有)
case ADDED:
// 之前这个属性有值,构造这个属性的数组结构为字符数组总量为2的空字符串数组
String[] beforeVal = childrenFieldVal.getOrDefault(name, new String[2]);
// 转化之前的对象对应的这个属性值
String[] beforeConvert = convert(recordField, node.canonicalGet(source), null);
// 将这个属性值赋值给字符串数组第一个位置,也就是旧数据
beforeVal[0] = beforeConvert[0];
// 放入这个最终的map中
childrenFieldVal.put(name, beforeVal);
break;
// removed 移除(这个是现在有值,以前的被移除了)
case REMOVED:
// 同上,这个是现在有值,以前的被移除了
String[] afterVal = childrenFieldVal.getOrDefault(name, new String[2]);
String[] afterConvert = convert(recordField, null, node.canonicalGet(target));
afterVal[1] = afterConvert[1];
childrenFieldVal.put(name, afterVal);
break;
default:
logger.warn("diff log not support");
break;
}
return null;
}
// 转化的方法,返回转化后的new String[]{}
String[] convert(RecordField recordField, Object before, Object after) {
try {
switch (recordField.type()) {
case USER:
String beforeUserNickName = null;
String afterUserNickName = null;
if (before != null && StringUtils.isNotBlank(before.toString())){
SysUser beforeUser = UserUtils.getUser(String.valueOf(before));
if (beforeUser != null){
beforeUserNickName = beforeUser.getNickName();
}
}
if (after != null && StringUtils.isNotBlank(after.toString())){
SysUser afterUser = UserUtils.getUser(String.valueOf(after));
if (afterUser != null){
afterUserNickName = afterUser.getNickName();
}
}
return new String[] {
beforeUserNickName,
afterUserNickName
};
case DICT:
return new String[] {
DictUtils.getDictLabel(String.valueOf(before), recordField.dictType(), ""),
DictUtils.getDictLabel(String.valueOf(after), recordField.dictType(), "")
};
case DATE:
String[] result = new String[2];
if (before != null) {
result[0] = DateUtil.format(DateUtil.parse(String.valueOf(before)), recordField.dateFormat());
}
if (after != null) {
result[1] = DateUtil.format(DateUtil.parse(String.valueOf(after)), recordField.dateFormat());
}
return result;
default:
return new String[] {
String.valueOf(before),
String.valueOf(after)
};
}
}catch (Exception e){
logger.error("日志记录报错",e);
}
return new String[]{
String.valueOf(before),
String.valueOf(after)
};
}
// 判断是该节点是否是子列表
private boolean valueIsCollection(DiffNode node, Object sourceObject, Object targetObject) {
if (sourceObject != null) {
Object sourceValue = node.canonicalGet(sourceObject);
if (sourceValue == null) {
if (targetObject != null) {
return node.canonicalGet(targetObject) instanceof Collection;
}
}
return sourceValue instanceof Collection;
}
return false;
}
效果演示
1. 对象属性变化
2. 子列表(后期优化)
总结
以上就是今天要讲的内容,后续会对子列表进行优化。发表出一篇集版本控制、数据记录、日志处理于一体的文章,当然,该文章已符合大多数人的需求。