用户操作日志记录字段修改前后值

你可能遇到这样的需求,要详细记录用户的操作日志,像下面这样:

用户张三将年龄从“20”改为“21”
用户张三将爱好从“篮球”改为“足球”

 通常,用户可以一次改多个字段,然后一次性保存,这些字段的数据修改记录要分别保存。

这样的日志需要知道以下数据:

  1. 用户修改了数据库中什么字段
  2. 这个字段对应的中文名是什么
  3. 这个字段原来的值是什么
  4. 这个字段新值是什么

这几个问题里,1和4是闭着眼都能搞明白的。

问题2,字段的中文名可以用一个枚举类把英文和中文对应起来,但是如果字段很多的话,很繁琐,然后想起了注解,对字段添加注解,是否可以获取字段的中文意思呢?

问题3,这个字段原来的值我们也是知道的,但如何保存呢?我首先想到的是:用户修改时将修改前的旧值和新值从前端一起传过来,但是这样有安全问题,因为用户可以按F12在浏览器修改页面上的旧值,所以需要在修改前从数据库先查询出所有字段的旧值,然后与新值依次对比,从中找到用户修改了哪些字段。

网上搜索一番,看到这篇文章,得到启示,下面按文章思路实现,并加以改进。

自定义一个注解类来标注字段对应的中文。

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 字段中文别名
 * @author test
 */
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface FieldAlias {
    String value() default "";
}

然后,类需要记录日志的字段上加注解:

public class User {
	
	private int userId;
	
	@FieldAlias("姓名")
	private String name;
	
	@FieldAlias("年龄")
	private int age;
	
	@FieldAlias("爱好")
	private String hobby;
	
	public User(){
		
	}
	
	public User(int userId, String name, int age) {
		this.userId = userId;
		this.name = name;
		this.age = age;
	}

    // 这里省略get set 方法
}
/**
 * 两个对象差异-字段新旧值
 * @author test
 */
public class FieldDiff {

	/**
	 * 字段英文名
	 */
	private String fieldENName;
	
	/**
	 * 字段中文名
	 */
	private String fieldCNName;
	
	/**
	 * 旧值
	 */
	private Object oldValue;
	
	/**
	 * 新值
	 */
	private Object newValue;
	
	
	public FieldDiff(String fieldENName, String fieldCNName, Object oldValue, Object newValue) {
		this.fieldENName = fieldENName;
		this.fieldCNName = fieldCNName;
		this.oldValue = oldValue;
		this.newValue = newValue;
	}

    // 这里省略get set 方法

	@Override
	public String toString() {
		String oldVal = this.oldValue == null ? "" : this.oldValue.toString();
        String newVal = this.newValue == null ? "" : this.newValue.toString();
		return "将 " + this.fieldCNName + " 从“" + oldVal + "” 修改为 “" + newVal + "”";
	}
	
}
import java.util.ArrayList;
import java.util.List;

/**
 * 两个对象差异
 * @author test
 */
public class BeanDiff {

	/**
	 * 所有差异字段list
	 */
	private List<FieldDiff> fieldDiffList = new ArrayList<>();
	
	public void addFieldDiff(FieldDiff fieldDiff) {
		this.fieldDiffList.add(fieldDiff);
	}

	public List<FieldDiff> getFieldDiffList() {
		return fieldDiffList;
	}

	public void setFieldDiffList(List<FieldDiff> fieldDiffList) {
		this.fieldDiffList = fieldDiffList;
	}
	
}
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;


/**
 * 实例字段差异比较工具类
 * @author test
 */
public class BeanCompareUtils {
	
	private static final String INCLUDE = "INCLUDE";
	private static final String EXCLUDE = "EXCLUDE";
	private static final String FILTER_TYPE = "FILTER_TYPE";
	private static final String FILTER_ARRAY = "FILTER_ARRAY";
	
	// 存放过滤类型及过滤字段数组
	private static ThreadLocal<Map<String, Object>> threadLocal = new ThreadLocal<>();
	
	public static void main(String[] args) {
		
		User oldUser = new User(1, "张三", 20);
		User newUser = new User(2, "李四", 21);
		oldUser.setMoney(1100.1);
		newUser.setMoney(2200.52);
		
		String[] fieldArray = new String[]{"age","name","hobby"};
		
		BeanDiff beanDiff = compareInclude(oldUser, newUser,fieldArray);
		
		List<FieldDiff> list = beanDiff.getFieldDiffList();
		if (list != null && list.size() > 0) {
			for (int i = 0; i< list.size(); i ++) {
				FieldDiff fieldDiff = list.get(i);
				System.out.println(fieldDiff.toString());
			}
		}
	}
	
	/**
	 * bean比较
	 * @param oldBean
	 * @param newBean
	 * @return
	 */
	public static BeanDiff compare(Object oldBean, Object newBean) {
		BeanDiff beanDiff = new BeanDiff();
		
		Class oldClass = oldBean.getClass();
		Class newClass = newBean.getClass();
		
		if (oldClass.equals(newClass)) {
			List<Field> fieldList = new ArrayList<>();
			fieldList = getCompareFieldList(fieldList, newClass);
			
			Map<String, Object> map = threadLocal.get();
			
			boolean needInclude = false;
			boolean needExclude = false;
			boolean hasArray = false;
			String[] fieldArray = null;
			
			if(map != null) {
				fieldArray = (String[])map.get(FILTER_ARRAY);
				String type = (String)map.get(FILTER_TYPE);
				
				if (fieldArray != null && fieldArray.length > 0) {
					// 数组排序
					Arrays.sort(fieldArray);
					hasArray = true;
					
					if (INCLUDE.equals(type)) {
						needInclude = true;
					} else if (EXCLUDE.equals(type)) {
						needExclude = true;
					}
				}
			}
			
			for (int i = 0; i < fieldList.size(); i ++) {
				Field field = fieldList.get(i);
				field.setAccessible(true);
				FieldAlias alias = field.getAnnotation(FieldAlias.class);
				
				try {
					Object oldValue = field.get(oldBean);
					Object newValue = field.get(newBean);
					
					if (hasArray) {
						// 二分法查找该字段是否被排除或包含
						int idx = Arrays.binarySearch(fieldArray, field.getName());
						
						// 该字段被指定排除或没有指定包含
						if ((needExclude && idx > -1) || (needInclude && idx < 0)) {
							continue;
						}
					}
					
					if (nullableNotEquals(oldValue, newValue)) {
						FieldDiff fieldDiff = new FieldDiff(field.getName(), alias.value(), oldValue, newValue);
						
						// 打印
						System.out.println(fieldDiff.toString());
						
						beanDiff.addFieldDiff(fieldDiff);
					}
					
				} catch (IllegalArgumentException e) {
					e.printStackTrace();
				} catch (IllegalAccessException e) {
					e.printStackTrace();
				}
			}
		}
		
		return beanDiff;
	}
	
	/**
	 * bean比较
	 * @param oldBean
	 * @param newBean
	 * @param includeFieldArray 需要包含的字段
	 * @return
	 */
	public static BeanDiff compareInclude(Object oldBean, Object newBean, String[] includeFieldArray) {
		Map<String, Object> map = new HashMap<>();
		map.put(FILTER_TYPE, INCLUDE);
		map.put(FILTER_ARRAY, includeFieldArray);
		threadLocal.set(map);
		
		return compare(oldBean, newBean);
	}
	
	/**
	 * bean比较
	 * @param oldBean
	 * @param newBean
	 * @param excludeFieldArray 需要排除的字段
	 * @return
	 */
	public static BeanDiff compareExclude(Object oldBean, Object newBean, String[] excludeFieldArray) {
		Map<String, Object> map = new HashMap<>();
		map.put(FILTER_TYPE, EXCLUDE);
		map.put(FILTER_ARRAY, excludeFieldArray);
		threadLocal.set(map);
		
		return compare(oldBean, newBean);
	}
	
	
	/**
	 * 获取需要比较的字段list
	 * @param fieldList
	 * @param clazz
	 * @return
	 */
	private static List<Field> getCompareFieldList(List<Field> fieldList, Class clazz) {
		Field[] fieldArray = clazz.getDeclaredFields();
		
		List<Field> list = Arrays.asList(fieldArray);
		
		for (int i = 0; i < list.size(); i ++) {
			Field field = list.get(i);
			FieldAlias alias = field.getAnnotation(FieldAlias.class);
			if (alias != null) {
				fieldList.add(field);
			}
		}
		
		Class superClass = clazz.getSuperclass();
		if (superClass != null) {
			getCompareFieldList(fieldList, superClass);
		}
		return fieldList;
	}
	
	
	/**
	 * 比较值是否不相等
	 * @param oldValue
	 * @param newValue
	 * @return
	 */
	private static boolean nullableNotEquals(Object oldValue, Object newValue) {
		
		if (oldValue == null && newValue == null) {
			return false;
		}

		if (oldValue != null && oldValue.equals(newValue)) {
			return false;
		}
		
		if (("".equals(oldValue) && newValue == null) || ("".equals(newValue) && oldValue == null)) {
			return false;
		}
		
		return true;
	}
	
}

Spring AOP(Aspect-Oriented Programming,面向切面编程)是一种编程模式,它允许开发者将跨越系统组件的业务关注点抽取出来,封装成独立的“切面”进行管理。对于字段修改操作,AOP可以利用`@Before`, `@Around`, 或 `@AfterReturning` 等通知(advice)来拦截。 如果你想在Spring AOP中监控字段修改前后变化,通常会使用`@Around`通知,因为它可以在方法执行的包围中添加额外的操作。你可以创建一个切面类,其中包含一个环绕通知(around advice),这个通知会在目标方法执行前后获取并记录字段。这里是一个简单的例子: ```java import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; @Aspect @Component public class FieldChangeAspect { @Around("execution(* com.example.YourClass.*(..))") public Object monitorFieldChanges(ProceedingJoinPoint joinPoint) throws Throwable { // 获取当前对象实例 YourClass target = (YourClass) joinPoint.getThis(); // 获取原始字段 Object originalValue = getFieldValue(target, "yourField"); // 执行原方法 Object result = joinPoint.proceed(); // 获取修改后的新 Object newValue = getFieldValue(target, "yourField"); // 这里可以对原始、新进行日志记录、检查或其他处理 // ... return result; } private Object getFieldValue(Object instance, String fieldName) { // 通过反射获取字段 Field field = instance.getClass().getDeclaredField(fieldName); field.setAccessible(true); return field.get(instance); } } ``` 在这个例子中,你需要替换`YourClass`和`"yourField"`为你实际的关注类和字段名。`getFieldValue`方法用于通过反射获取和设置字段
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值