利用Java反射機制實現物件相同欄位的複製操作
一、如何實現不同型別物件之間的複製問題?
1、為什麼會有這個問題?
近來在進行一個專案開發的時候,為了隱藏後端資料庫表結構、同時也為了配合給前端一個更友好的API介面文件(swagger API文件),我採用POJO來對應資料表結構,使用VO來給傳遞前端要展示的資料,同時使用DTO來進行請求引數的封裝。以上是一個具體的場景,可以發現這樣子一個現象:POJO、VO、DTO物件是同一個資料的不同檢視,所以會有很多相同的欄位,由於不同的地方使用不同的物件,無可避免的會存在物件之間的值遷移問題,遷移的一個特徵就是需要遷移的值欄位相同。欄位相同,於是才有了不同物件之間進行值遷移複製的問題。
2、現有的解決方法
一個一個的get出來後又set進去。這個方法無可避免會增加很多的編碼複雜度,還是一些很沒有營養的程式碼,看多了還會煩,所以作為一個有點小追求的程式設計師都沒有辦法忍受這種摧殘。
使用別人已經存在的工具。在spring包裡面有一個可以複製物件屬性的工具方法,可以進行物件值的複製,下一段我們詳細去分析它的這個工具方法。
自己動手豐衣足食。自己造工具來用,之所以自己造工具不是因為喜歡造工具,而是現有的工具沒辦法解決自己的需求,不得已而為之。
二、他山之石可以攻玉,詳談spring的物件複製工具
1、看看spring的物件複製工具到底咋樣?
類名:org.springframework.beans.BeanUtils
這個類裡面所有的屬性複製的方法都呼叫了同一個方法,我們就直接分析這個原始的方法就行了。
/** * Copy the property values of the given source bean into the given target bean. * <p>Note: The source and target classes do not have to match or even be derived * from each other,as long as the properties match. Any bean properties that the * source bean exposes but the target bean does not will silently be ignored. * @param source the source bean:也就是說要從這個物件裡面複製值出去 * @param target the target bean:出去就是複製到這裡面來 * @param editable the class (or interface) to restrict property setting to:這個類物件是target的父類或其實現的介面,用於控制屬性複製的範圍 * @param ignoreProperties array of property names to ignore:需要忽略的欄位 * @throws BeansException if the copying failed * @see BeanWrapper */ 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<String> 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<Class<?>,SoftReference<Map<String,Field>>> resolvedClassCache = new ConcurrentHashMap<>(); /** * 同步解析位元組碼物件,將解析的結果放入到快取 1、解析後的欄位物件全部 accessAble * 1、返回的集合不支援修改,要修改請記得自己重新建一個複製的副本 * @param sourceClass:需要解析的位元組碼物件 */ @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") public static Map<String,Field> resolveClassFieldMap(final Class<?> sourceClass){ SoftReference<Map<String,Field>> softReference = resolvedClassCache.get(sourceClass); //判斷是否已經被初始化 if(softReference == null || softReference.get() == null){ //對同一個位元組碼物件的解析是同步的,但是不同位元組碼物件的解析是併發的,因為位元組碼物件只有一個 synchronized(sourceClass){ softReference = resolvedClassCache.get(sourceClass); if(softReference == null || softReference.get() == null){ //採用:<欄位名稱,欄位物件> 來記錄解析結果 Map<String,Field> fieldMap = new HashMap<>(); /* Returns an array of Field objects reflecting all the fields declared by the class or interface represented by this Class object. This includes public,protected,default access,and private fields,but excludes inherited fields */ Field[] declaredFields = sourceClass.getDeclaredFields(); if(declaredFields != null && declaredFields.length > 0){ for(Field field : declaredFields){ /* Set the accessible flag for this object to the indicated boolean value. */ field.setAccessible(true); //欄位名稱和欄位物件 fieldMap.put(field.getName(),field); } } //設定為不變Map,這個肯定是不能夠改的啊!所以取的時候需要重新構建一個map fieldMap = Collections.unmodifiableMap(fieldMap); softReference = new SoftReference<>(fieldMap); /* 更新快取,將解析後的資料加入到快取裡面去 */ resolvedClassCache.put(sourceClass,softReference); return fieldMap; } } } /* 執行到這裡來的時候要麼早就存在,要麼就是已經被其他的執行緒給初始化了 */ return softReference.get(); }
2、我需要能夠進行物件的複製,基本方法
/** * 進行屬性的基本複製操作 * @param source:源物件 * @param sourceFieldMap:原物件解析結果 * @param target:目標物件 * @param targetFieldMap:目標物件解析結果 */ public static void copyObjectProperties(Object source,Map<String,Field> sourceFieldMap,Field> 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<Class<?>,Field>>> resolvedClassCache = new ConcurrentHashMap<>(); /* **************************************** * 獲取一個物件指定條件欄位名稱的工具方法 **************************************** */ /** * 獲取一個物件裡面欄位為null的欄位名稱集合 */ public static String[] getNullValueFieldNames(Object source){ //非空校驗:NullPointerException Objects.requireNonNull(source); Class<?> sourceClass = source.getClass(); //從快取裡面獲取,如果快取裡面沒有就會進行第一次反射解析 Map<String,Field> classFieldMap = getClassFieldMapWithCache(sourceClass); List<String> 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[]{}); } /** * 獲取一個物件裡面欄位不為null的欄位名稱集合 */ public static String[] getNonNullValueFieldNames(Object source){ //非空校驗 Objects.requireNonNull(source); //獲取空值欄位名稱 String[] nullValueFieldNames = getNullValueFieldNames(source); Map<String,Field> classFieldMap = getClassFieldMapWithCache(source.getClass()); //獲取全部的欄位名稱,因為原資料沒辦法修改,所以需要重新建立一個集合來進行判斷 Set<String> allFieldNames = new HashSet<>(classFieldMap.keySet()); //移除掉值為null的欄位名稱 allFieldNames.removeAll(Arrays.asList(nullValueFieldNames)); return allFieldNames.toArray(new String[]{}); } /* *************************************************************** * 複製一個物件的相關工具方法,注意事項如下: * 1、只能複製欄位名稱相同且資料型別相容的欄位資料 * 2、只能複製這個物件實際類(執行時動態型別)裡面宣告的各種欄位 *************************************************************** */ /** * 將一個物件裡面欄位相同、型別相容的資料複製到另外一個物件去 * 1、只複製類的執行時型別的宣告的全部訪問許可權的欄位 * @param source:從這個物件複製 * @param target:複製到這個物件來 */ public static void copyPropertiesSimple(Object source,Object target){ copyObjectProperties( source,new HashMap<>(getClassFieldMapWithCache(source.getClass())),target,new HashMap<>(getClassFieldMapWithCache(target.getClass()))); } /** * 除實現 copyPropertiesSimple 的功能外,會忽略掉原物件的指定欄位的複製 * @param ignoreFieldNames:需要忽略的原物件欄位名稱集合 */ public static void copyPropertiesWithIgnoreSourceFields(Object source,String ...ignoreFieldNames){ Map<String,Field> sourceFieldMap = new HashMap<>(getClassFieldMapWithCache(source.getClass())); filterByFieldName(sourceFieldMap,ignoreFieldNames); copyObjectProperties(source,sourceFieldMap,new HashMap<>(getClassFieldMapWithCache(target.getClass()))); } /** * 除實現 copyPropertiesSimple 的功能外,會忽略掉原物件欄位值為null的欄位 */ public static void copyPropertiesWithNonNullSourceFields(Object source,Object target){ Map<String,Field> sourceFieldMap = new HashMap<>(getClassFieldMapWithCache(source.getClass())); filterByFieldValue(source,true); copyObjectProperties(source,new HashMap<>(getClassFieldMapWithCache(target.getClass()))); } /** * 除實現 copyPropertiesSimple 的功能外,會忽略掉目標物件的指定欄位的複製 * @param ignoreFieldNames:需要忽略的原物件欄位名稱集合 */ public static void copyPropertiesWithIgnoreTargetFields(Object source,Field> targetFieldMap = new HashMap<>(getClassFieldMapWithCache(target.getClass())); filterByFieldName(targetFieldMap,targetFieldMap); } /** * 除實現 copyPropertiesSimple 的功能外,如果目標物件的屬性值不為null將不進行覆蓋 */ public static void copyPropertiesWithTargetFieldNonOverwrite(Object source,Field> targetFieldMap = new HashMap<>(getClassFieldMapWithCache(target.getClass())); filterByFieldValue(target,targetFieldMap,false); copyObjectProperties(source,targetFieldMap); } /** * 進行復制的完全定製複製 * @param source:源物件 * @param target:目標物件 * @param ignoreSourceFieldNames:需要忽略的原物件欄位名稱集合 * @param ignoreTargetFieldNames:要忽略的目標物件欄位集合 * @param isSourceFieldValueNullAble:是否在源物件的欄位為null的時候仍然進行賦值 * @param isTargetFiledValueOverwrite:是否在目標物件的值不為null的時候仍然進行賦值 */ public static void copyPropertiesWithConditions(Object source,String[] ignoreSourceFieldNames,String[] ignoreTargetFieldNames,boolean isSourceFieldValueNullAble,boolean isTargetFiledValueOverwrite){ Map<String,Field> sourceFieldMap = new HashMap<>(getClassFieldMapWithCache(source.getClass())); Map<String,Field> targetFieldMap = new HashMap<>(getClassFieldMapWithCache(target.getClass())); if(!isSourceFieldValueNullAble){ filterByFieldValue(source,true); } if(!isTargetFiledValueOverwrite){ filterByFieldValue(target,false); } filterByFieldName(sourceFieldMap,ignoreSourceFieldNames); filterByFieldName(targetFieldMap,ignoreTargetFieldNames); copyObjectProperties(source,targetFieldMap); } /* ****************************** * 內部工具方法或者內部相容方法 ****************************** */ /** * 同步解析位元組碼物件,將解析的結果放入到快取 1、解析後的欄位物件全部 accessAble * 1、返回的集合不支援修改,要修改請記得自己重新建一個複製的副本 * @param sourceClass:需要解析的位元組碼物件 */ @SuppressWarnings("SynchronizationOnLocalVariableOrMethodParameter") public static Map<String,Field>> softReference = resolvedClassCache.get(sourceClass); //判斷是否已經被初始化 if(softReference == null || softReference.get() == null){ //對同一個位元組碼物件的解析是同步的,但是不同位元組碼物件的解析是併發的 synchronized(sourceClass){ softReference = resolvedClassCache.get(sourceClass); if(softReference == null || softReference.get() == null){ Map<String,Field> fieldMap = new HashMap<>(); /* Returns an array of Field objects reflecting all the fields declared by the class or interface represented by this Class object. This includes public,but excludes inherited fields */ Field[] declaredFields = sourceClass.getDeclaredFields(); if(declaredFields != null && declaredFields.length > 0){ for(Field field : declaredFields){ /* Set the accessible flag for this object to the indicated boolean value. */ 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(); } /** * 確保正確的從快取裡面獲取解析後的資料 * 1、返回的集合不支援修改,要修改請記得自己重新建一個複製的副本 * @param sourceClass:需要解析的位元組碼物件 */ public static Map<String,Field> getClassFieldMapWithCache(Class<?> sourceClass){ //檢視快取裡面有沒有已經解析完畢的現成的資料 SoftReference<Map<String,Field>> softReference = resolvedClassCache.get(sourceClass); //確保classFieldMap的正確初始化和快取 if(softReference == null || softReference.get() == null){ //解析位元組碼物件 return resolveClassFieldMap(sourceClass); }else { //從快取裡面正確的取出資料 return softReference.get(); } } /** * 將一個可變引數集合轉換為List集合,當為空的時候返回空集合 */ public static <T> List<T> resolveArrayToList(T ...args){ List<T> result = new ArrayList<>(); if(args != null && args.length > 0){ result = Arrays.asList(args); } return result; } /** * 進行屬性的基本複製操作 * @param source:源物件 * @param sourceFieldMap:原物件解析結果 * @param target:目標物件 * @param targetFieldMap:目標物件解析結果 */ public static void copyObjectProperties(Object source,sourceField.get(source)); }catch(IllegalAccessException e){ e.printStackTrace(); } } } ); } /** * 忽略掉物件裡面的某些欄位 */ public static void filterByFieldName(Map<String,Field> fieldMap,String ... ignoreFieldNames){ //需要忽略的物件欄位 List<String> ignoreNames = ReflectUtil.<String>resolveArrayToList(ignoreFieldNames); //移除忽略的物件欄位 fieldMap.keySet().removeAll(ignoreNames); } /** * 忽略掉非空的欄位或者空的欄位 */ public static void filterByFieldValue(Object object,boolean filterNullAble){ Iterator<String> 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之間相同的欄位互相填充,所以就寫了個偷懶的方法。記錄一下
/** * 將兩個JavaBean裡相同的欄位自動填充 * @param dto 引數物件 * @param obj 待填充的物件 */ 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(); } } /** * 將兩個JavaBean裡相同的欄位自動填充,按指定的欄位填充 * @param dto * @param obj * @param String[] fields */ 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,getMethod.invoke(dto)); } } } } catch (Exception e) { e.printStackTrace(); } }
但這樣寫不能把父類有的屬性自動賦值所以修改了一下
/** * 將兩個JavaBean裡相同的欄位自動填充 * @param obj 原JavaBean物件 * @param toObj 將要填充的物件 */ public static void autoFillEqFields(Object obj,Object toObj) { try { Map<String,Method> 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反射機制實現物件相同欄位的複製操作就是小編分享給大家的全部內容了,希望能給大家一個參考,也希望大家多多支援我們。