web/springboot數據變更歷史記錄設計
在一些領域,記錄數據的變更歷史是非常重要的。比如人力資源系統…
需要記錄個人的成長曆史。再比如一些非常注重安全的系統,希望在必要時可以對所有的歷史操作追根溯源,有據可查。
1.前言
比如,修改一個人的姓名從“張三”變爲了“李四”,那麼在進行記錄的時候,記錄的信息可能如下:
姓名:(張三)=>(李四);
如圖:
這樣就很好的體現出了修改了哪個字段,修改前後的數據分別是什麼。關鍵的信息無論怎麼修改都會有據可查,時間、人物、修改數據前後信息等。
2.實現方式(較low):
直接做個工具類,調用傳入 即可:
package com.bonc.util;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public class Test<T> {
public String contrastObj(Object oldBean, Object newBean) {
String str="";
T pojo1 = (T) oldBean;
T pojo2 = (T) newBean;
try {
Class clazz = pojo1.getClass();
Field[] fields = pojo1.getClass().getDeclaredFields();
int i=1;
for (Field field : fields) {
if("serialVersionUID".equals(field.getName())){
continue;
}
PropertyDescriptor pd = new PropertyDescriptor(field.getName(), clazz);
Method getMethod = pd.getReadMethod();
Object o1 = getMethod.invoke(pojo1);
Object o2 = getMethod.invoke(pojo2);
if(o1==null || o2 == null){
continue;
}
if (!o1.toString().equals(o2.toString())) {
if(i!=1){
str+=";";
}
str+=i+"字段名稱:"+field.getName()+",舊值:"+o1+",新值:"+o2;
i++;
}
}
} catch (Exception e) {
e.printStackTrace();
}
return str;
}
public static void main(String[] args) {
// 模擬舊數據
entity oldModel = new entity();
oldModel.setId("1");
oldModel.setName("張三");
// 模擬新數據
entity model = new entity();
model.setId("2");
model.setName("李四");
Test<entity> t= new Test<>();
String list = t.contrastObj(oldModel,model);
System.out.println("oldModel:"+oldModel);
System.out.println("model:"+model);
System.out.println("list:"+list);
}
}
當然需要建個實體類:
@Data
public class entity {
private String id;
private String name;
}
結果如圖:
最後插入歷史記錄表中即可。
3.更優雅的方式(推薦):
通過java反射機制來實現。
JAVA反射機制是在運行狀態中,對於任意一個類,都能夠知道這個類的所有屬性和方法;對於任意一個對象,都能夠調用它的任意方法和屬性。
思路:
- 獲取到兩個對象中屬性列表,
- 遍歷對比,
- 屬性名相同屬性值不同的把屬性名及兩個對象的屬性值保存進Map<String,Object>裏,
- 返回List<Map<String,Object>
源碼如下:
1.新建FieldMeta 類:
java的反射機制,通過註解實現。
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME) // 註解會在class字節碼文件中存在,在運行時可以通過反射獲取到
@Target({ElementType.FIELD,ElementType.METHOD})//定義註解的作用目標**作用範圍字段、枚舉的常量/方法
@Documented //說明該註解將被包含在javadoc中
public @interface FieldMeta {
String name() default "";
String description() default "";
}
2.新建CompareObjectUtils類:
對比兩個對象中同名屬性的值是否相同。
package com.zoutao.web.entity;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Component
public class CompareObjectUtils{
private static CompareObjectUtils compareObjectUtils;
@PostConstruct
public void init() {
compareObjectUtils = this;
}
/**
* 獲取兩個對象同名屬性內容不相同的列表
* @param class1 對象1
* @param class2 對象2
* @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();
//遍歷屬性列表field1
for (int i = 0; i < field1.length; i++) {
if(field1[i].isAnnotationPresent(FieldMeta.class))
//遍歷屬性列表field2
for (int j = 0; j < field2.length; j++) {
//如果field1[i]屬性名與field2[j]屬性名內容相同
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)) && field1[i].isAnnotationPresent(FieldMeta.class) && field2[j].isAnnotationPresent(FieldMeta.class)) {
FieldMeta metaAnnotation = field1[i].getAnnotation(FieldMeta.class);
Map<String, Object> map2 = new HashMap<String, Object>();
map2.put("name", metaAnnotation.name());
map2.put("old", field1[i].get(class1));
map2.put("new", field2[j].get(class2));
list.add(map2);
}
break;
}
}
}
return list;
}
//對比兩個數據是否內容相同
public static boolean compareTwo(Object object1, Object object2) {
if (object1 == null && object2 == null) {
return true;
}
// 因源數據是沒有進行賦值,是null值,改爲""。
//if (object1 == "" && object2 == null) {
// return true;
//}
//if (object1 == null && object2 == "") {
// return true;
// }
if (object1 == null && object2 != null) {
return false;
}
if (object1.equals(object2)) {
return true;
}
return false;
}
}
3.調用方式:
impl中的方法,controller調用即可。BasePowerOnRateServiceImpl.java:
// 隨着開機率信息model變更而更新歷史記錄表
@Override
public boolean updateWithItem(BasePowerOnRate model) throws IllegalAccessException, ClassNotFoundException {
// 查舊數據
BasePowerOnRate oldModel = basePowerOnRateMapper.selectById(model.getId());
List<Map<String, Object>> list = new ArrayList<>();
list = CompareObjectUtils.compareTwoClass(oldModel,model); //舊新數據對比
String content = ""; // 定義變更字符串
for(Map<String, Object> map : list){
content += map.get("name") + ":" + map.get("old") + " 變更爲 " + map.get("new") + ";";
}
if(content.length()>0){
BasePowerOnRateHistory item = new BasePowerOnRateHistory(); // 數據變更實體對象
item.setBootUpId(model.getId());
item.setChangeContent(content); //變更內容
basePowerOnRateHistoryMapper.insert(item); //記錄表新增歷史
}
basePowerOnRateMapper.updateById(model); //更新原數據表
return true;
}
效果如圖:
簡單容易實現,也不易出現問題,判斷傳入的對象中是否有 id,如果有 id 則說明是修改,如果沒有 id 則說明是新建。
注意:
1.該方式並不完美,如果新添加的字段有對應的字典,那麼需要添加字典對應的關聯,這樣就需要每次修改代碼,但是上訴滿足日常web項目開發需求。
2.這種方式在高併發的情況下不適用,可以通過kafka將數據收集到行式數據庫,用更新flag來代替刪除,就能很容易看到數據的變更記錄了,即使過億級別查詢也非常快。
==
本文結束,下次見~