在日常开发过程中,有时候需要记录用户修改了某条数据的哪些字段,需要知道修改前和修改后的数据,如以下示例:修改用户名和昵称。
这是我个人做的一个通用后台管理系统练习项目;
后端架构:
> springboot > mybatis-plus > druid > saToken > mysql > redis > quartz > swagger+knife4 > 持续迭代中...
前端架构:
> vue3 > vite > elementPlus > axios > pinia > vform3 > vue-router > vuex
目前已完成基本权限管理,支持开箱即用,持续迭代中...
用户名原始值:test1
昵称原始值:测试用户
用户名修改为:test666
昵称修改为:测试用户666
查看变更记录列表如图:
1.创建注解
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface ChangeRecordLog {
// 获取编辑信息的解析类,目前为使用id获取
Class parseclass() default DefaultContentParse.class;
// 查询数据库所调用的class文件 selectById方法所在的Service类
Class serviceclass() default IService.class;
// 是否需要比较新旧数据
boolean needDefaultCompare() default false;
// id的类型
Class idType() default Integer.class;
// 操作对象的id字段名称
String tableId() default "id";
//操作类型 add update delete
String type() default "update";
}
2.创建切面类
@Aspect
@Component
@Slf4j
@SuppressWarnings("all")
public class ChangeRecordAspect {
@Autowired
private ChangeRecordMapper changeRecordMapper;
@Autowired
private ApplicationContext applicationContext;
//保存修改之前的数据
Map<String, Object> oldMap = new HashMap<>();
/**
* 处理请求前执行
*/
@Before(value = "@annotation(operateLog)")
public void boBefore(JoinPoint joinPoint, ChangeRecordLog operateLog)
{
ContentParser contentParser = (ContentParser) applicationContext.getBean(operateLog.parseclass());
//旧值
Object oldObject = contentParser.getResult(joinPoint, operateLog, operateLog.tableId());
if (operateLog.needDefaultCompare()) {
oldMap = (Map<String, Object>) objectToMap(oldObject); // 存储修改前的对象
}
}
/**
* 处理请求后执行
* @param joinPoint
* @param operateLog
* @throws Throwable
*/
@After("@annotation(operateLog)")
public void around(JoinPoint joinPoint, ChangeRecordLog operateLog) throws Throwable {
try {
ChangeRecord changeRecord = new ChangeRecord();
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 从切面织入点处通过反射机制获取织入点处的方法
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod(); // 获取切入点所在的方法
String meName = method.getName(); // 获取请求的方法名
String className = joinPoint.getTarget().getClass().getName(); // 获取请求的类名
String methodName = className + "." + meName;
String uri = request.getRequestURL().toString(); // 请求uri
Object[] args = joinPoint.getArgs();
// 请求参数 预留
//String requestParams = JSON.toJSONString(args);
//创建人信息
changeRecord.setChangeTime(new Date());
changeRecord.setCreateName(LoginUtils.getUsername());
Object object = null;
ContentParser contentParser;
try {
contentParser = (ContentParser) applicationContext.getBean(operateLog.parseclass());
object = contentParser.getResult(joinPoint, operateLog, operateLog.tableId());
} catch (Exception e) {
e.printStackTrace();
log.error("service加载失败:", e);
}
if ("add".equals(operateLog.type())) {
Map<String, Object> dataMap = (Map<String, Object>) objectToMap(object);
log.info("新增的数据:{}" + dataMap.toString());
}
/*if ("delete".equals(operateLog.type())) {
log.info("删除的数据:{}" + oldMap);
}*/
if (operateLog.needDefaultCompare()) {
//比较新数据与数据库原数据
List<Map<String, Object>> list = defaultDealUpdate(object, oldMap, operateLog.tableId());
for (Map<String, Object> dataMap : list) {
changeRecord.setChangeField(String.valueOf(dataMap.get("filedName")));
changeRecord.setBeforeChange(String.valueOf(dataMap.get("oldValue")));
changeRecord.setAfterChange(String.valueOf(dataMap.get("newValue")));
changeRecord.setTypeId(Long.parseLong(String.valueOf(dataMap.get(operateLog.tableId()))));
String remark = "修改" + changeRecord.getChangeField() + "为" + changeRecord.getAfterChange() +
",原" + changeRecord.getChangeField() + "为" +
(StringUtils.isBlank(changeRecord.getBeforeChange()) ? "空" : changeRecord.getBeforeChange());
changeRecord.setRemark(remark);
changeRecordMapper.insert(changeRecord);
}
}
} catch (Exception e) {
log.info("自定义变更记录注解出现异常");
e.printStackTrace();
}
}
private List<Map<String, Object>> defaultDealUpdate(Object newObject, Map<String, Object> oldMap, String tableId) {
try {
List<Map<String, Object>> list = new ArrayList<>();
Map<String, Object> newMap = (Map<String, Object>) objectToMap(newObject);
Object finalNewObject = newObject;
oldMap.forEach((k, v) -> {
Object newResult = newMap.get(k);
if (null != v && !v.equals(newResult)) {
Field field = ReflectionUtils.getAccessibleField(finalNewObject, k);
DataName dataName = field.getAnnotation(DataName.class);
if (null != dataName && StringUtils.isNotEmpty(dataName.name())) {
//翻译表达式 0=男,1=女
String readConverterExp = dataName.readConverterExp();
Map result = new HashMap();
result.put("filedName", dataName.name());
result.put(tableId, newMap.get(tableId));
if (StringUtils.isNotEmpty(dataName.readConverterExp())) {
String oldValue = convertByExp(
String.valueOf(v), dataName.readConverterExp(), ",");
String newValue = convertByExp(
String.valueOf(newResult), dataName.readConverterExp(), ",");
result.put("oldValue", oldValue);
result.put("newValue", newValue);
} else {
result.put("oldValue", v);
result.put("newValue", newResult);
}
list.add(result);
}
}
});
log.info("比较的数据哈:{}" + list.toString());
return list;
} catch (Exception e) {
log.error("比较异常", e);
e.printStackTrace();
throw new RuntimeException("比较异常", e);
}
}
private Map<?, ?> objectToMap(Object obj) {
if (obj == null) {
return null;
}
ObjectMapper mapper = new ObjectMapper();
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
// 如果使用JPA请自己打开这条配置
// mapper.addMixIn(Object.class, IgnoreHibernatePropertiesInJackson.class);
Map<?, ?> mappedObject = mapper.convertValue(obj, Map.class);
return mappedObject;
}
/**
* 翻译
*
* @param propertyValue 参数值如:0
* @param converterExp 翻译注解的值如:0=男,1=女,2=未知
* @param separator 分隔符
* @return 解析后值
*/
public static String convertByExp(String propertyValue, String converterExp, String separator) {
StringBuilder propertyString = new StringBuilder();
String[] convertSource = converterExp.split(",");
for (String item : convertSource) {
String[] itemArray = item.split("=");
if (StringUtils.containsAny(propertyValue, separator)) {
for (String value : propertyValue.split(separator)) {
if (itemArray[0].equals(value)) {
propertyString.append(itemArray[1] + separator);
break;
}
}
} else {
if (itemArray[0].equals(propertyValue)) {
return itemArray[1];
}
}
}
return StringUtils.stripEnd(propertyString.toString(), separator);
}
}
3.字段别名注解
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
@Documented
public @interface DataName {
String name() default ""; // 字段名称
/**
* 读取枚举内容转义表达式 (如: 0=男,1=女,2=未知)
*/
String readConverterExp() default "";
}
//在实体类上使用
/**
* 用户账号
*/
@DataName(name = "用户名")
private String userName;
/**
* 用户昵称
*/
@DataName(name = "昵称")
private String nickName;
4.工具类-解析接口
public interface ContentParser {
/**
* 获取信息返回查询出的对象
*
* @param joinPoint 查询条件的参数
* @param operateLog 注解
* @return 获得的结果
*/
public Object getResult(JoinPoint joinPoint, ChangeRecordLog operateLog, String tableId);
}
5.工具类-解析接口实现类
@Component
public class DefaultContentParse implements ContentParser {
@Autowired
private ApplicationContext applicationContext;
@Override
public Object getResult(JoinPoint joinPoint, ChangeRecordLog operateLog, String tableId) {
Object info = joinPoint.getArgs()[0];
Object id = ReflectionUtils.getFieldValue(info, tableId);
Assert.notNull(id, "id不能为空");
Class idType = operateLog.idType();
if (idType.isInstance(id)) {
Class cls = operateLog.serviceclass();
IService service = (IService) applicationContext.getBean(cls);
Object result = service.getById((Serializable) id);
return result;
} else {
throw new RuntimeException("请核实id type");
}
}
}
6.反射工具类
@Slf4j
public class ReflectionUtils {
/**
* 直接读取对象属性值, 无视private/protected修饰符, 不经过getter函数.
* @param obj 读取的对象
* @param fieldName 读取的列
* @return 属性值
*/
public static Object getFieldValue(final Object obj, final String fieldName) {
Field field = getAccessibleField(obj, fieldName);
if (field == null) {
throw new IllegalArgumentException("Could not find field [" + fieldName + "] on target [" + obj + "]");
}
Object result = null;
try {
result = field.get(obj);
} catch (IllegalAccessException e) {
log.error("不可能抛出的异常{}", e.getMessage());
e.printStackTrace();
}
return result;
}
/**
* 循环向上转型, 获取对象的DeclaredField, 并强制设置为可访问.如向上转型到Object仍无法找到, 返回null.
* @param obj 查找的对象
* @param fieldName 列名
* @return 列
*/
public static Field getAccessibleField(final Object obj, final String fieldName) {
for (Class<?> superClass = obj.getClass(); superClass != Object.class; superClass = superClass.getSuperclass()) {
try {
Field field = superClass.getDeclaredField(fieldName);
makeAccessible(field);
return field;
} catch (NoSuchFieldException e) { // NOSONAR
// Field不在当前类定义,继续向上转型
e.printStackTrace();
continue; // new add
}
}
return null;
}
/**
* 改变private/protected的成员变量为public,尽量不调用实际改动的语句,避免JDK的SecurityManager抱怨。
* @param
*/
public static void makeAccessible(Field field) {
if ((!Modifier.isPublic(field.getModifiers()) || !Modifier.isPublic(field.getDeclaringClass().getModifiers()) || Modifier
.isFinal(field.getModifiers())) && !field.isAccessible()) {
field.setAccessible(true);
}
}
/**
* 获取两个对象同名属性内容不相同的列表
* @param class1 old对象
* @param class2 new对象
* @return 区别列表
* @throws ClassNotFoundException 异常
* @throws IllegalAccessException 异常
*/
public static List<Map<String ,Object>> compareTwoClass(Object class1, Object class2) throws ClassNotFoundException, IllegalAccessException {
List<Map<String,Object>> list=new ArrayList<Map<String, Object>>();
// 获取对象的class
Class<?> clazz1 = class1.getClass();
Class<?> clazz2 = class2.getClass();
// 获取对象的属性列表
Field[] field1 = clazz1.getDeclaredFields();
Field[] field2 = clazz2.getDeclaredFields();
StringBuilder sb=new StringBuilder();
// 遍历属性列表field1
for(int i=0;i<field1.length;i++) {
// 遍历属性列表field2
for (int j = 0; j < field2.length; j++) {
// 如果field1[i]属性名与field2[j]属性名内容相同
if (field1[i].getName().equals(field2[j].getName())) {
if (field1[i].getName().equals(field2[j].getName())) {
field1[i].setAccessible(true);
field2[j].setAccessible(true);
// 如果field1[i]属性值与field2[j]属性值内容不相同
if (!compareTwo(field1[i].get(class1), field2[j].get(class2))) {
Map<String, Object> map2 = new HashMap<String, Object>();
DataName name=field1[i].getAnnotation(DataName.class);
String fieldName="";
if(name!=null){
fieldName=name.name();
} else {
fieldName=field1[i].getName();
}
map2.put("name", fieldName);
map2.put("old", field1[i].get(class1));
map2.put("new", field2[j].get(class2));
list.add(map2);
}
break;
}
}
}
}
return list;
}
/**
* 对比两个数据是否内容相同
* @param object1 比较对象1
* @param object2 比较对象2
* @return boolean类型
*/
public static boolean compareTwo(Object object1,Object object2){
if(object1==null&&object2==null){
return true;
}
if(object1==null&&object2!=null){
return false;
}
if(object1.equals(object2)){
return true;
}
return false;
}
}
7.使用示例:在接口上使用注解
@ChangeRecordLog(serviceclass = SysUserService.class,
needDefaultCompare = true,
idType = Long.class,
tableId = "userId")
@ApiOperation(value = "修改用户")
@PostMapping("/edit")
public R edit(@Validated @RequestBody SysUser user) {
if (StringUtils.isNotNull(user.getUserId()) && user.isAdmin()) {
return R.fail("不允许操作超级管理员用户");
}
if (!userService.checkUserNameUnique(user)) {
return R.fail("修改用户'" + user.getUserName() + "'失败,登录账号已存在");
} else if (StringUtils.isNotEmpty(user.getPhonenumber()) && !userService.checkPhoneUnique(user)) {
return R.fail("修改用户'" + user.getUserName() + "'失败,手机号码已存在");
}
userService.updateUser(user);
return R.ok();
}