Java Bean 集合属性级联拷贝

使用的是 Spring BeanUtils.copyProperties,CGlib 的没有测,不过估计因为性能原因也会差不多。

如果是单集合属性,就像下面这样

@Data
public class A {
  List<B> prop;
}

@Data
public class C {
  List<D> prop;
}

看到很多文章说,Spring 的 bean copy 会直接对擦除类型的 List<?> 进行赋值,也就是 C 中的 prop 实际类型成了 List<B>

但是实际 debug 进去之后会发现 Spring 在判断是否可赋值这里是有对泛型进行解析的,获取到了实际的泛型类型 List<B>List<D>,发现不一致直接返回了 false,所以这个字段根本就没有进行赋值操作,拷贝完成后也是 null。

此处 Spring 是 5.3.4 的,不知道是不是修改了这里。

这种比较好解决,封装一层 Spring 拷贝,在 Spring 拷贝完成后再重新反射拷贝一次 Collection 类型字段即可,具体操作就是 foreach A 中的集合对象 B,分别实例化一个 D,然后用 Spring 的 bean copy 拷贝把 B 拷贝成 D 然后重新设置 C 的 prop Collection 引用。

具体代码如下:

package com.seliote.fr.util;

import com.seliote.fr.exception.UtilException;
import lombok.extern.log4j.Log4j2;
import org.springframework.lang.NonNull;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.*;

import static org.springframework.beans.BeanUtils.getPropertyDescriptors;

/**
 * 反射相关工具类
 *
 * @author seliote
 */
@Log4j2
public class ReflectUtils {

    /**
     * 获取指定 Class 对象的泛型类型的全限定类名
     *
     * @param clazz Class 对象
     * @param <T>   Class 对象的泛型类型
     * @return Class 对象的泛型类型的全限定类名
     */
    @NonNull
    public static <T> String getClassName(@NonNull Class<T> clazz) {
        var name = clazz.getCanonicalName();
        log.trace("ReflectUtils.getClassName(Class<T>) for: {}, result: {}", clazz, name);
        return name;
    }

    /**
     * 获取指定对象的全限定类名
     *
     * @param object 对象
     * @param <T>    对象的泛型类型
     * @return 对象的全限定类名
     */
    @NonNull
    public static <T> String getClassName(@NonNull T object) {
        var name = ReflectUtils.getClassName(object.getClass());
        log.trace("ReflectUtils.getClassName(T) for: {}, result: {}", object, name);
        return name;
    }

    /**
     * Bean 属性拷贝,在宿对象引用的基础上进行拷贝
     * 1. 源宿属性不要求完全一致,会被忽略或置默认
     * 2. 源中的 Collection 属性会同时被拷贝(按照名称、类型自动转换,互引用会导致无限递归),ignoreProps 参数同时作用在其上
     *
     * @param source      数据源对象
     * @param target      数据宿对象
     * @param ignoreProps 数据源中需要忽略的属性
     */
    public static void copy(@NonNull Object source, @NonNull Object target, String... ignoreProps) {
        try {
            org.springframework.beans.BeanUtils.copyProperties(source, target, ignoreProps);
            handleCollectionProp(source, target);
            log.trace("BeanUtils.copy(Object, Object, String...) for {}, {}, {}",
                    source, target, Arrays.toString(ignoreProps));
        } catch (IllegalAccessException | InvocationTargetException exception) {
            log.warn("Bean copy occur {}, message {}", getClassName(exception), exception.getMessage());
            throw new UtilException(exception);
        }
    }

    /**
     * Bean 属性拷贝,返回一个新的宿类型的对象
     * 1. 目标数据对象必须为顶层类
     * 2. 目标数据对象必须由默认无参构造函数
     * 3. 源宿属性不要求完全一致,会被忽略或置默认
     * 4. 源中的 Collection 属性会同时被拷贝(按照名称、类型自动转换,互引用会导致无限递归),ignoreProps 参数同时作用在其上
     *
     * @param source      数据源对象
     * @param targetClass 目标数据 Class 类型
     * @param ignoreProps 数据源中需要忽略的属性
     * @param <T>         目标数据 Class 类型泛型
     * @return 目标数据对象
     */
    @NonNull
    public static <T> T copy(@NonNull Object source, @NonNull Class<T> targetClass, String... ignoreProps) {
        try {
            T target = targetClass.getDeclaredConstructor().newInstance();
            copy(source, target, ignoreProps);
            log.trace("BeanUtils.copy(Object, Class<T>, String...) for {}, {}, {}, result {}",
                    source, targetClass, Arrays.toString(ignoreProps), target);
            return target;
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException
                | InvocationTargetException exception) {
            log.warn("Bean copy occur {}, message {}", getClassName(exception), exception.getMessage());
            throw new UtilException(exception);
        }
    }

    /**
     * 处理 Collection 属性
     * 要求元素中名称完全一致,且均为 Collection
     *
     * @param source      数据源对象
     * @param target      数据宿对象
     * @param ignoreProps 数据源中需要忽略的属性
     * @param <S>         源数据泛型类
     * @param <T>         宿数据泛型类
     * @throws InvocationTargetException 被调用对象存在异常时抛出
     * @throws IllegalAccessException    被调用对象存在异常时抛出
     */
    private static <S, T> void handleCollectionProp(@NonNull S source, @NonNull T target, String... ignoreProps)
            throws InvocationTargetException, IllegalAccessException {
        Class<?> sourceClass = source.getClass();
        Class<?> targetClass = target.getClass();
        var sourcePds = getPropertyDescriptors(sourceClass);
        var targetPds = getPropertyDescriptors(targetClass);
        // 注意这里的 ignoreList 同时作用在内部的 Collection 上
        var ignoreList = ignoreProps != null ? Arrays.asList(ignoreProps) : Collections.emptyList();
        for (var sourcePd : sourcePds) {
            // 获取源 Getter 判断是否有不在 ignoreProps 中的 Collection 类型并为非 null 值
            if (Collection.class.isAssignableFrom(sourcePd.getPropertyType())
                    && !ignoreList.contains(sourcePd.getName())
                    && sourcePd.getReadMethod().invoke(source) != null) {
                for (var targetPd : targetPds) {
                    // 源有 Collection,判断宿属性以及类型,以及宿该字段是否还未赋值值
                    if (targetPd.getName().equals(sourcePd.getName())
                            && Collection.class.isAssignableFrom(targetPd.getPropertyType())
                            && targetPd.getReadMethod().invoke(target) == null) {
                        copyCollectionProp(source, target, sourcePd, targetPd);
                    }
                }
            }
        }
    }

    /**
     * 拷贝 Collection 属性
     *
     * @param source   数据源对象
     * @param target   数据宿对象
     * @param sourcePd 数据源 PropertyDescriptor
     * @param targetPd 数据宿 PropertyDescriptor
     * @param <S>      源数据泛型类
     * @param <T>      宿数据泛型类
     * @throws InvocationTargetException 被调用对象存在异常时抛出
     * @throws IllegalAccessException    被调用对象存在异常时抛出
     */
    private static <S, T> void copyCollectionProp(@NonNull S source, @NonNull T target,
                                                  @NonNull PropertyDescriptor sourcePd,
                                                  @NonNull PropertyDescriptor targetPd)
            throws InvocationTargetException, IllegalAccessException {
        // 只能 <Object> 了,但是不影响
        Collection<Object> targetCollection;
        Class<?> targetCollectionClass = targetPd.getPropertyType();
        if (targetCollectionClass.isAssignableFrom(List.class)) {
            targetCollection = new ArrayList<>();
        } else if (targetCollectionClass.isAssignableFrom(Set.class)) {
            targetCollection = new HashSet<>();
        } else {
            log.error("Unknown Collection type when copy: {}, property: {}",
                    getClassName(source), sourcePd.getName());
            throw new UtilException("Unknown Collection type");
        }
        // 设置引用
        targetPd.getWriteMethod().invoke(target, targetCollection);
        Collection<?> sourceCollection = (Collection<?>) sourcePd.getReadMethod().invoke(source);
        for (var sourceCollectionElement : sourceCollection) {
            try {
                // targetPd 这里的实际类型是 GenericTypeAwarePropertyDescriptor
                // getBeanClass() 方法可以直接得到泛型,但是该类是包可见性,SecurityManager 限制跨不过去
                var targetCollectionGenericClass = (Class<?>)
                        (((ParameterizedType) (target.getClass().getDeclaredField(targetPd.getName())
                                .getGenericType())).getActualTypeArguments()[0]);
                // 拷贝出新对象
                var targetCollectionElement = copy(sourceCollectionElement, targetCollectionGenericClass);
                targetCollection.add(targetCollectionElement);
            } catch (NoSuchFieldException exception) {
                log.error("Error when copy bean Collection<?> property, source type: {}, " +
                                "target type: {}, field: {}, exception: {}",
                        getClassName(source), getClassName(target), sourcePd.getName(), exception.getMessage());
                throw new UtilException(exception);
            }
        }
    }
}

可以看到 C 中 prop 属性已经是 List<D> 且数据也没问题。

这样就完了吗???

很显然没有,我们忽略了一种情况:

@Data
public class E<T> {
  List<T> prop;
}

假设要对两个 E<B> 进行拷贝,这种也是没有问题的,但是这和我们上面的代码没有任何关系,是 Spring 自身完成了赋值,我们代码中有一行 targetPd.getReadMethod().invoke(target) 判断这个 Collection 字段是否已经有值,如果有我们就不去覆盖了。而 Spring 在这里获取到的实际类型则是 List<?>,所以执行了赋值操作。

接下来就是一个迷惑行为,要对 E<B>E<D> 进行属性拷贝,不难猜出,由于对泛型参数解析为 List<?> 导致编译期检查失效,E<D> 的 prop 属性将会变为 List<B>

这可是一个大坑啊,编译期检查失效了,如果在 D 里有方法要调,JVM 反手就是一个 ClassCastException,这种 bug 都还比较好找,再假设 D 上注解了 @JsonProperty 而 B 上没有,后面把 D 序列化了,发现 @JsonProperty 无效了,可能找了一天也没发现问题在哪。

而我们上面封装的代码由于 targetPd.getReadMethod().invoke(target) == null 检查导致也没有做任何操作,如果想要避免这种情况转而让拷贝实际类型,我们把宿数据源上的判空去掉,让对所有 Collection 类型都再按实际类型拷贝一次。

会发现还是不行,还是 JVM 熟悉的 ClassCastException,但是不是对 D 类型,而是上面代码中想要获取宿 Collection 的实际类型,这里的类型成了 T,也就是泛型参数。

上面的思路不行了,获取到的 Collection 泛型类型成了 T,目标类型实例化不了了,不仅实例化不了,而且获取都获取不到,JVM 的泛型擦除有时候就是这么扯淡。

但是办法还是有的,类似 Jackson 的 TypeReference,我们也需要做类型捕获。我们可以通过修改工具类方法签名来实现,就像 Jackson、FastJSON 提供的那样。

还有一个办法,也是类型捕获,但是提前到创建对象的时候:如果你用了我的代码,当我代码走到这里而我得到的不是一个 Class 而是一个泛型变量,你的宿类里一定有一个方法能够让我得到这个泛型变量对应的实际类型,我们规定这个方法为 public Class<?> genericRealType(String),当然啊,如果你没有提供,那我就只好抛异常出去了。

先改成这个模式试一下。提供一个类型捕获基类,所有拷贝的宿对象如果含有 Collection 属性且 Collection 中使用了泛型变量,那么就都需要继承此类,当然也可以直接提供 public Class<?> genericRealType(String) 而不继承此类。

package com.seliote.fr.util;

import java.util.*;

/**
 * Bean 拷贝类型捕获
 * 宿对象中含有 List<T> 的对象均需继承此类
 *
 * @author seliote
 */
@SuppressWarnings("unused")
public abstract class BeanCopyTypeReference {

    // 只有一个
    private static final String DEFAULT_NAME = "only_one";

    // 用于 Bean 拷贝时类型捕获
    private final Map<String, Class<?>> genericTypes;

    public BeanCopyTypeReference() {
        genericTypes = null;
    }

    public BeanCopyTypeReference(Class<?> genericType) {
        this.genericTypes = new HashMap<>() {{
            put(DEFAULT_NAME, genericType);
        }};
    }

    public BeanCopyTypeReference(Map<String, Class<?>> genericTypes) {
        this.genericTypes = genericTypes;
    }

    /**
     * 获取实际的捕获类型
     *
     * @param generic 需要获取的泛型变量名称
     * @return 捕获类型
     */
    public Class<?> genericRealType(String generic) {
        if (genericTypes.size() == 1) {
            return genericTypes.values().stream().findAny().get();
        } else {
            return genericTypes.get(generic);
        }
    }
}

下面这个是没有使用继承测试的例子,于是 E 就需要变成

@Data
public class E<T> {

    @Getter(AccessLevel.NONE)
    @Setter(AccessLevel.NONE)
    private Class<?> genericType;

    public E() {
    }

    public E(Class<?> genericRealType) {
        this.genericType = genericRealType;
    }

    @SuppressWarnings("unused")
    public Class<?> genericRealType(String generic) {
        return genericType;
    }

    List<T> prop;
}

而原处理逻辑也需要加上泛型变量的操作

package com.seliote.fr.util;

import com.seliote.fr.exception.UtilException;
import lombok.extern.log4j.Log4j2;
import org.springframework.lang.NonNull;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.*;

import static org.springframework.beans.BeanUtils.getPropertyDescriptors;

/**
 * 反射相关工具类
 *
 * @author seliote
 */
@Log4j2
public class ReflectUtils {

    /**
     * 获取指定 Class 对象的泛型类型的全限定类名
     *
     * @param clazz Class 对象
     * @param <T>   Class 对象的泛型类型
     * @return Class 对象的泛型类型的全限定类名
     */
    @NonNull
    public static <T> String getClassName(@NonNull Class<T> clazz) {
        var name = clazz.getCanonicalName();
        log.trace("ReflectUtils.getClassName(Class<T>) for: {}, result: {}", clazz, name);
        return name;
    }

    /**
     * 获取指定对象的全限定类名
     *
     * @param object 对象
     * @param <T>    对象的泛型类型
     * @return 对象的全限定类名
     */
    @NonNull
    public static <T> String getClassName(@NonNull T object) {
        var name = ReflectUtils.getClassName(object.getClass());
        log.trace("ReflectUtils.getClassName(T) for: {}, result: {}", object, name);
        return name;
    }

    /**
     * 获取方法的全限定名称
     *
     * @param method Method 对象
     * @return 方法的全限定名称
     */
    @NonNull
    public static String getMethodName(@NonNull Method method) {
        var name = ReflectUtils.getClassName(method.getDeclaringClass()) + "." + method.getName();
        log.trace("ReflectUtils.getMethodName(Method) for: {}, result: {}", method, name);
        return name;
    }

    /**
     * Bean 属性拷贝,在宿对象引用的基础上进行拷贝
     * 1. 源宿属性不要求完全一致,会被忽略或置默认
     * 2. 源中的 Collection 属性会同时被拷贝(按照名称、类型自动转换,互引用会导致无限递归),ignoreProps 参数同时作用在其上
     * 3. 源宿对象 Collection 属性泛型类型不同时需要宿对象含有 public Class<?> genericRealType(String) 方法且返回相应的类型
     *
     * @param source      数据源对象
     * @param target      数据宿对象
     * @param ignoreProps 数据源中需要忽略的属性
     */
    public static void copy(@NonNull Object source, @NonNull Object target, String... ignoreProps) {
        try {
            org.springframework.beans.BeanUtils.copyProperties(source, target, ignoreProps);
            handleCollectionProp(source, target);
            log.trace("BeanUtils.copy(Object, Object, String...) for {}, {}, {}",
                    source, target, Arrays.toString(ignoreProps));
        } catch (IllegalAccessException | InvocationTargetException exception) {
            log.warn("Bean copy occur {}, message {}", getClassName(exception), exception.getMessage());
            throw new UtilException(exception);
        }
    }

    /**
     * Bean 属性拷贝,返回一个新的宿类型的对象
     * 1. 目标数据对象必须为顶层类
     * 2. 目标数据对象必须由默认无参构造函数
     * 3. 源宿属性不要求完全一致,会被忽略或置默认
     * 4. 源中的 Collection 属性会同时被拷贝(按照名称、类型自动转换,互引用会导致无限递归),ignoreProps 参数同时作用在其上
     *
     * @param source      数据源对象
     * @param targetClass 目标数据 Class 类型
     * @param ignoreProps 数据源中需要忽略的属性
     * @param <T>         目标数据 Class 类型泛型
     * @return 目标数据对象
     */
    @NonNull
    public static <T> T copy(@NonNull Object source, @NonNull Class<T> targetClass, String... ignoreProps) {
        try {
            T target = targetClass.getDeclaredConstructor().newInstance();
            copy(source, target, ignoreProps);
            log.trace("BeanUtils.copy(Object, Class<T>, String...) for {}, {}, {}, result {}",
                    source, targetClass, Arrays.toString(ignoreProps), target);
            return target;
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException
                | InvocationTargetException exception) {
            log.warn("Bean copy occur {}, message {}", getClassName(exception), exception.getMessage());
            throw new UtilException(exception);
        }
    }

    /**
     * 处理 Collection 属性
     * 要求元素中名称完全一致,且均为 Collection
     *
     * @param source      数据源对象
     * @param target      数据宿对象
     * @param ignoreProps 数据源中需要忽略的属性
     * @param <S>         源数据泛型类
     * @param <T>         宿数据泛型类
     * @throws InvocationTargetException 被调用对象存在异常时抛出
     * @throws IllegalAccessException    被调用对象存在异常时抛出
     */
    private static <S, T> void handleCollectionProp(@NonNull S source, @NonNull T target, String... ignoreProps)
            throws InvocationTargetException, IllegalAccessException {
        Class<?> sourceClass = source.getClass();
        Class<?> targetClass = target.getClass();
        var sourcePds = getPropertyDescriptors(sourceClass);
        var targetPds = getPropertyDescriptors(targetClass);
        // 注意这里的 ignoreList 同时作用在内部的 Collection 上
        var ignoreList = ignoreProps != null ? Arrays.asList(ignoreProps) : Collections.emptyList();
        for (var sourcePd : sourcePds) {
            // 获取源 Getter 判断是否有不在 ignoreProps 中的 Collection 类型并为非 null 值
            if (Collection.class.isAssignableFrom(sourcePd.getPropertyType())
                    && !ignoreList.contains(sourcePd.getName())
                    && sourcePd.getReadMethod().invoke(source) != null) {
                for (var targetPd : targetPds) {
                    // 源有 Collection,判断宿属性以及类型,不再判断宿该字段是否还未赋值
                    if (targetPd.getName().equals(sourcePd.getName())
                            && Collection.class.isAssignableFrom(targetPd.getPropertyType())) {
                        copyCollectionProp(source, target, sourcePd, targetPd);
                    }
                }
            }
        }
    }

    /**
     * 拷贝 Collection 属性
     *
     * @param source   数据源对象
     * @param target   数据宿对象
     * @param sourcePd 数据源 PropertyDescriptor
     * @param targetPd 数据宿 PropertyDescriptor
     * @param <S>      源数据泛型类
     * @param <T>      宿数据泛型类
     * @throws InvocationTargetException 被调用对象存在异常时抛出
     * @throws IllegalAccessException    被调用对象存在异常时抛出
     */
    private static <S, T> void copyCollectionProp(@NonNull S source, @NonNull T target,
                                                  @NonNull PropertyDescriptor sourcePd,
                                                  @NonNull PropertyDescriptor targetPd)
            throws InvocationTargetException, IllegalAccessException {
        // 只能 <Object> 了,但是不影响
        Collection<Object> targetCollection;
        Class<?> targetCollectionClass = targetPd.getPropertyType();
        if (targetCollectionClass.isAssignableFrom(List.class)) {
            targetCollection = new ArrayList<>();
        } else if (targetCollectionClass.isAssignableFrom(Set.class)) {
            targetCollection = new HashSet<>();
        } else {
            log.error("Unknown Collection type when copy: {}, property: {}",
                    getClassName(source), sourcePd.getName());
            throw new UtilException("Unknown Collection type");
        }
        // 设置引用
        targetPd.getWriteMethod().invoke(target, targetCollection);
        Collection<?> sourceCollection = (Collection<?>) sourcePd.getReadMethod().invoke(source);
        for (var sourceCollectionElement : sourceCollection) {
            try {
                copyCollectionElement(target, targetPd, sourceCollectionElement, targetCollection);
            } catch (NoSuchFieldException | NoSuchMethodException exception) {
                log.error("Error when copy bean Collection<?> property, source type: {}, " +
                                "target type: {}, field: {}, exception: {}",
                        getClassName(source), getClassName(target), sourcePd.getName(), exception.getMessage());
                throw new UtilException(exception);
            }
        }
    }

    /**
     * 拷贝 Collection 属性中的元素
     *
     * @param target                  数据宿对象
     * @param targetPd                数据宿 PropertyDescriptor
     * @param sourceCollectionElement 数据源对象中要拷贝的元素
     * @param targetCollection        数据宿对象中 Collection 的新引用
     * @param <T>                     宿数据泛型类
     * @throws NoSuchFieldException      数据宿对象无该域
     * @throws NoSuchMethodException     数据宿对象无 genericRealType 方法
     * @throws InvocationTargetException 数据宿对象方法调用失败
     * @throws IllegalAccessException    数据宿对象禁止访问
     */
    private static <T> void copyCollectionElement(@NonNull T target,
                                                  @NonNull PropertyDescriptor targetPd,
                                                  @NonNull Object sourceCollectionElement,
                                                  @NonNull Collection<Object> targetCollection)
            throws NoSuchFieldException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        // targetPd 这里的实际类型是 GenericTypeAwarePropertyDescriptor
        // getBeanClass() 方法可以直接得到泛型,但是该类是包可见性,SecurityManager 限制跨不过去
        Class<?> targetCollectionGenericClass;
        var parameterizedType = (((ParameterizedType)
                (target.getClass().getDeclaredField(targetPd.getName()).getGenericType())
        ).getActualTypeArguments()[0]);
        if (parameterizedType instanceof Class<?>) {
            targetCollectionGenericClass = (Class<?>) parameterizedType;
        } else {
            var genericRealTypeMethod = target.getClass()
                    .getMethod("genericRealType", String.class);
            var realType = genericRealTypeMethod.invoke(target, parameterizedType.getTypeName());
            if (!(realType instanceof Class<?>)) {
                throw new UtilException("Method genericRealType return value error:" + getClassName(target));
            }
            targetCollectionGenericClass = (Class<?>) realType;
        }
        // 拷贝出新对象
        var targetCollectionElement = copy(sourceCollectionElement, targetCollectionGenericClass);
        targetCollection.add(targetCollectionElement);
    }
}

愉快的测试一把

还没完,我们还忽略了一种情况,List<E<D>> 如果属性本身就是一个集合...方法中直接获取到的就是 List 对象的属性,也就是 getClass getSize 这种,并不是我们期望的。

当然这种类也不是我们期望的,上面的继承方法对于直接使用 List <?> 是行不通的,当然也可以选择创建子类继承 List...这样会不会太有想法了?

行吧,还是得用 TypeReference。

先写个 TypeReference 吧,原理就是 Class<?>getGenericSuperclass() 方法能够获取到父类的泛型类型,很多框架都有提供这个功能,比如 MyBatis 里也有,可以参考一下。

package com.seliote.fr.util;

import com.seliote.fr.exception.UtilException;

import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;

/**
 * 泛型类型捕获,可用于 Bean 拷贝中含有泛型参数类型
 *
 * @author seliote
 */
@SuppressWarnings("unused")
public abstract class TypeReference<T> {

    private final Type type;

    /**
     * Protected 构造器,确保子类可构造
     */
    protected TypeReference() {
        Type superClass = getClass().getGenericSuperclass();
        if (superClass instanceof Class<?>) {
            throw new UtilException("Illegal TypeReference");
        }
        type = ((ParameterizedType) superClass).getActualTypeArguments()[0];
    }

    /**
     * 获取捕获的类型
     *
     * @return 捕获的类型
     */
    public Type getType() {
        return type;
    }
}

下来就是拷贝 List 了,原理也比较简单了,方法签名是 `> T copy(@NonNull Collection source, @NonNull TypeReference typeReference),内部获取到 typeReference 的实际类型,创建对应的 Collection 对象,然后循环 source的元素用之前改造过的copy` 方法拷贝即可。

下面就是最终的代码了:

package com.seliote.fr.util;

import com.seliote.fr.exception.UtilException;
import lombok.extern.log4j.Log4j2;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;

import java.beans.PropertyDescriptor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.util.*;

import static org.springframework.beans.BeanUtils.getPropertyDescriptors;

/**
 * 反射相关工具类
 *
 * @author seliote
 */
@Log4j2
public class ReflectUtils {

    /**
     * 获取指定 Class 对象的泛型类型的全限定类名
     *
     * @param clazz Class 对象
     * @param <T>   Class 对象的泛型类型
     * @return Class 对象的泛型类型的全限定类名
     */
    @NonNull
    public static <T> String getClassName(@NonNull Class<T> clazz) {
        var name = clazz.getCanonicalName();
        log.trace("ReflectUtils.getClassName(Class<T>) for: {}, result: {}", clazz, name);
        return name;
    }

    /**
     * 获取指定对象的全限定类名
     *
     * @param object 对象
     * @param <T>    对象的泛型类型
     * @return 对象的全限定类名
     */
    @NonNull
    public static <T> String getClassName(@NonNull T object) {
        var name = ReflectUtils.getClassName(object.getClass());
        log.trace("ReflectUtils.getClassName(T) for: {}, result: {}", object, name);
        return name;
    }

    /**
     * 获取方法的全限定名称
     *
     * @param method Method 对象
     * @return 方法的全限定名称
     */
    @NonNull
    public static String getMethodName(@NonNull Method method) {
        var name = ReflectUtils.getClassName(method.getDeclaringClass()) + "." + method.getName();
        log.trace("ReflectUtils.getMethodName(Method) for: {}, result: {}", method, name);
        return name;
    }

    /**
     * 含有泛型参数属性的 Bean 拷贝
     * 1. 源宿属性不要求完全一致,会被忽略或置默认
     * 2. 源中的 Collection 属性会同时被拷贝(按照名称、类型自动转换,互引用会导致无限递归)
     * 3. 宿对象 Collection 属性含有泛型参数时必须提供 TypeReference 引用其泛型参数类型,否则会导致异常
     *
     * @param source        数据源对象
     * @param target        数据宿对象
     * @param typeReference 数据宿对象中泛型参数类型捕获,当不存在泛型参数时可为空,存在泛型参数但未提供时抛出异常
     */
    public static void copy(@NonNull Object source, @NonNull Object target, @Nullable TypeReference<?> typeReference) {
        try {
            org.springframework.beans.BeanUtils.copyProperties(source, target);
            handleCollectionProp(source, target, typeReference);
            log.trace("ReflectUtils.copy(Object, Object, TypeReference<?>) for {}, {}, {}",
                    source, target, typeReference);
        } catch (IllegalAccessException | InvocationTargetException exception) {
            log.warn("Bean copy occur {}, message {}", getClassName(exception), exception.getMessage());
            throw new UtilException(exception);
        }
    }

    /**
     * Bean 属性拷贝
     * 1. 源宿属性不要求完全一致,会被忽略或置默认
     * 2. 源中的 Collection 属性会同时被拷贝(按照名称、类型自动转换,互引用会导致无限递归)
     * 3. 源中数据不得含有泛型参数
     *
     * @param source 数据源对象
     * @param target 数据宿对象
     */
    public static void copy(@NonNull Object source, @NonNull Object target) {
        copy(source, target, null);
        log.trace("ReflectUtils.copy(Object, Object, String...) for {}, {}",
                source, target);
    }

    /**
     * Bean 属性拷贝,返回一个新的宿类型的对象
     * 1. 目标数据对象必须为顶层类
     * 2. 目标数据对象必须由默认无参构造函数
     * 3. 源宿属性不要求完全一致,会被忽略或置默认
     * 4. 源中的 Collection 属性会同时被拷贝(按照名称、类型自动转换,互引用会导致无限递归)
     * 5. 源中数据不得含有泛型参数
     *
     * @param source      数据源对象
     * @param targetClass 目标数据 Class 类型
     * @param <T>         目标数据 Class 类型泛型
     * @return 目标数据对象
     */
    @NonNull
    public static <T> T copy(@NonNull Object source, @NonNull Class<T> targetClass) {
        try {
            T target = targetClass.getDeclaredConstructor().newInstance();
            copy(source, target);
            log.trace("ReflectUtils.copy(Object, Class<T>, String...) for {}, {}, result {}",
                    source, targetClass, target);
            return target;
        } catch (NoSuchMethodException | InstantiationException | IllegalAccessException
                | InvocationTargetException exception) {
            log.warn("Bean copy occur {}, message {}", getClassName(exception), exception.getMessage());
            throw new UtilException(exception);
        }
    }

    /**
     * Collection 类型拷贝,返回一个新 Collection
     *
     * @param source        数据源 Collection
     * @param typeReference 数据宿对象 TypeReference
     * @param <T>           数据宿对象实际泛型类型
     * @return 数据宿对象 Collection
     */
    public static <T extends Collection<?>> T copy(@NonNull Collection<?> source,
                                                   @NonNull TypeReference<T> typeReference) {
        // Class<?> List<BalaBala>
        var referenceType = (ParameterizedType)
                (((ParameterizedType) typeReference.getClass().getGenericSuperclass()).getActualTypeArguments()[0]);
        // Class<?> BalaBala
        var targetElementType = (Class<?>) (referenceType.getActualTypeArguments()[0]);
        Collection<Object> target;
        if (List.class.isAssignableFrom(source.getClass())) {
            target = new ArrayList<>();
        } else {
            target = new HashSet<>();
        }
        for (var sourceElement : source) {
            target.add(copy(sourceElement, targetElementType));
        }
        // 没办法了,为了其他地方直接能用,只能强转了
        // 但是这里是一定不会报 ClassCastException,创建了 T 的引用类型,只是原始类型用的 Object
        //noinspection unchecked
        return (T) target;
    }

    /**
     * 处理 Collection 属性
     * 要求元素中名称完全一致,且均为 Collection
     *
     * @param source        数据源对象
     * @param target        数据宿对象
     * @param typeReference 数据宿对象中泛型参数类型捕获,当不存在泛型参数时可为空,存在泛型参数但未提供时抛出异常
     * @param <S>           源数据泛型类
     * @param <T>           宿数据泛型类
     * @throws InvocationTargetException 被调用对象存在异常时抛出
     * @throws IllegalAccessException    被调用对象存在异常时抛出
     */
    private static <S, T> void handleCollectionProp(@NonNull S source, @NonNull T target,
                                                    @Nullable TypeReference<?> typeReference)
            throws InvocationTargetException, IllegalAccessException {
        Class<?> sourceClass = source.getClass();
        Class<?> targetClass = target.getClass();
        var sourcePds = getPropertyDescriptors(sourceClass);
        var targetPds = getPropertyDescriptors(targetClass);
        for (var sourcePd : sourcePds) {
            // 获取源 Getter 判断是否有不在 ignoreProps 中的 Collection 类型并为非 null 值
            if (Collection.class.isAssignableFrom(sourcePd.getPropertyType())
                    && sourcePd.getReadMethod().invoke(source) != null) {
                for (var targetPd : targetPds) {
                    // 源有 Collection,判断宿属性以及类型,不再判断宿该字段是否还未赋值
                    if (targetPd.getName().equals(sourcePd.getName())
                            && Collection.class.isAssignableFrom(targetPd.getPropertyType())) {
                        copyCollectionProp(source, target, sourcePd, targetPd, typeReference);
                    }
                }
            }
        }
    }

    /**
     * 拷贝 Collection 属性
     *
     * @param source        数据源对象
     * @param target        数据宿对象
     * @param sourcePd      数据源 PropertyDescriptor
     * @param targetPd      数据宿 PropertyDescriptor
     * @param typeReference 数据宿对象中泛型参数类型捕获,当不存在泛型参数时可为空,存在泛型参数但未提供时抛出异常
     * @param <S>           源数据泛型类
     * @param <T>           宿数据泛型类
     * @throws InvocationTargetException 被调用对象存在异常时抛出
     * @throws IllegalAccessException    被调用对象存在异常时抛出
     */
    private static <S, T> void copyCollectionProp(@NonNull S source, @NonNull T target,
                                                  @NonNull PropertyDescriptor sourcePd,
                                                  @NonNull PropertyDescriptor targetPd,
                                                  @Nullable TypeReference<?> typeReference)
            throws InvocationTargetException, IllegalAccessException {
        // 只能 <Object> 了,但是不影响
        Collection<Object> targetCollection;
        Class<?> targetCollectionClass = targetPd.getPropertyType();
        if (targetCollectionClass.isAssignableFrom(List.class)) {
            targetCollection = new ArrayList<>();
        } else if (targetCollectionClass.isAssignableFrom(Set.class)) {
            targetCollection = new HashSet<>();
        } else {
            log.error("Unknown Collection type when copy: {}, property: {}",
                    getClassName(source), sourcePd.getName());
            throw new UtilException("Unknown Collection type");
        }
        // 设置引用
        targetPd.getWriteMethod().invoke(target, targetCollection);
        Collection<?> sourceCollection = (Collection<?>) sourcePd.getReadMethod().invoke(source);
        for (var sourceCollectionElement : sourceCollection) {
            try {
                copyCollectionElement(target, targetPd, sourceCollectionElement, targetCollection, typeReference);
            } catch (NoSuchFieldException | NoSuchMethodException exception) {
                log.error("Error when copy bean Collection<?> property, source type: {}, " +
                                "target type: {}, field: {}, exception: {}",
                        getClassName(source), getClassName(target), sourcePd.getName(), exception.getMessage());
                throw new UtilException(exception);
            }
        }
    }

    /**
     * 拷贝 Collection 属性中的元素
     *
     * @param target                  数据宿对象
     * @param targetPd                数据宿 PropertyDescriptor
     * @param sourceCollectionElement 数据源对象中要拷贝的元素
     * @param targetCollection        数据宿对象中 Collection 的新引用
     * @param typeReference           数据宿对象中泛型参数类型捕获,当不存在泛型参数时可为空,存在泛型参数但未提供时抛出异常
     * @param <T>                     宿数据泛型类
     * @throws NoSuchFieldException  数据宿对象无该域
     * @throws NoSuchMethodException 数据宿对象无 genericRealType 方法
     */
    private static <T> void copyCollectionElement(@NonNull T target,
                                                  @NonNull PropertyDescriptor targetPd,
                                                  @NonNull Object sourceCollectionElement,
                                                  @NonNull Collection<Object> targetCollection,
                                                  @Nullable TypeReference<?> typeReference)
            throws NoSuchFieldException, NoSuchMethodException {
        // targetPd 这里的实际类型是 GenericTypeAwarePropertyDescriptor
        // getBeanClass() 方法可以直接得到泛型,但是该类是包可见性,SecurityManager 限制跨不过去
        Class<?> targetCollectionGenericClass;
        var parameterizedType = (((ParameterizedType)
                (target.getClass().getDeclaredField(targetPd.getName()).getGenericType())
        ).getActualTypeArguments()[0]);
        if (parameterizedType instanceof Class<?>) {
            targetCollectionGenericClass = (Class<?>) parameterizedType;
        } else {
            if (typeReference == null) {
                log.error("Copy generic type but no typeReference provide: {}", getClassName(target));
                throw new UtilException("Copy generic type but no typeReference provide: " + getClassName(target));
            }
            targetCollectionGenericClass = (Class<?>)
                    ((ParameterizedType) typeReference.getClass().getGenericSuperclass()).getActualTypeArguments()[0];
        }
        // 拷贝出新对象
        var targetCollectionElement = copy(sourceCollectionElement, targetCollectionGenericClass);
        targetCollection.add(targetCollectionElement);
    }
}
原文地址:https://www.cnblogs.com/seliote/p/14509260.html