Spring 源码阅读:BeanUtils.copyProperties 详解

注意!是浅拷贝!

Spring MVC处理信息时经常需要从 DO 转为 DTO,BeanUtils下的copyProperties() 是经常用到的方法。

之前也写过类似的工具类,现在来看 Spring 的具体实现。

示例代码

// 描述
复制给定源 Bean 的属性值至目标 Bean 中。
源和目标的类不需要完全匹配或互相派生,只需要属性相互匹配。
任何源 Bean 暴露但目标 Bean 不暴露的属性将会被忽视。
在 Spring 5.3 中,在匹配源对象和目标对象中的属性时,此方法遵循通用类型信息。
/**
 * 定义三个类,并调用 copyProperties 查看结果
 * 这里省略 toString() 方法
 */
@Data
public class A {
    private String name;
    private String age;
    private String tel;
}

@Data
public class B {
    private String name;
    private int age;
    private Tel tel;
}

@Data
public class Tel {
    String telPhone;
}

public class test {
    public static void main(String[] args) {
        A a = new A();
        a.setName("ANAME");
        a.setAge("10");
        a.setTel("10086");
        B b = new B();
        BeanUtils.copyProperties(a, b);
        System.out.println(a);
        System.out.println(b);
    }
}

结果:


A{name='ANAME', age='10', tel='10086'}
B{name='ANAME', age=0, tel=null}

看起来 copyProperties 只会复制类型和名称相同的属性值,下面进入源码。


源码注释

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 类型。
    Class<?> actualEditable = target.getClass();
    //editable 不为空且 target 不为 editable 实例,抛出异常
    if (editable != null) {
        if (!editable.isInstance(target)) {
            throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
                    "] not assignable to Editable class [" + editable.getName() + "]");
        }
        //editable 不为空,则作为目标对象的 Class 类型
        actualEditable = editable;
    }
    /**
     * PropertyDescriptor,属性都获取对应的描述器,内部存储了读写方法的名称
     * ,读写操作方法的 MethodRef 和一些引用。
     * MethodRef 也属于方法和类型的引用包装类(软引用和弱引用)
     * 通过 getPropertyDescriptors() 方法,可以通过 Class 给出的信息去检索对应类
     * 的 PropertyDescriptor
     */
    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) {
                    //处理类型转换
                    ResolvableType sourceResolvableType = ResolvableType.forMethodReturnType(readMethod);
                    ResolvableType targetResolvableType = ResolvableType.forMethodParameter(writeMethod, 0);

                    //如果任一 ResolvableType 具有不可解析的泛型,则忽略可分配检查中的泛型类型
                    boolean isAssignable =
                            (sourceResolvableType.hasUnresolvableGenerics() || targetResolvableType.hasUnresolvableGenerics() ?
                                    ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType()) :
                                    targetResolvableType.isAssignableFrom(sourceResolvableType));

                    if (isAssignable) {
                        try {
                            //读方法不为 public,设置 Accessible 强制调用
                            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);
                        }
                    }
                }
            }
        }
    }
}

显而易见,copyProperties() 同样依赖于 getter-setter 进行取值和注入。如果缺失 get 或 set 方法就会导致该方法忽视对应的属性。