利用Java反射机制实现对象相同字段的复制操作
一、如何实现不同类型对象之间的复制问题?
1、为什么会有这个问题?
近来在进行一个项目开发的时候,为了隐藏后端数据库表结构、同时也为了配合给前端一个更友好的API接口文档(swaggerAPI文档),我采用POJO来对应数据表结构,使用VO来给传递前端要展示的数据,同时使用DTO来进行请求参数的封装。以上是一个具体的场景,可以发现这样子一个现象:POJO、VO、DTO对象是同一个数据的不同视图,所以会有很多相同的字段,由于不同的地方使用不同的对象,无可避免的会存在对象之间的值迁移问题,迁移的一个特征就是需要迁移的值字段相同。字段相同,于是才有了不同对象之间进行值迁移复制的问题。
2、现有的解决方法
一个一个的get出来后又set进去。这个方法无可避免会增加很多的编码复杂度,还是一些很没有营养的代码,看多了还会烦,所以作为一个有点小追求的程序员都没有办法忍受这种摧残。
使用别人已经存在的工具。在spring包里面有一个可以复制对象属性的工具方法,可以进行对象值的复制,下一段我们详细去分析它的这个工具方法。
自己动手丰衣足食。自己造工具来用,之所以自己造工具不是因为喜欢造工具,而是现有的工具没办法解决自己的需求,不得已而为之。
二、他山之石可以攻玉,详谈spring的对象复制工具
1、看看spring的对象复制工具到底咋样?
类名:org.springframework.beans.BeanUtils
这个类里面所有的属性复制的方法都调用了同一个方法,我们就直接分析这个原始的方法就行了。
/** *Copythepropertyvaluesofthegivensourcebeanintothegiventargetbean. *Note:Thesourceandtargetclassesdonothavetomatchorevenbederived *fromeachother,aslongasthepropertiesmatch.Anybeanpropertiesthatthe *sourcebeanexposesbutthetargetbeandoesnotwillsilentlybeignored. *@paramsourcethesourcebean:也就是说要从这个对象里面复制值出去 *@paramtargetthetargetbean:出去就是复制到这里面来 *@parameditabletheclass(orinterface)torestrictpropertysettingto:这个类对象是target的父类或其实现的接口,用于控制属性复制的范围 *@paramignorePropertiesarrayofpropertynamestoignore:需要忽略的字段 *@throwsBeansExceptionifthecopyingfailed *@seeBeanWrapper */ privatestaticvoidcopyProperties(Objectsource,Objecttarget,Class>editable,String...ignoreProperties) throwsBeansException{ //这里在校验要复制的对象是不可以为null的,这两个方法可是会报错的!! Assert.notNull(source,"Sourcemustnotbenull"); Assert.notNull(target,"Targetmustnotbenull"); //这里和下面的代码就有意思了 Class>actualEditable=target.getClass();//获取目标对象的动态类型 //下面判断的意图在于控制属性复制的范围 if(editable!=null){ //必须是target对象的父类或者其实现的接口类型,相当于instanceof运算符 if(!editable.isInstance(target)){ thrownewIllegalArgumentException("Targetclass["+target.getClass().getName()+ "]notassignabletoEditableclass["+editable.getName()+"]"); } actualEditable=editable; } //不得不说,下面这段代码乖巧的像绵羊,待我们来分析分析它是如何如何乖巧的 PropertyDescriptor[]targetPds=getPropertyDescriptors(actualEditable);//获取属性描述,描述是什么?描述就是对属性的方法信息的封装,好乖。 List
ignoreList=(ignoreProperties!=null?Arrays.asList(ignoreProperties):null); //重头戏开始了!开始进行复制了 for(PropertyDescriptortargetPd:targetPds){ //先判断有没有写方法,没有写方法我也就没有必要读属性出来了,这个懒偷的真好! MethodwriteMethod=targetPd.getWriteMethod(); //首先,没有写方法的字段我不写,乖巧撒?就是说你不让我改我就不改,让我忽略我就忽略! if(writeMethod!=null&&(ignoreList==null||!ignoreList.contains(targetPd.getName()))){ PropertyDescriptorsourcePd=getPropertyDescriptor(source.getClass(),targetPd.getName()); //如果没办法从原对象里面读出属性也没有必要继续了 if(sourcePd!=null){ MethodreadMethod=sourcePd.getReadMethod(); //这里就更乖巧了!写方法不让我写我也不写!!! if(readMethod!=null&& ClassUtils.isAssignable(writeMethod.getParameterTypes()[0],readMethod.getReturnType())){ try{ //这里就算了,来都来了,就乖乖地进行值复制吧,别搞东搞西的了 if(!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())){ readMethod.setAccessible(true); } Objectvalue=readMethod.invoke(source); if(!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())){ writeMethod.setAccessible(true); } writeMethod.invoke(target,value); } catch(Throwableex){ thrownewFatalBeanException( "Couldnotcopyproperty'"+targetPd.getName()+"'fromsourcetotarget",ex); } } } } } }
2、对复制工具的一些看法和总结
总结上一段代码的分析,我们发现spring自带的工具有以下特点:
它名副其实的是在复制属性,而不是字段!!
它可以通过一个目标对象的父类或者其实现的接口来控制需要复制属性的范围
很贴心的可以忽略原对象的某些字段,可以通过2的方法忽略某些目标对象的字段
但是,这远远不够!!!我需要如下的功能:
复制对象的字段,而不是属性,也就是说我需要一个更暴力的复制工具。
我需要忽略原对象的某些字段,同时也能够忽略目标对象的某些字段。
我的项目还需要忽略原对象为null的字段和目标对象不为null的字段
带着这三个需求,开始我的工具制造。
三、自己动手丰衣足食
1、我需要解析字节码
为了避免对字节码的重复解析,使用缓存来保留解析过的字节码解析结果,同时为了不让这个工具太占用内存,使用软引用来进行缓存,上代码:
/* ****************************************************** *基础的用于支持反射解析的解析结果缓存,使用软引用实现 ****************************************************** */ privatestaticfinalMap,SoftReference
2、我需要能够进行对象的复制,基本方法
/** *进行属性的基本复制操作 *@paramsource:源对象 *@paramsourceFieldMap:原对象解析结果 *@paramtarget:目标对象 *@paramtargetFieldMap:目标对象解析结果 */ publicstaticvoidcopyObjectProperties(Objectsource,MapsourceFieldMap,Objecttarget,Map targetFieldMap){ //进行属性值复制 sourceFieldMap.forEach( (fieldName,sourceField)->{ //查看目标对象是否存在这个字段 FieldtargetField=targetFieldMap.get(fieldName); if(targetField!=null){ try{ //对目标字段进行赋值操作 targetField.set(target,sourceField.get(source)); }catch(IllegalAccessExceptione){ e.printStackTrace(); } } } ); }
3、夜深了,准备睡觉了
基于这两个方法,对其进行封装,实现了我需要的功能,并且在项目中运行目前还没有bug,应该可以直接用在生产环境,各位看官觉得可以可以拿来试一试哦!!
4、完整的代码(带注释:需要自取,无外部依赖,拿来即用)
packageedu.cqupt.demonstration.common.util;
importjava.lang.ref.SoftReference;
importjava.lang.reflect.Field;
importjava.util.*;
importjava.util.concurrent.ConcurrentHashMap;
/**
*反射的工具集,主要用于对对象的复制操作
*/
publicclassReflectUtil{
/*
******************************************************
*基础的用于支持反射解析的解析结果缓存,使用软引用实现
******************************************************
*/
privatestaticfinalMap,SoftReference>>resolvedClassCache=newConcurrentHashMap<>();
/*
****************************************
*获取一个对象指定条件字段名称的工具方法
****************************************
*/
/**
*获取一个对象里面字段为null的字段名称集合
*/
publicstaticString[]getNullValueFieldNames(Objectsource){
//非空校验:NullPointerException
Objects.requireNonNull(source);
Class>sourceClass=source.getClass();
//从缓存里面获取,如果缓存里面没有就会进行第一次反射解析
MapclassFieldMap=getClassFieldMapWithCache(sourceClass);
ListnullValueFieldNames=newArrayList<>();
classFieldMap.forEach(
(fieldName,field)->{
try{
//挑选出值为null的字段名称
if(field.get(source)==null){
nullValueFieldNames.add(fieldName);
}
}catch(IllegalAccessExceptione){
e.printStackTrace();
}
}
);
returnnullValueFieldNames.toArray(newString[]{});
}
/**
*获取一个对象里面字段不为null的字段名称集合
*/
publicstaticString[]getNonNullValueFieldNames(Objectsource){
//非空校验
Objects.requireNonNull(source);
//获取空值字段名称
String[]nullValueFieldNames=getNullValueFieldNames(source);
MapclassFieldMap=getClassFieldMapWithCache(source.getClass());
//获取全部的字段名称,因为原数据没办法修改,所以需要重新建立一个集合来进行判断
SetallFieldNames=newHashSet<>(classFieldMap.keySet());
//移除掉值为null的字段名称
allFieldNames.removeAll(Arrays.asList(nullValueFieldNames));
returnallFieldNames.toArray(newString[]{});
}
/*
***************************************************************
*复制一个对象的相关工具方法,注意事项如下:
*1、只能复制字段名称相同且数据类型兼容的字段数据
*2、只能复制这个对象实际类(运行时动态类型)里面声明的各种字段
***************************************************************
*/
/**
*将一个对象里面字段相同、类型兼容的数据复制到另外一个对象去
*1、只复制类的运行时类型的声明的全部访问权限的字段
*@paramsource:从这个对象复制
*@paramtarget:复制到这个对象来
*/
publicstaticvoidcopyPropertiesSimple(Objectsource,Objecttarget){
copyObjectProperties(
source,newHashMap<>(getClassFieldMapWithCache(source.getClass())),
target,newHashMap<>(getClassFieldMapWithCache(target.getClass())));
}
/**
*除实现copyPropertiesSimple的功能外,会忽略掉原对象的指定字段的复制
*@paramignoreFieldNames:需要忽略的原对象字段名称集合
*/
publicstaticvoidcopyPropertiesWithIgnoreSourceFields(Objectsource,Objecttarget,String...ignoreFieldNames){
MapsourceFieldMap=newHashMap<>(getClassFieldMapWithCache(source.getClass()));
filterByFieldName(sourceFieldMap,ignoreFieldNames);
copyObjectProperties(source,sourceFieldMap,target,newHashMap<>(getClassFieldMapWithCache(target.getClass())));
}
/**
*除实现copyPropertiesSimple的功能外,会忽略掉原对象字段值为null的字段
*/
publicstaticvoidcopyPropertiesWithNonNullSourceFields(Objectsource,Objecttarget){
MapsourceFieldMap=newHashMap<>(getClassFieldMapWithCache(source.getClass()));
filterByFieldValue(source,sourceFieldMap,true);
copyObjectProperties(source,sourceFieldMap,target,newHashMap<>(getClassFieldMapWithCache(target.getClass())));
}
/**
*除实现copyPropertiesSimple的功能外,会忽略掉目标对象的指定字段的复制
*@paramignoreFieldNames:需要忽略的原对象字段名称集合
*/
publicstaticvoidcopyPropertiesWithIgnoreTargetFields(Objectsource,Objecttarget,String...ignoreFieldNames){
MaptargetFieldMap=newHashMap<>(getClassFieldMapWithCache(target.getClass()));
filterByFieldName(targetFieldMap,ignoreFieldNames);
copyObjectProperties(source,newHashMap<>(getClassFieldMapWithCache(source.getClass())),target,targetFieldMap);
}
/**
*除实现copyPropertiesSimple的功能外,如果目标对象的属性值不为null将不进行覆盖
*/
publicstaticvoidcopyPropertiesWithTargetFieldNonOverwrite(Objectsource,Objecttarget){
MaptargetFieldMap=newHashMap<>(getClassFieldMapWithCache(target.getClass()));
filterByFieldValue(target,targetFieldMap,false);
copyObjectProperties(source,newHashMap<>(getClassFieldMapWithCache(source.getClass())),target,targetFieldMap);
}
/**
*进行复制的完全定制复制
*@paramsource:源对象
*@paramtarget:目标对象
*@paramignoreSourceFieldNames:需要忽略的原对象字段名称集合
*@paramignoreTargetFieldNames:要忽略的目标对象字段集合
*@paramisSourceFieldValueNullAble:是否在源对象的字段为null的时候仍然进行赋值
*@paramisTargetFiledValueOverwrite:是否在目标对象的值不为null的时候仍然进行赋值
*/
publicstaticvoidcopyPropertiesWithConditions(Objectsource,Objecttarget
,String[]ignoreSourceFieldNames,String[]ignoreTargetFieldNames
,booleanisSourceFieldValueNullAble,booleanisTargetFiledValueOverwrite){
MapsourceFieldMap=newHashMap<>(getClassFieldMapWithCache(source.getClass()));
MaptargetFieldMap=newHashMap<>(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);
}
/*
******************************
*内部工具方法或者内部兼容方法
******************************
*/
/**
*同步解析字节码对象,将解析的结果放入到缓存1、解析后的字段对象全部accessAble
*1、返回的集合不支持修改,要修改请记得自己重新建一个复制的副本
*@paramsourceClass:需要解析的字节码对象
*/
@SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter")
publicstaticMapresolveClassFieldMap(finalClass>sourceClass){
SoftReference>softReference=resolvedClassCache.get(sourceClass);
//判断是否已经被初始化
if(softReference==null||softReference.get()==null){
//对同一个字节码对象的解析是同步的,但是不同字节码对象的解析是并发的
synchronized(sourceClass){
softReference=resolvedClassCache.get(sourceClass);
if(softReference==null||softReference.get()==null){
MapfieldMap=newHashMap<>();
/*
ReturnsanarrayofFieldobjectsreflectingallthefieldsdeclaredbytheclassorinterfacerepresentedbythis
Classobject.Thisincludespublic,protected,defaultaccess,andprivatefields,butexcludesinheritedfields
*/
Field[]declaredFields=sourceClass.getDeclaredFields();
if(declaredFields!=null&&declaredFields.length>0){
for(Fieldfield:declaredFields){
/*
Settheaccessibleflagforthisobjecttotheindicatedbooleanvalue.
*/
field.setAccessible(true);
fieldMap.put(field.getName(),field);
}
}
//设置为不变Map
fieldMap=Collections.unmodifiableMap(fieldMap);
softReference=newSoftReference<>(fieldMap);
/*
更新缓存,将解析后的数据加入到缓存里面去
*/
resolvedClassCache.put(sourceClass,softReference);
returnfieldMap;
}
}
}
/*
运行到这里来的时候要么早就存在,要么就是已经被其他的线程给初始化了
*/
returnsoftReference.get();
}
/**
*确保正确的从缓存里面获取解析后的数据
*1、返回的集合不支持修改,要修改请记得自己重新建一个复制的副本
*@paramsourceClass:需要解析的字节码对象
*/
publicstaticMapgetClassFieldMapWithCache(Class>sourceClass){
//查看缓存里面有没有已经解析完毕的现成的数据
SoftReference>softReference=resolvedClassCache.get(sourceClass);
//确保classFieldMap的正确初始化和缓存
if(softReference==null||softReference.get()==null){
//解析字节码对象
returnresolveClassFieldMap(sourceClass);
}else{
//从缓存里面正确的取出数据
returnsoftReference.get();
}
}
/**
*将一个可变参数集合转换为List集合,当为空的时候返回空集合
*/
publicstaticListresolveArrayToList(T...args){
Listresult=newArrayList<>();
if(args!=null&&args.length>0){
result=Arrays.asList(args);
}
returnresult;
}
/**
*进行属性的基本复制操作
*@paramsource:源对象
*@paramsourceFieldMap:原对象解析结果
*@paramtarget:目标对象
*@paramtargetFieldMap:目标对象解析结果
*/
publicstaticvoidcopyObjectProperties(Objectsource,MapsourceFieldMap,Objecttarget,MaptargetFieldMap){
//进行属性值复制
sourceFieldMap.forEach(
(fieldName,sourceField)->{
//查看目标对象是否存在这个字段
FieldtargetField=targetFieldMap.get(fieldName);
if(targetField!=null){
try{
//对目标字段进行赋值操作
targetField.set(target,sourceField.get(source));
}catch(IllegalAccessExceptione){
e.printStackTrace();
}
}
}
);
}
/**
*忽略掉对象里面的某些字段
*/
publicstaticvoidfilterByFieldName(MapfieldMap,String...ignoreFieldNames){
//需要忽略的对象字段
ListignoreNames=ReflectUtil.resolveArrayToList(ignoreFieldNames);
//移除忽略的对象字段
fieldMap.keySet().removeAll(ignoreNames);
}
/**
*忽略掉非空的字段或者空的字段
*/
publicstaticvoidfilterByFieldValue(Objectobject,MapfieldMap,booleanfilterNullAble){
Iteratoriterator=fieldMap.keySet().iterator();
if(filterNullAble){
while(iterator.hasNext()){
try{
//移除值为null的字段
if(fieldMap.get(iterator.next()).get(object)==null){
iterator.remove();
}
}catch(IllegalAccessExceptione){
e.printStackTrace();
}
}
}else{
while(iterator.hasNext()){
try{
//移除字段不为null的字段
if(fieldMap.get(iterator.next()).get(object)!=null){
iterator.remove();
}
}catch(IllegalAccessExceptione){
e.printStackTrace();
}
}
}
}
}
补充知识:Java将两个JavaBean里相同的字段自动填充
最近因为经常会操作讲两个JavaBean之间相同的字段互相填充,所以就写了个偷懒的方法。记录一下
/**
*将两个JavaBean里相同的字段自动填充
*@paramdto参数对象
*@paramobj待填充的对象
*/
publicstaticvoidautoFillEqFields(Objectdto,Objectobj){
try{
Field[]pfields=dto.getClass().getDeclaredFields();
Field[]ofields=obj.getClass().getDeclaredFields();
for(Fieldof:ofields){
if(of.getName().equals("serialVersionUID")){
continue;
}
for(Fieldpf:pfields){
if(of.getName().equals(pf.getName())){
PropertyDescriptorrpd=newPropertyDescriptor(pf.getName(),dto.getClass());
MethodgetMethod=rpd.getReadMethod();//获得读方法
PropertyDescriptorwpd=newPropertyDescriptor(pf.getName(),obj.getClass());
MethodsetMethod=wpd.getWriteMethod();//获得写方法
setMethod.invoke(obj,getMethod.invoke(dto));
}
}
}
}catch(Exceptione){
e.printStackTrace();
}
}
/**
*将两个JavaBean里相同的字段自动填充,按指定的字段填充
*@paramdto
*@paramobj
*@paramString[]fields
*/
publicstaticvoidautoFillEqFields(Objectdto,Objectobj,String[]fields){
try{
Field[]ofields=obj.getClass().getDeclaredFields();
for(Fieldof:ofields){
if(of.getName().equals("serialVersionUID")){
continue;
}
for(Stringfield:fields){
if(of.getName().equals(field)){
PropertyDescriptorrpd=newPropertyDescriptor(field,dto.getClass());
MethodgetMethod=rpd.getReadMethod();//获得读方法
PropertyDescriptorwpd=newPropertyDescriptor(field,obj.getClass());
MethodsetMethod=wpd.getWriteMethod();//获得写方法
setMethod.invoke(obj,getMethod.invoke(dto));
}
}
}
}catch(Exceptione){
e.printStackTrace();
}
}
但这样写不能把父类有的属性自动赋值所以修改了一下
/**
*将两个JavaBean里相同的字段自动填充
*@paramobj原JavaBean对象
*@paramtoObj将要填充的对象
*/
publicstaticvoidautoFillEqFields(Objectobj,ObjecttoObj){
try{
MapgetMaps=newHashMap<>();
Method[]sourceMethods=obj.getClass().getMethods();
for(Methodm:sourceMethods){
if(m.getName().startsWith("get")){
getMaps.put(m.getName(),m);
}
}
Method[]targetMethods=toObj.getClass().getMethods();
for(Methodm:targetMethods){
if(!m.getName().startsWith("set")){
continue;
}
Stringkey="g"+m.getName().substring(1);
Methodgetm=getMaps.get(key);
if(null==getm){
continue;
}
//写入方法写入
m.invoke(toObj,getm.invoke(obj));
}
}catch(Exceptione){
e.printStackTrace();
}
}
以上这篇利用Java反射机制实现对象相同字段的复制操作就是小编分享给大家的全部内容了,希望能给大家一个参考,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。