引言

​ 开发过程中我们会使用版本控制工具,如SVN、Git等。但是我们一样会遇到一种情形:在一套试题系统中,有新建题目、编辑题目、删除题目等功能,且题目可以被多个人修改,每人修改一次即是一个版本。现在的需求就是需要记录每一次修改的详细信息每次版本之间的差异,甚至还可以版本回滚

​ 例如题目编号为20170919170800000061的题目被A创建,依次被BCD各修改了一次,此时需要比较BA间的差异、CB间的差异、DC间的差异,到最后审核阶段如果B 的版本比较符合,则需要把试题版本内容回滚到B版本作为最后的版本。

仔细分析一下题干,我们的需求是1.比较版本的差异,2.版本的回滚。之前有考虑过两种方案:

  1. 修改时在前端进行比较,只记录版本的差异,后台只需要进行存取即可。
  2. 把所有版本信息全部存储在数据库,在请求时后台进行比较差异。

第一种方案带来的问题是没法进行版本回滚,只记录下来了差异,回滚时将会是灾难,那么第二种方案才是较合适的选择。

下图是数据库中的版本修改记录

题目的所有信息全部存储在itemJson中,比较版本间的差异即是比较版本间的itemJson,现在的目标就是要提取两个版本中itemJson中的差异。

通过从网上查找资料找到了两种比较合适的方法,值得借鉴一下。

版本差异(比较Json的方法)

计较两个Json(X,Y),其中可能情况:

  1. X和Y中均有相同字段
  2. X中存在Y中不存在的字段
  3. Y中存在X中不存在的字段

需要掌握:

  1. 各个字段的用处和意义
  2. 字段在Map、Json、JavaBean、List、JsonString形态之间的转换

通过Map间接比较

引入Maven依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.9.1</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>

定义静态工具类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//处理json字符串
public static <T> T readJsonToObject(String jsonString, TypeReference<T> tr) {
ObjectMapper objectMapper = new ObjectMapper();
if (jsonString == null || "".equals(jsonString)) {
return null;
} else {
try {
return (T) objectMapper.readValue(jsonString, tr);
} catch (Exception e) {

logger.debug("json error:" + e);
}
}
return null;
}

定义Map比较的工具类

​ 通过google的guava表达式中的 *Maps.difference(map1,map2)*方法进行比较,单此方法可比较正常的Map和String内容,对于List方式的比较,同时进行了数值和list内容顺序的比较,显然不符合我们的匹配规则,所以我们要对这个方法配合List的containAll方法进一步做封装。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
public static List<Map<String, String>> compareMap(Map<String, Object> oldVersion, Map<String, Object> newVersion) {
MapDifference<String, Object> difference = Maps.difference(oldVersion, newVersion);

// 获取所有不同点
Map<String, MapDifference.ValueDifference<Object>> differenceMap = difference.entriesDiffering();
List<Map<String, String>> result = new ArrayList<>();
Iterator diffIterator = differenceMap.entrySet().iterator();
while (diffIterator.hasNext()) {
Map.Entry entry = (java.util.Map.Entry) diffIterator.next();

MapDifference.ValueDifference<Object> valueDifference = (MapDifference.ValueDifference<Object>) entry
.getValue();
boolean isList = valueDifference.leftValue() instanceof List
&& valueDifference.rightValue() instanceof List;
boolean isMap = valueDifference.leftValue() instanceof Map && valueDifference.rightValue() instanceof Map;
if (!isList && !isMap) {
Map<String, String> map = new HashMap<>();
String fieldKey = String.valueOf(entry.getKey());

// 选择题中选项内容改变
if (oldVersion.get("content") != null && oldVersion.get("name") != null) {
map.put("fieldName", judgeOption(oldVersion.get("name").toString()));
} else {
map.put("fieldName", judgeFiledName(fieldKey));
}
map.put("fieldKey", fieldKey);
map.put("oldValue", judgeFiledKey(fieldKey, valueDifference.leftValue().toString()));
map.put("newValue", judgeFiledKey(fieldKey, valueDifference.rightValue().toString()));
result.add(map);
}

// 处理结果是否为List,则递归执行比较规则
if (valueDifference.leftValue() instanceof List && valueDifference.rightValue() instanceof List) {
JSONArray j = JSONArray.parseArray(JSON.toJSONString(valueDifference.leftValue()));
JSONArray p = JSONArray.parseArray(JSON.toJSONString(valueDifference.rightValue()));
JSONObject js = new JSONObject();
JSONObject js1 = new JSONObject();
for (int i = 0; i < j.size(); i++) {
js.put(i + "", j.get(i));
}
for (int i = 0; i < p.size(); i++) {
js1.put(i + "", p.get(i));
}
Map<String, Object> requestMap = JsonUtils.readJsonToObject(js.toString(),
new TypeReference<Map<String, Object>>() {
});
Map<String, Object> requestMap1 = JsonUtils.readJsonToObject(js1.toString(),
new TypeReference<Map<String, Object>>() {
});
List<Map<String, String>> m = compareMap(requestMap, requestMap1); //当修改多个选项
for (int i = 0; i < m.size(); i++) {
result.add(compareMap(requestMap, requestMap1).get(i));
}
}
// 处理结果是否为Map,则递归执行比较规则
if (valueDifference.leftValue() instanceof Map && valueDifference.rightValue() instanceof Map) {
result.add(compareMap((Map<String, Object>) valueDifference.leftValue(),
(Map<String, Object>) valueDifference.rightValue()).get(0));
}
}

// 若A中有B中不存在的值
Map<String, Object> entriesOnlyOnLeft = difference.entriesOnlyOnLeft();
if (entriesOnlyOnLeft != null && !entriesOnlyOnLeft.isEmpty()) {

Iterator it = entriesOnlyOnLeft.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = (java.util.Map.Entry) it.next();
Map<String, String> map = new HashMap<>();
String fieldKey = entry.getKey();
map.put("fieldKey", fieldKey);
map.put("fieldName", judgeFiledName(fieldKey));
map.put("oldValue", judgeFiledKey(fieldKey, String.valueOf(entry.getValue())));
map.put("newValue", "");
result.add(map);
}
}

// 若B中有A中不存在的值
Map<String, Object> onlyOnRightMap = difference.entriesOnlyOnRight();
if (onlyOnRightMap != null && !onlyOnRightMap.isEmpty()) {
Iterator it = onlyOnRightMap.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<String, String> entry = (java.util.Map.Entry) it.next();
Map<String, String> map = new HashMap<>();
String fieldKey = String.valueOf(entry.getKey());
map.put("fieldKey", fieldKey);
map.put("fieldName", judgeFiledName(fieldKey));
map.put("oldValue", "");
map.put("newValue", judgeFiledKey(fieldKey, String.valueOf(entry.getValue())));
result.add(map);
}
}
return result;
}

定义静态调用方法

1
2
3
4
5
6
7
public static List<Map<String, String>> compareJSON(String jsonOld, String jsonNew) {
Map<String, Object> oldVersion = JsonUtils.readJsonToObject(jsonOld, new TypeReference<Map<String, Object>>() {
});
Map<String, Object> newVersion = JsonUtils.readJsonToObject(jsonNew, new TypeReference<Map<String, Object>>() {
});
return compareMap(oldVersion, newVersion);
}

返回结果

经过处理后的返回结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
[
{
"newValue": "10",
"fieldName": "分数",
"fieldKey": "score",
"oldValue": "8"
},
{
"newValue": "10",
"fieldName": "教材依据",
"fieldKey": "teachingMaterialBasis",
"oldValue": "2"
},
{
"newValue": "2",
"fieldName": "大纲依据",
"fieldKey": "syllabusBasis",
"oldValue": "1"
},
{
"newValue": "1.0",
"fieldName": "难度系数",
"fieldKey": "difficult",
"oldValue": "0.4"
},
{
"newValue": "掌握",
"fieldName": "能力层次",
"fieldKey": "abilityLevel",
"oldValue": "熟悉"
},
{
"newValue": "测试关键词",
"fieldName": "关键字",
"fieldKey": "keyWord",
"oldValue": "个"
}
]

查看所有源码点击:下载

转为JavaBean比较

将itemJson字符串转化为JavaBean,比较JavaBean之前的差异。比较JavaBean间的差异可以用Javers。

引入Javers的Maven依赖

1
2
3
4
5
<dependency>
<groupId>org.javers</groupId>
<artifactId>javers-core</artifactId>
<version>3.5.0</version>
</dependency>

将itemJson转为JavaBean

方式一:利用Jackson

工具类JsonUtils
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public class JsonUtils {
// 定义jackson对象
private static final ObjectMapper MAPPER = new ObjectMapper();

/**
* 将对象转换成json字符串。
* <p>Title: pojoToJson</p>
* <p>Description: </p>
* @param data
* @return
*/
public static String objectToJson(Object data) {
try {
String string = MAPPER.writeValueAsString(data);
return string;
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return null;
}

/**
* 将json结果集转化为对象
*
* @param jsonData json数据
* @param clazz 对象中的object类型
* @return
*/
public static <T> T jsonToPojo(String jsonData, Class<T> beanType) {
try {
T t = MAPPER.readValue(jsonData, beanType);
return t;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}

/**
* 将json数据转换成pojo对象list
* <p>Title: jsonToList</p>
* <p>Description: </p>
* @param jsonData
* @param beanType
* @return
*/
public static <T>List<T> jsonToList(String jsonData, Class<T> beanType) {
JavaType javaType = MAPPER.getTypeFactory().constructParametricType(List.class, beanType);
try {
List<T> list = MAPPER.readValue(jsonData, javaType);
return list;
} catch (Exception e) {
e.printStackTrace();
}

return null;
}
}
Json转Pojo
1
2
Item item1 = JsonUtils.jsonToPojo(itemJsonOld, Item.class);
Item item2 = JsonUtils.jsonToPojo(itemJsonNew, Item.class);

方式二:利用fastJson

引入Maven依赖
1
2
3
4
5
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.1.26</version>
</dependency>
Json转Pojo
1
2
Item item1 = JSONObject.parseObject(json3, Item.class);
Item item2 = JSONObject.parseObject(json4, Item.class);

利用Javers比较JavaBean

1
2
3
4
5
6
7
8
9
10
11
Javers j = JaversBuilder.javers().build();
Diff diff = j.compare(item1, item2);
if (diff.hasChanges()) {
List<Change> changes = diff.getChanges();
for (Change change : changes) {
if (change instanceof ValueChange) {
ValueChange valChange = (ValueChange) change;
System.out.println(valChange.getPropertyName() + " -- " + valChange.getLeft() + "--" + valChange.getRight());
}
}
}

版本回滚

​ 其实版本回滚在上面的比较中已经说了,就是把需要回滚的版本itemJson转化为JavaBean传给前台,同时生成一份最新的版本为当前版本,记录操作人、操作时间等等记录即可。需要了解及使用Gson、fastJson、Jackson的使用,及使用工具将Map、Json、JavaBean、List、JsonString对象之间状态的转换。

总结

  1. 熟悉业务。
  2. 掌握Map、Json、JavaBean、List、JsonString对象之间状态的转换。
  3. 版本需要存储所有信息便于回滚。
  4. 个人倾向使用Javers比较JavaBean进行比较版本差异。