前言
许久不见甚是想念。
背景
上个月的需求中碰到过这样的一个场景,我需要在版本记录表中查询出最新的数据和旧的数据,然后一一比较所有的属性,如果新数据相比于旧数据的属性值发生了改变,就保留新数据的该属性值,如果未发生改变就将该属性置为null。 对于List等集合属性,我们需要将未发生变化集合元素移除,只保留变更的元素。
思考过程
刚开始的话最先想到的就是依次获取所有属性的值与旧数据的值一一比较,但是我打开这个DTO一看,属性太多了,像我这种懒人是不可能这样一一遍历的,既费时间又不优雅。
如下:
if(Objects.equals(newData.getA(),oldData.getA())){
newData.setA(null);
}
if(Objects.equals(newData.getB(),oldData.getB())){
newData.setA(null);
}
if(Objects.equals(newData.getC(),oldData.getC())){
newData.setA(null);
}
if(Objects.equals(newData.getD(),oldData.getD())){
newData.setA(null);
}
if(Objects.equals(newData.getE(),oldData.getE())){
newData.setA(null);
}
......
于是我就另辟蹊径去了,仔细一想,通过反射可以获取类的所有属性,不知道能不能行。反射的八股文背的倒是很流畅,但是真正使用起来还是不熟练的,突然想起来之前面试的时候被问过,你在开发过程中有没有使用过反射,我的印象中除了一些使用的工具类(BeanUtils等)底层的原理是反射并没有真正的用过反射的一些api。这就使得我更加确定要用反射去完成这个场景了,同时为了实现在多处地方调用,我需要把这个方法抽取出来,因此我这个方法要满足所有的类,并非只固定一种类型,因此使用Object类作为入参。
第一版代码:
public static void retainChangedFields(Object newData, Object oldData) {
if (newData == null || oldData == null || !newData.getClass().equals(oldData.getClass())) {
return;
}
Field[] fields = newData.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
try {
Object newValue = field.get(newData);
Object oldValue = field.get(oldData);
if (Objects.equals(newValue, oldValue)) {
field.set(newData, null);
}
} catch (Exception e) {
logger.error("获取字段值错误", e);
}
}
}
后来又需要排除一些字段不参与比较,就有了第二版:
public static void retainChangedFields(Object newData, Object oldData,Set<String> excludeFields) {
if (newData == null || oldData == null || !newData.getClass().equals(oldData.getClass())) {
return;
}
Field[] fields = newData.getClass().getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
try {
if (excludeFields != null && excludeFields.contains(field.getName())) {
continue;
}
Object newValue = field.get(newData);
Object oldValue = field.get(oldData);
if (Objects.equals(newValue, oldValue)) {
field.set(newData, null);
}
} catch (Exception e) {
logger.error("获取字段值错误", e);
}
}
}
这样我们就只需要将要排除的字段放入Set中传进来就可以了。
但是后来测试发现了bug,我这样做只能处理非集合属性,对于集合属性并不能达到我想要的效果(对于集合属性,保留变化了的元素,没有变化的元素remove掉),于是我就开始了我的第三版。代码解析在代码的注释里,大家可以看看。
public static void retainChangedFields(Object newData, Object oldData,Set<String> excludeFields,Set<String> listExcludeFields) {
if (newData == null || oldData == null || !newData.getClass().equals(oldData.getClass())) {
return;
}
//获取newData的所有声明字段(不包括父类的字段)
Field[] fields = newData.getClass().getDeclaredFields();
for (Field field : fields) {
//设置允许访问私有的字段
field.setAccessible(true);
try {
//如果是要排除的字段就跳过
if (excludeFields != null && excludeFields.contains(field.getName())) {
continue;
}
//获取字段对应的值
Object newValue = field.get(newData);
Object oldValue = field.get(oldData);
//处理 List 类型的字段
if (newValue instanceof List && oldValue instanceof List) {
List<?> newList = new ArrayList<>((List<?>) newValue);
List<?> oldList = (List<?>) oldValue;
// 将 oldList 中的每个元素进行处理(通过 copyObjectExcludeFields 排除指定字段后)并存入一个 Set 中。
Set<Object> oldSet = oldList.stream()
.map(data -> copyObjectExcludeFields(data, listExcludeFields))
.collect(Collectors.toSet());
// 移除 newList 中那些在 oldSet 中存在的元素(同样是经过 copyObjectExcludeFields 处理后比较)
newList.removeIf(item -> {
Object copy = copyObjectExcludeFields(item, listExcludeFields);
return oldSet.contains(copy);
});
field.set(newData, newList.isEmpty() ? null : newList);
} else {
if (Objects.equals(newValue, oldValue)) {
field.set(newData, null);
}
}
} catch (Exception e) {
logger.error("获取字段值错误", e);
}
}
}
这样就完美解决了集合属性的问题了,简单且优雅,也能通用的被其他类型调用了,也是很好的使用了一次反射了,下次面试再被问到是否使用过反射就有的说了。 (以上代码可能会有小错误,大家见谅)
ps:本来已经写好了一篇文章但是由于部分内容并没有完成(比较忙,需求很多),因此先发这篇文章了,下一篇文章中简单概述了一下我前段时间的一些情况,时间线上其实是在这篇文章之前的,大家敬请期待。