前景提要

BeanUtils 相信不少读者都会使用到,特别是 Entity 和 DTO 相互转换的时候,将会大大减少手写 get 和 set 方法,减少转换过程中出错的概率,使代码看起来也更加简单明了。

首先讲讲 为什么 要自定义 BeanUtils#copyProperties 的逻辑。

BeanUtils 拷贝字段部分不成功

  1. 最近使用 BeanUtils#copyProperties 的时候遇到一个问题:如果被拷贝对象有父类且父类为泛型类的时候,父类中的字段(泛型类型)都将会被解析为 Object 类型,导致 BeanUtils 不能正确设置该泛型字段的值。

  2. 等待接下来分析完成之后,将会采用 自定义 BeanUtils#copyProperties 的逻辑 来解决这个问题。

问题复现

首先我们通过接下来的 测试,来发现具体的问题所在。

测试实体类

  • Base(这里使用泛型,主要是为了暴露问题的根源,这里的 id 字段为泛型类型<ID>),
1
2
3
4
5
6
7
8
9
10
public abstract class Base<ID> {

private ID id;

private Date createTime;

private Date updateTime;

...getter & setter
}
  • Person(正常的继承至 Base,并且设置泛型类型为 Integer)
1
2
3
4
5
6
7
8
9
10
public class Person extends Base<Integer> {

private String name;

...getter & settter

...equals & hashCode

...toString
}
  • PersonDTO(Person 的数据传输对象,包含 Person 中的部分属性,其中 id 字段的类型为 Integer)
1
2
3
4
5
6
7
8
9
10
11
12
public class PersonDTO {

private Integer id;

private String name;

...getter & settter

...equals & hashCode

...toString
}

测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class BeanUtilsTest {

@test
public void copyPropertiesTest() {
Person person = new Person();
person.setId = 2018;
person.setName = "john";

PersonDTO personDTO = new PersonDTO();
BeanUtils.copyProperties(person, personDTO);

Assert.assertEquals(person.getName(), personDTO.getName());
Assert.assertEquals(person.getId(), personDTO.getId());
}
}

测试结果

FAILED:
expect: person.getId(): 2018, but personDTO.getId(): null

按道理来说,PersonDTO 中的 id 字段和 Person 所继承的 id 字段是属于同一种类型(Integer),但是从实际结果上看,并没有拷贝成功。

解决思路

不断的打断点和调试,最后发现是由于 Person 的父类 Base 是泛型类,该泛型类中的字段 id 的类型在 BeanUtils 中解析(实际上是反射解析)为了 Object 类型,而不是实际的 Integer 类型。这就很好的解释了为什么拷贝失败。

查看 BeanUtils 的源码会发现,它会检查源(source)与目标(target)的类型是否能够匹配,如果不匹配会自动忽略该属性。关键代码如下:

  • BeanUtils

https://github.com/spring-projects/spring-framework/blob/master/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java#L677

1
2
3
4
5
6
7
8
9
...
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable, @Nullable String... ignoreProperties) throws BeansException {
...

if (readMethod != null &&
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {

...
  • ClassUtils

https://github.com/spring-projects/spring-framework/blob/master/spring-core/src/main/java/org/springframework/util/ClassUtils.java#L528

1
2
3
4
5
6
7
8
public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {
...

if (lhsType.isAssignableFrom(rhsType)) {
return true;

...
}

ClassUtils.isAsssignable() 判断了是否源(source)与目标(target)类型是否匹配。正是由于解析出来的泛型类型为 Object,所以这里将会直接返回 false,导致该字段不能正确设置源字段的值,最后的结果为 null 也是应该的。

解决方案

在不改变 Spring 的源代码的情况下,这里有两种方法解决这个问题:

以下所有代码仅仅是为了解决上述问题所重写的,仅仅提供一种自定义的思路,读者可根据自己的实际情况进行修改。

方法一: 保留源(source)和目标(target)的类型,为泛型做特殊处理

  • 继承 ClassUtils,并重写 isAssignable 方法

这里由于 isAssignable 方法需要用到 ClassUtils 里面的私有字段,但是继承之后没办法用到私有字段。

查看 ClassUtils 源码之后,实现了所需的字段,并初始化这些字段。

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
public static class ClassUtilsImpl extends ClassUtils {

private static final Map<Class<?>, Class<?>> primitiveWrapperTypeMap = new IdentityHashMap<>(8);

private static final Map<Class<?>, Class<?>> primitiveTypeToWrapperMap = new IdentityHashMap<>(8);

static {
primitiveWrapperTypeMap.put(Boolean.class, boolean.class);
primitiveWrapperTypeMap.put(Byte.class, byte.class);
primitiveWrapperTypeMap.put(Character.class, char.class);
primitiveWrapperTypeMap.put(Double.class, double.class);
primitiveWrapperTypeMap.put(Float.class, float.class);
primitiveWrapperTypeMap.put(Integer.class, int.class);
primitiveWrapperTypeMap.put(Long.class, long.class);
primitiveWrapperTypeMap.put(Short.class, short.class);

// Map entry iteration is less expensive to initialize than forEach with lambdas
for (Map.Entry<Class<?>, Class<?>> entry : primitiveWrapperTypeMap.entrySet()) {
primitiveTypeToWrapperMap.put(entry.getValue(), entry.getKey());
}
}

public static boolean isAssignable(Class<?> lhsType, Class<?> rhsType) {
Assert.notNull(lhsType, "Left-hand side type must not be null");
Assert.notNull(rhsType, "Right-hand side type must not be null");

// handle it for generice.
if (rhsType.equals(Object.class)) {
return true;
}

return ClassUtils.isAssignable(lhsType, rhsType);
}
}
  • 继承 BeanUtils,并重写 copyProperties(*)
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
public abstract class BeanUtilsImpl extends org.springframework.beans.BeanUtils {


public static void copyProperties(Object source, Object target) throws BeansException {
copyProperties(source, target, null, (String[]) null);
}

public static void copyProperties(Object source, Object target, Class<?> editable) throws BeansException {
copyProperties(source, target, editable, (String[]) null);
}

public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {
copyProperties(source, target, null, ignoreProperties);
}

private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
@Nullable String... ignoreProperties) throws BeansException {

Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");

Class<?> actualEditable = target.getClass();
if (editable != null) {
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 &&
ClassUtilsImpl.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);
}
}
}
}
}
}
}

方法二: or 抛弃源(source)和目标(target)的类型检查

不用继承 ClassUtils,直接移除 #40 行的 isAssignable() 判断。如果测试过程中出现了类型转换异常,开发人员通过错误信息也能够精确定位到问题所在,但是不推荐这样做。

总结

自定义 BeanUtils 的方法应根据项目的需求进行定制,这里展示的仅仅是解决了当前遇到的泛型问题。笔者想要传达的是自定义 BeanUtils#copyProperties 逻辑的一种思路。

笔者也不太清楚这种方式是否会造成什么不良影响,如果读者有任何建设性的批评或者建议,可以一起交流一番 johnniang@riseup.net