一、如何实现不同类型对象之间的复制问题?
1、为什么会有这个问题?
近来在进行一个项目开发的时候,为了隐藏后端数据库表结构、同时也为了配合给前端一个更友好的API接口文档(swagger API文档),我采用POJO来对应数据表结构,使用VO来给传递前端要展示的数据,同时使用DTO来进行请求参数的封装。以上是一个具体的场景,可以发现这样子一个现象:POJO、VO、DTO对象是同一个数据的不同视图,所以会有很多相同的字段,由于不同的地方使用不同的对象,无可避免的会存在对象之间的值迁移问题,迁移的一个特征就是需要迁移的值字段相同。字段相同,于是才有了不同对象之间进行值迁移复制的问题。
2、现有的解决方法
一个一个的get出来后又set进去。这个方法无可避免会增加很多的编码复杂度,还是一些很没有营养的代码,看多了还会烦,所以作为一个有点小追求的程序员都没有办法忍受这种摧残。
使用别人已经存在的工具。在spring包里面有一个可以复制对象属性的工具方法,可以进行对象值的复制,下一段我们详细去分析它的这个工具方法。
自己动手丰衣足食。自己造工具来用,之所以自己造工具不是因为喜欢造工具,而是现有的工具没办法解决自己的需求,不得已而为之。
二、他山之石可以攻玉,详谈spring的对象复制工具
1、看看spring的对象复制工具到底咋样?
类名:org.springframework.beans.BeanUtils
这个类里面所有的属性复制的方法都调用了同一个方法,我们就直接分析这个原始的方法就行了。
|
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
|
/**
* Copy the property values of the given source bean into the given target bean.
* <p>Note: The source and target classes do not have to match or even be derived
* from each other, as long as the properties match. Any bean properties that the
* source bean exposes but the target bean does not will silently be ignored.
* @param source the source bean:也就是说要从这个对象里面复制值出去
* @param target the target bean:出去就是复制到这里面来
* @param editable the class (or interface) to restrict property setting to:这个类对象是target的父类或其实现的接口,用于控制属性复制的范围
* @param ignoreProperties array of property names to ignore:需要忽略的字段
* @throws BeansException if the copying failed
* @see BeanWrapper
*/
private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties)
throws BeansException {
//这里在校验要复制的对象是不可以为null的,这两个方法可是会报错的!!
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
//这里和下面的代码就有意思了
Class<?> actualEditable = target.getClass();//获取目标对象的动态类型
//下面判断的意图在于控制属性复制的范围
if (editable != null) {
//必须是target对象的父类或者其实现的接口类型,相当于instanceof运算符
if (!editable.isInstance(target)) {
throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
"] not assignable to Editable class [" + editable.getName() + "]");
}
actualEditable = editable;
}
//不得不说,下面这段代码乖巧的像绵羊,待我们来分析分析它是如何如何乖巧的
PropertyDescriptor[] targetPds = getPropertyDescriptors(actualEditable);//获取属性描述,描述是什么?描述就是对属性的方法信息的封装,好乖。
List<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);
//重头戏开始了!开始进行复制了
for (PropertyDescriptor targetPd : targetPds) {
//先判断有没有写方法,没有写方法我也就没有必要读属性出来了,这个懒偷的真好!
Method writeMethod = targetPd.getWriteMethod();
//首先,没有写方法的字段我不写,乖巧撒?就是说你不让我改我就不改,让我忽略我就忽略!
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
//如果没办法从原对象里面读出属性也没有必要继续了
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
//这里就更乖巧了!写方法不让我写我也不写!!!
if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
//这里就算了,来都来了,就乖乖地进行值复制吧,别搞东搞西的了
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
catch (Throwable ex) {
throw new FatalBeanException(
"Could not copy property '" + targetPd.getName() + "' from source to target", ex);
}
}
}
}
}
}
|
2、对复制工具的一些看法和总结
总结上一段代码的分析,我们发现spring自带的工具有以下特点:
它名副其实的是在复制属性,而不是字段!!
它可以通过一个目标对象的父类或者其实现的接口来控制需要复制属性的范围
很贴心的可以忽略原对象的某些字段,可以通过2的方法忽略某些目标对象的字段
但是,这远远不够!!!我需要如下的功能:
复制对象的字段,而不是属性,也就是说我需要一个更暴力的复制工具。
我需要忽略原对象的某些字段,同时也能够忽略目标对象的某些字段。
我的项目还需要忽略原对象为null的字段和目标对象不为null的字段
带着这三个需求,开始我的工具制造。
三、自己动手丰衣足食
1、我需要解析字节码
为了避免对字节码的重复解析,使用缓存来保留解析过的字节码解析结果,同时为了不让这个工具太占用内存,使用软引用来进行缓存,上代码:
|
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
|
/*
******************************************************
* 基础的用于支持反射解析的解析结果缓存,使用软引用实现
******************************************************
*/
private static final Map<Class<?>,SoftReference<Map<String,Field>>> resolvedClassCache = new ConcurrentHashMap<>();
/**
* 同步解析字节码对象,将解析的结果放入到缓存 1、解析后的字段对象全部 accessAble
* 1、返回的集合不支持修改,要修改请记得自己重新建一个复制的副本
* @param sourceClass:需要解析的字节码对象
*/
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
public static Map<String,Field> resolveClassFieldMap(final Class<?> sourceClass){
SoftReference<Map<String,Field>> softReference = resolvedClassCache.get(sourceClass);
//判断是否已经被初始化
if(softReference == null || softReference.get() == null){
//对同一个字节码对象的解析是同步的,但是不同字节码对象的解析是并发的,因为字节码对象只有一个
synchronized(sourceClass){
softReference = resolvedClassCache.get(sourceClass);
if(softReference == null || softReference.get() == null){
//采用:<字段名称,字段对象> 来记录解析结果
Map<String,Field> fieldMap = new HashMap<>();
/*
Returns an array of Field objects reflecting all the fields declared by the class or interface represented by this
Class object. This includes public, protected, default access, and private fields, but excludes inherited fields
*/
Field[] declaredFields = sourceClass.getDeclaredFields();
if(declaredFields != null && declaredFields.length > 0){
for(Field field : declaredFields){
/*
Set the accessible flag for this object to the indicated boolean value.
*/
field.setAccessible(true);
//字段名称和字段对象
fieldMap.put(field.getName(),field);
}
}
//设置为不变Map,这个肯定是不能够改的啊!所以取的时候需要重新构建一个map
fieldMap = Collections.unmodifiableMap(fieldMap);
softReference = new SoftReference<>(fieldMap);
/*
更新缓存,将解析后的数据加入到缓存里面去
*/
resolvedClassCache.put(sourceClass,softReference);
return fieldMap;
}
}
}
/*
运行到这里来的时候要么早就存在,要么就是已经被其他的线程给初始化了
*/
return softReference.get();
}
|
2、我需要能够进行对象的复制,基本方法
|
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
|
/**
* 进行属性的基本复制操作
* @param source:源对象
* @param sourceFieldMap:原对象解析结果
* @param target:目标对象
* @param targetFieldMap:目标对象解析结果
*/
public static void copyObjectProperties(Object source,Map<String,Field> sourceFieldMap,Object target,Map<String,Field> targetFieldMap){
//进行属性值复制
sourceFieldMap.forEach(
(fieldName,sourceField) -> {
//查看目标对象是否存在这个字段
Field targetField = targetFieldMap.get(fieldName);
if(targetField != null){
try{
//对目标字段进行赋值操作
targetField.set(target,sourceField.get(source));
}catch(IllegalAccessException e){
e.printStackTrace();
}
}
}
);
}
|
3、夜深了,准备睡觉了
基于这两个方法,对其进行封装,实现了我需要的功能,并且在项目中运行目前还没有bug,应该可以直接用在生产环境,各位看官觉得可以可以拿来试一试哦!!
4、完整的代码(带注释:需要自取,无外部依赖,拿来即用)
|
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
|
