前景提要

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

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

BeanUtils 拷贝字段部分不成功

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

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

问题复现

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

测试实体类

  • Base(这里使用泛型,主要是为了暴露问题的根源,这里的 id 字段为泛型类型<ID>),
public abstract class Base<ID> {

  private ID id;

  private Date createTime;

  private Date updateTime;

  ...getter & setter
}
  • Person(正常的继承至 Base,并且设置泛型类型为 Integer)
public class Person extends Base<Integer> {

  private String name;

  ...getter & settter

  ...equals & hashCode

  ...toString
}
  • PersonDTO(Person 的数据传输对象,包含 Person 中的部分属性,其中 id 字段的类型为 Integer)
public class PersonDTO {

  private Integer id;

  private String name;

  ...getter & settter

  ...equals & hashCode

  ...toString
}

测试

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

...
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

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 源码之后,实现了所需的字段,并初始化这些字段。

 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(*)
  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 逻辑的一种思路。

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