一、如何实现不同类型对象之间的复制问题?
1、为什么会有这个问题?
近来在进行一个项目开发的时候,为了隐藏后端数据库表结构、同时也为了配合给前端一个更友好的API接口文档(swagger API文档),我采用POJO来对应数据表结构,使用VO来给传递前端要展示的数据,同时使用DTO来进行请求参数的封装。以上是一个具体的场景,可以发现这样子一个现象:POJO、VO、DTO对象是同一个数据的不同视图,所以会有很多相同的字段,由于不同的地方使用不同的对象,无可避免的会存在对象之间的值迁移问题,迁移的一个特征就是需要迁移的值字段相同。字段相同,于是才有了不同对象之间进行值迁移复制的问题。
2、现有的解决方法
一个一个的get出来后又set进去。这个方法无可避免会增加很多的编码复杂度,还是一些很没有营养的代码,看多了还会烦,所以作为一个有点小追求的程序员都没有办法忍受这种摧残。
使用别人已经存在的工具。在spring包里面有一个可以复制对象属性的工具方法,可以进行对象值的复制,下一段我们详细去分析它的这个工具方法。
自己动手丰衣足食。自己造工具来用,之所以自己造工具不是因为喜欢造工具,而是现有的工具没办法解决自己的需求,不得已而为之。
二、他山之石可以攻玉,详谈spring的对象复制工具
1、看看spring的对象复制工具到底咋样?
类名:org.springframework.beans.BeanUtils
这个类里面所有的属性复制的方法都调用了同一个方法,我们就直接分析这个原始的方法就行了。
private static void copyProperties(Object source, Object target, Class> editable, String... ignoreProperties)
throws BeansException {
//这里在校验要复制的对象是不可以为null的,这两个方法可是会报错的!!
Assert.notNull(source, "Source must not be null");
Assert.notNull(target, "Target must not be null");
//这里和下面的代码就有意思了
Class> actualEditable = target.getClass();//获取目标对象的动态类型
//下面判断的意图在于控制属性复制的范围
if (editable != null) {
//必须是target对象的父类或者其实现的接口类型,相当于instanceof运算符
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 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 &&
ClassUtils.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);
}
}
}
}
}
}
2、对复制工具的一些看法和总结
总结上一段代码的分析,我们发现spring自带的工具有以下特点:
它名副其实的是在复制属性,而不是字段!!
它可以通过一个目标对象的父类或者其实现的接口来控制需要复制属性的范围
很贴心的可以忽略原对象的某些字段,可以通过2的方法忽略某些目标对象的字段
但是,这远远不够!!!我需要如下的功能:
复制对象的字段,而不是属性,也就是说我需要一个更暴力的复制工具。
我需要忽略原对象的某些字段,同时也能够忽略目标对象的某些字段。
我的项目还需要忽略原对象为null的字段和目标对象不为null的字段
带着这三个需求,开始我的工具制造。
三、自己动手丰衣足食
1、我需要解析字节码
为了避免对字节码的重复解析,使用缓存来保留解析过的字节码解析结果,同时为了不让这个工具太占用内存,使用软引用来进行缓存,上代码:
private static final Map,SoftReference
2、我需要能够进行对象的复制,基本方法
public static void copyObjectProperties(Object source,MapsourceFieldMap,Object target,Map targetFieldMap){ //进行属性值复制 sourceFieldMap.forEach( (fieldName,sourceField) -> { //查看目标对象是否存在这个字段 Field targetField = targetFieldMap.get(fieldName); if(targetField != null){ try{ //对目标字段进行赋值操作 targetField.set(target,sourceField.get(source)); }catch(IllegalAccessException e){ e.printStackTrace(); } } } ); }
3、夜深了,准备睡觉了
基于这两个方法,对其进行封装,实现了我需要的功能,并且在项目中运行目前还没有bug,应该可以直接用在生产环境,各位看官觉得可以可以拿来试一试哦!!
4、完整的代码(带注释:需要自取,无外部依赖,拿来即用)
package edu.cqupt.demonstration.common.util;
import java.lang.ref.SoftReference;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
public class ReflectUtil {
private static final Map,SoftReference>> resolvedClassCache = new ConcurrentHashMap<>();
public static String[] getNullValueFieldNames(Object source){
//非空校验:NullPointerException
Objects.requireNonNull(source);
Class> sourceClass = source.getClass();
//从缓存里面获取,如果缓存里面没有就会进行第一次反射解析
Map classFieldMap = getClassFieldMapWithCache(sourceClass);
List nullValueFieldNames = new ArrayList<>();
classFieldMap.forEach(
(fieldName,field) -> {
try{
//挑选出值为null的字段名称
if(field.get(source) == null){
nullValueFieldNames.add(fieldName);
}
}catch(IllegalAccessException e){
e.printStackTrace();
}
}
);
return nullValueFieldNames.toArray(new String[]{});
}
public static String[] getNonNullValueFieldNames(Object source){
//非空校验
Objects.requireNonNull(source);
//获取空值字段名称
String[] nullValueFieldNames = getNullValueFieldNames(source);
Map classFieldMap = getClassFieldMapWithCache(source.getClass());
//获取全部的字段名称,因为原数据没办法修改,所以需要重新建立一个集合来进行判断
Set allFieldNames = new HashSet<>(classFieldMap.keySet());
//移除掉值为null的字段名称
allFieldNames.removeAll(Arrays.asList(nullValueFieldNames));
return allFieldNames.toArray(new String[]{});
}
public static void copyPropertiesSimple(Object source,Object target){
copyObjectProperties(
source,new HashMap<>(getClassFieldMapWithCache(source.getClass())),
target,new HashMap<>(getClassFieldMapWithCache(target.getClass())));
}
public static void copyPropertiesWithIgnoreSourceFields(Object source,Object target,String ...ignoreFieldNames){
Map sourceFieldMap = new HashMap<>(getClassFieldMapWithCache(source.getClass()));
filterByFieldName(sourceFieldMap,ignoreFieldNames);
copyObjectProperties(source,sourceFieldMap,target,new HashMap<>(getClassFieldMapWithCache(target.getClass())));
}
public static void copyPropertiesWithNonNullSourceFields(Object source,Object target){
Map sourceFieldMap = new HashMap<>(getClassFieldMapWithCache(source.getClass()));
filterByFieldValue(source,sourceFieldMap,true);
copyObjectProperties(source,sourceFieldMap,target,new HashMap<>(getClassFieldMapWithCache(target.getClass())));
}
public static void copyPropertiesWithIgnoreTargetFields(Object source,Object target,String ...ignoreFieldNames){
Map targetFieldMap = new HashMap<>(getClassFieldMapWithCache(target.getClass()));
filterByFieldName(targetFieldMap,ignoreFieldNames);
copyObjectProperties(source,new HashMap<>(getClassFieldMapWithCache(source.getClass())),target,targetFieldMap);
}
public static void copyPropertiesWithTargetFieldNonOverwrite(Object source,Object target){
Map targetFieldMap = new HashMap<>(getClassFieldMapWithCache(target.getClass()));
filterByFieldValue(target,targetFieldMap,false);
copyObjectProperties(source,new HashMap<>(getClassFieldMapWithCache(source.getClass())),target,targetFieldMap);
}
public static void copyPropertiesWithConditions(Object source,Object target
,String[] ignoreSourceFieldNames,String[] ignoreTargetFieldNames
,boolean isSourceFieldValueNullAble,boolean isTargetFiledValueOverwrite){
Map sourceFieldMap = new HashMap<>(getClassFieldMapWithCache(source.getClass()));
Map targetFieldMap = new HashMap<>(getClassFieldMapWithCache(target.getClass()));
if(!isSourceFieldValueNullAble){
filterByFieldValue(source,sourceFieldMap,true);
}
if(!isTargetFiledValueOverwrite){
filterByFieldValue(target,targetFieldMap,false);
}
filterByFieldName(sourceFieldMap,ignoreSourceFieldNames);
filterByFieldName(targetFieldMap,ignoreTargetFieldNames);
copyObjectProperties(source,sourceFieldMap,target,targetFieldMap);
}
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
public static Map resolveClassFieldMap(final Class> sourceClass){
SoftReference> softReference = resolvedClassCache.get(sourceClass);
//判断是否已经被初始化
if(softReference == null || softReference.get() == null){
//对同一个字节码对象的解析是同步的,但是不同字节码对象的解析是并发的
synchronized(sourceClass){
softReference = resolvedClassCache.get(sourceClass);
if(softReference == null || softReference.get() == null){
Map fieldMap = new HashMap<>();
Field[] declaredFields = sourceClass.getDeclaredFields();
if(declaredFields != null && declaredFields.length > 0){
for(Field field : declaredFields){
field.setAccessible(true);
fieldMap.put(field.getName(),field);
}
}
//设置为不变Map
fieldMap = Collections.unmodifiableMap(fieldMap);
softReference = new SoftReference<>(fieldMap);
resolvedClassCache.put(sourceClass,softReference);
return fieldMap;
}
}
}
return softReference.get();
}
public static Map getClassFieldMapWithCache(Class> sourceClass){
//查看缓存里面有没有已经解析完毕的现成的数据
SoftReference> softReference = resolvedClassCache.get(sourceClass);
//确保classFieldMap的正确初始化和缓存
if(softReference == null || softReference.get() == null){
//解析字节码对象
return resolveClassFieldMap(sourceClass);
}else {
//从缓存里面正确的取出数据
return softReference.get();
}
}
public static List resolveArrayToList(T ...args){
List result = new ArrayList<>();
if(args != null && args.length > 0){
result = Arrays.asList(args);
}
return result;
}
public static void copyObjectProperties(Object source,Map sourceFieldMap,Object target,Map targetFieldMap){
//进行属性值复制
sourceFieldMap.forEach(
(fieldName,sourceField) -> {
//查看目标对象是否存在这个字段
Field targetField = targetFieldMap.get(fieldName);
if(targetField != null){
try{
//对目标字段进行赋值操作
targetField.set(target,sourceField.get(source));
}catch(IllegalAccessException e){
e.printStackTrace();
}
}
}
);
}
public static void filterByFieldName(Map fieldMap,String ... ignoreFieldNames){
//需要忽略的对象字段
List ignoreNames = ReflectUtil.resolveArrayToList(ignoreFieldNames);
//移除忽略的对象字段
fieldMap.keySet().removeAll(ignoreNames);
}
public static void filterByFieldValue(Object object,Map fieldMap,boolean filterNullAble){
Iterator iterator = fieldMap.keySet().iterator();
if(filterNullAble){
while(iterator.hasNext()){
try{
//移除值为null的字段
if(fieldMap.get(iterator.next()).get(object) == null){
iterator.remove();
}
}catch(IllegalAccessException e){
e.printStackTrace();
}
}
}else {
while(iterator.hasNext()){
try{
//移除字段不为null的字段
if(fieldMap.get(iterator.next()).get(object) != null){
iterator.remove();
}
}catch(IllegalAccessException e){
e.printStackTrace();
}
}
}
}
}
补充知识:Java将两个JavaBean里相同的字段自动填充
最近因为经常会操作讲两个JavaBean之间相同的字段互相填充,所以就写了个偷懒的方法。记录一下
public static void autoFillEqFields(Object dto, Object obj) {
try {
Field[] pfields = dto.getClass().getDeclaredFields();
Field[] ofields = obj.getClass().getDeclaredFields();
for (Field of : ofields) {
if (of.getName().equals("serialVersionUID")) {
continue;
}
for (Field pf : pfields) {
if (of.getName().equals(pf.getName())) {
PropertyDescriptor rpd = new PropertyDescriptor(pf.getName(), dto.getClass());
Method getMethod = rpd.getReadMethod();// 获得读方法
PropertyDescriptor wpd = new PropertyDescriptor(pf.getName(), obj.getClass());
Method setMethod = wpd.getWriteMethod();// 获得写方法
setMethod.invoke(obj, getMethod.invoke(dto));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void autoFillEqFields(Object dto, Object obj, String[] fields) {
try {
Field[] ofields = obj.getClass().getDeclaredFields();
for (Field of : ofields) {
if (of.getName().equals("serialVersionUID")) {
continue;
}
for (String field : fields) {
if (of.getName().equals(field)) {
PropertyDescriptor rpd = new PropertyDescriptor(field, dto.getClass());
Method getMethod = rpd.getReadMethod();// 获得读方法
PropertyDescriptor wpd = new PropertyDescriptor(field, obj.getClass());
Method setMethod = wpd.getWriteMethod();// 获得写方法
setMethod.invoke(obj, getMethod.invoke(dto));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
但这样写不能把父类有的属性自动赋值所以修改了一下
public static void autoFillEqFields(Object obj, Object toObj) {
try {
Map getMaps = new HashMap<>();
Method[] sourceMethods = obj.getClass().getMethods();
for (Method m : sourceMethods) {
if (m.getName().startsWith("get")) {
getMaps.put(m.getName(), m);
}
}
Method[] targetMethods = toObj.getClass().getMethods();
for (Method m : targetMethods) {
if (!m.getName().startsWith("set")) {
continue;
}
String key = "g" + m.getName().substring(1);
Method getm = getMaps.get(key);
if (null == getm) {
continue;
}
// 写入方法写入
m.invoke(toObj, getm.invoke(obj));
}
} catch (Exception e) {
e.printStackTrace();
}
}
以上这篇利用Java反射机制实现对象相同字段的复制操作就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持考高分网。



