Java Bean Collection 属性级联拷贝

通过 Spring 的 BeanUtils.copyProperties() 实现 Java Bean 中集合属性的级联拷贝。

简单来说就是

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

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

之间的拷贝,也就是拷贝 Bean 中同名不同泛型的集合类属性。

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. 目标数据对象必须由默认无参构造函数
     * 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();
            org.springframework.beans.BeanUtils.copyProperties(source, target, ignoreProps);
            log.trace("BeanUtils.copy(Object, Class<T>, String...) for {}, {}, {}, result {}",
                    source, targetClass, Arrays.toString(ignoreProps), target);
            handleCollectionProp(source, 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);
            }
        }
    }
}
上一篇:仿微信朋友圈图片拖拽更换位置,拖拽删除


下一篇:Android开发 碎片Fragment的API全解与标准使用