一、前言
工作时有个需求,记录修改记录。但是好几张表关联在一起,如果一张表一张表写,造成大量的业务代码,还浪费时间。
在此基础上想到一个办法,利用AOP实现修改记录。
基于环境:springboot springaop mybatis
二、注意点及实现方案
1. 表结构设计
CREATE TABLE `biz_update_record` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`type` char(2) DEFAULT NULL COMMENT '标识',
`type_id` varchar(51) DEFAULT NULL COMMENT '标识ID',
`field` varchar(255) DEFAULT NULL COMMENT '字段',
`field_remark` varchar(255) DEFAULT NULL COMMENT '字段描述',
`old_text` varchar(255) DEFAULT NULL COMMENT '旧值',
`new_text` varchar(255) DEFAULT NULL COMMENT '新值',
`action` varchar(255) DEFAULT NULL COMMENT '操作唯一标识',
`remark` varchar(500) DEFAULT NULL COMMENT '描述',
`create_time` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=918 DEFAULT CHARSET=utf8mb4;
2. 思路实现
自定义注解 解决主表与关联表的关系和描述,使用AOP完成拦截器的功能。
根据不同的service 调用不同的selectXXByXXId完成旧数据的查询。
2.1 Update注解
public @interface Update {
......
/**
* 插入的ID
*/
String id() default "id";
/**
* update查询的主键
*/
String primaryKey() default "id";
/**
* 说明内容
*/
String remark() default "";
/**
* 读取内容转表达式 (如: 0=男|1=女|2=未知)
*/
String readConverterExp() default "";
......
}
主要几个属性:
- id:当前表的ID
- primaryKey:主表的ID
- remark:字段的描述
- readConverterExp:支持内容表达式(必须按照规定结构)
2.2 主要实现流程
- 1 aop 拦截注解
- 2 根据不同的操作类型,执行不同handler方法。(以Update为例)
- 3 update方法中形参的Class对象,转成默认的mapper.selectXXByXXId方法查询旧数据。
- 4 旧数据与新数据对比后,执行插入操作。
三、注意点及实现方案
1. 获取成员属性
/**
* 获取类成员的名称和注解
*/
private List<UpdateField> getClassField(Class clazz, Object obj) {
List<UpdateField> list = new ArrayList<>();
// 占位
list.add(0, null);
for (Field field : clazz.getDeclaredFields()) {
field.setAccessible(true);
if (field.isAnnotationPresent(Update.class)) {
Update update = field.getAnnotation(Update.class);
Object value = typeFormatter(ReflectUtils.invokeGetter(obj, field.getName()));
// 解析内容表达式
value = StringUtils.isNotEmpty(update.readConverterExp()) ? reverseByExp(value, update.readConverterExp()) : value;
if (StringUtils.isNotEmpty(update.remark())) {
UpdateField updateField = new UpdateField();
updateField.setUpdate(field.getAnnotation(Update.class));
updateField.setField(field.getName());
updateField.setProperty(value);
list.add(updateField);
}
// pojo 只允许一个field属性 代表当前数据的标识
if ((update.field())) {
list.set(0, new UpdateField(field.getName(), update, value));
}
}
}
return list;
}
2. 形参转默认mapper查询方法
/**
*
* 根据当前class 类名 获取默认mapper名称以及默认ById方法
*/
private String getName(Class clazz, String id) {
String methodName = clazz.getName().replaceAll(DEFAULT_PACKAGE_MODEL_NAME, DEFAULT_PACKAGE_MAPPER_NAME);
methodName = methodName.replaceAll(clazz.getSimpleName(), clazz.getSimpleName() + "Mapper");
methodName += ".select" + StringUtils.capitalize(clazz.getSimpleName()) + "By" + StringUtils.capitalize(id);
return methodName;
}
3. 解析表达式
/**
*
* 反向解析值
*/
private static Object reverseByExp(Object propertyValue, String converterExp) {
for (String item : converterExp.split("\\|")) {
String[] itemArray = item.split("=");
if (itemArray[0].equals(propertyValue)) {
return itemArray[1];
}
}
return propertyValue;
}
- 执行handler( 以UPDATE操作为例)
private void handleUpdateRecord(Update update, Object newObj) {
try {
Class clazz = newObj.getClass();
//根据默认方法名称 查询 update之前数据
Object oldObj = sqlSession.selectOne(getName(clazz, update.primaryKey()), ReflectUtils.invokeGetter(newObj, update.primaryKey()));
Object id = ReflectUtils.invokeGetter(oldObj, update.id());
List<UpdateField> newList = getClassField(clazz, newObj);
List<UpdateField> oldList = getClassField(clazz, oldObj);
// 删除field
oldList.remove(0);
UpdateField fieldUpdateType = newList.remove(0);
for (int i = 0; i < newList.size(); i++) {
UpdateField oldUpdateField = oldList.get(i);
UpdateField newUpdateField = newList.get(i);
UpdateRecord updateRecord = new UpdateRecord();
updateRecord.setTypeId(String.valueOf(id));
updateRecord.setType(update.updateRecordEnum().getType());
Object oldProperty = oldUpdateField.getProperty();
Object newProperty = newUpdateField.getProperty();
if (oldProperty == null) {
if (newProperty == null || (StringUtils.isEmpty(newProperty))) {
break;
}
} else if (oldProperty.getClass() == String.class && StringUtils.isEmpty(oldProperty.toString())) {
if (newProperty == null || (StringUtils.isEmpty(newProperty))) {
break;
}
} else if (!oldUpdateField.getProperty().equals(newUpdateField.getProperty())) {
updateRecord.setOldText(oldProperty.toString());
}
AsyncManager.me().execute(AsyncFactory.recordUpdate(setUpdateRecord(updateRecord, newUpdateField, fieldUpdateType)));
}
} catch (Exception e) {
e.printStackTrace();
}
}
四、使用说明
- field = true 描述的成员是不能修改的
- 由于水平有限,代码只供参考。这代码质量用到项目里怕是有点难。。。
- 项目里比较乱,东拼西凑的代码,请见谅。
五、未解决问题
如果拦截的方法抛了异常。回滚的问题目前还在考虑!!!
六、总结
总体上来说功能算是实现了,但是细节上得打磨一下。我估摸着如果公司项目里不出BUG,我是不会改了,能跑就行要求不高,水平有限。
项目地址