通过 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);
}
}
}
}