在Java開發工作中,有很多時候我們需要將不同的兩個對象實例進行屬性復制,從而基于源對象的屬性信息進行后續操作,而不改變源對象的屬性信息。這兩個對象實例有可能是同一個類的兩個實例,也可能是不同類的兩個實例,但是他們的屬相名稱相同。例如DO、DTO、VO、DAO等,這些實體的意義請查看DDD中分層架構。本文主要介紹幾種對象拷貝的方法
1. 對象拷貝
對象拷貝分為深拷貝和淺拷貝。根據使用場景進行不同選擇。在Java中,數據類型分為值類型(基本數據類型)和引用類型,值類型包括int、double、byte、boolean、char等簡單數據類型,引用類型包括類、接口、數組等復雜類型。
深度拷貝和淺度拷貝的主要區別在于是否支持引用類型的屬性拷貝,本文將探討目前使用較多的幾種對象拷貝的方案,以及其是否支持深拷貝和性能對比。
2. BeanUtils
2.1?apache的BeanUtils方案
使用org.apache.commons.beanutils.BeanUtils進行對象深入復制時候,主要通過向BeanUtils框架注入新的類型轉換器,因為默認情況下,BeanUtils對復雜對象的復制是引用,例如:
public static void beanUtilsTest() throws Exception {// 注冊轉化器BeanUtilsBean.getInstance().getConvertUtils().register(new ArbitrationConvert(), ArbitrationDO.class);Wrapper wrapper = new Wrapper();wrapper.setName("copy");wrapper.setNameDesc("copy complex object!");wrapper.setArbitration(newArbitrationDO());Wrapper dest = new Wrapper();// 對象復制BeanUtils.copyProperties(dest, wrapper);// 屬性驗證wrapper.getArbitration().setBizId("1");System.out.println(wrapper.getArbitration() == dest.getArbitration());System.out.println(wrapper.getArbitration().getBizId().equals(dest.getArbitration().getBizId()));
}public class ArbitrationConvert implements Converter {@Overridepublic <T> T convert(Class<T> type, Object value) {if (ArbitrationDO.class.equals(type)) {try {return type.cast(BeanUtils.cloneBean(value));} catch (Exception e) {e.printStackTrace();}}return null;}
}
可以發現,使用org.apache.commons.beanutils.BeanUtils復制引用時,主和源的引用為同一個,即改變了主的引用屬性會影響到源的引用,所以這是一種淺拷貝。
需要注意的是,apache的BeanUtils中,以下類型如果為空,會報錯(org.apache.commons.beanutils.ConversionException: No value specified for ?*)
/*** Register the converters for other types.* </p>* This method registers the following converters:* <ul>* <li>Class.class - {@link ClassConverter}* <li>java.util.Date.class - {@link DateConverter}* <li>java.util.Calendar.class - {@link CalendarConverter}* <li>File.class - {@link FileConverter}* <li>java.sql.Date.class - {@link SqlDateConverter}* <li>java.sql.Time.class - {@link SqlTimeConverter}* <li>java.sql.Timestamp.class - {@link SqlTimestampConverter}* <li>URL.class - {@link URLConverter}* </ul>* @param throwException <code>true if the converters should* throw an exception when a conversion error occurs, otherwise <code>* <code>false if a default value should be used.*/private void registerOther(boolean throwException) {register(Class.class, throwException ? new ClassConverter() : new ClassConverter(null));register(java.util.Date.class, throwException ? new DateConverter() : new DateConverter(null));register(Calendar.class, throwException ? new CalendarConverter() : new CalendarConverter(null));register(File.class, throwException ? new FileConverter() : new FileConverter(null));register(java.sql.Date.class, throwException ? new SqlDateConverter() : new SqlDateConverter(null));register(java.sql.Time.class, throwException ? new SqlTimeConverter() : new SqlTimeConverter(null));register(Timestamp.class, throwException ? new SqlTimestampConverter() : new SqlTimestampConverter(null));register(URL.class, throwException ? new URLConverter() : new URLConverter(null));}
當遇到這種問題是,可以手動將類型轉換器注冊進去,比如data類型:
public class BeanUtilEx extends BeanUtils { private static Map cache = new HashMap();
private static Log logger = LogFactory.getFactory().getInstance(BeanUtilEx.class); private BeanUtilEx() {
} static {
// 注冊sql.date的轉換器,即允許BeanUtils.copyProperties時的源目標的sql類型的值允許為空
ConvertUtils.register(new org.apache.commons.beanutils.converters.SqlDateConverter(null), java.sql.Date.class);
ConvertUtils.register(new org.apache.commons.beanutils.converters.SqlDateConverter(null), java.util.Date.class);
ConvertUtils.register(new org.apache.commons.beanutils.converters.SqlTimestampConverter(null), java.sql.Timestamp.class);
// 注冊util.date的轉換器,即允許BeanUtils.copyProperties時的源目標的util類型的值允許為空
} public static void copyProperties(Object target, Object source)
throws InvocationTargetException, IllegalAccessException {
// 支持對日期copy
org.apache.commons.beanutils.BeanUtils.copyProperties(target, source); }
2.2 apache的PropertyUtils方案
PropertyUtils的copyProperties()方法幾乎與BeanUtils.copyProperties()相同,主要的區別在于后者提供類型轉換功能,即發現兩個JavaBean的同名屬性為不同類型時,在支持的數據類型范圍內進行轉換,PropertyUtils不支持這個功能,所以說BeanUtils使用更普遍一點,犯錯的風險更低一點。而且它仍然屬于淺拷貝。
Apache提供了 SerializationUtils.clone(T),T對象需要實現 Serializable 接口,他屬于深克隆。
2.3 spring的BeanUtils方案
Spring中的BeanUtils,其中實現的方式很簡單,就是對兩個對象中相同名字的屬性進行簡單get/set,僅檢查屬性的可訪問性。
public static void copyProperties(Object source, Object target) throws BeansException {copyProperties(source, target, (Class)null, (String[])null);}public static void copyProperties(Object source, Object target, Class<?> editable) throws BeansException {copyProperties(source, target, editable, (String[])null);}public static void copyProperties(Object source, Object target, String... ignoreProperties) throws BeansException {copyProperties(source, target, (Class)null, ignoreProperties);}private static void copyProperties(Object source, Object target, Class<?> editable, String... ignoreProperties) throws BeansException {Assert.notNull(source, "Source must not be null");Assert.notNull(target, "Target must not be null");Class actualEditable = target.getClass();if(editable != null) {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;PropertyDescriptor[] var7 = targetPds;int var8 = targetPds.length;for(int var9 = 0; var9 < var8; ++var9) {PropertyDescriptor targetPd = var7[var9];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 ex = readMethod.invoke(source, new Object[0]);if(!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {writeMethod.setAccessible(true);}writeMethod.invoke(target, new Object[]{ex});} catch (Throwable var15) {throw new FatalBeanException("Could not copy property \'" + targetPd.getName() + "\' from source to target", var15);}}}}}}
可以看到, 成員變量賦值是基于目標對象的成員列表, 并且會跳過ignore的以及在源對象中不存在的, 所以這個方法是安全的, 不會因為兩個對象之間的結構差異導致錯誤, 但是必須保證同名的兩個成員變量類型相同.
3. dozer
Dozer(http://dozer.sourceforge.net/)能夠實現深拷貝。Dozer是基于反射來實現對象拷貝,反射調用set/get 或者是直接對成員變量賦值?。 該方式通過invoke執行賦值,實現時一般會采用beanutil, Javassist等開源庫。
簡單引用網上的例子,大多都是基于xml的配置,具體請查看其它Blog:
package com.maven.demo;import java.util.HashMap;
import java.util.Map;import org.dozer.DozerBeanMapper;
import org.junit.Test;import static org.junit.Assert.assertEquals;public class Demo{/*** map->bean*/@Testpublic void testDozer1() {Map<String,Object> map = new HashMap();map.put("id", 10000L);map.put("name", "小兵");map.put("description", "帥氣逼人");DozerBeanMapper mapper = new DozerBeanMapper();ProductVO product = mapper.map(map, ProductVO.class);assertEquals("小兵",product.getName());assertEquals("帥氣逼人",product.getDescription());assertEquals(Long.valueOf("10000"), product.getId());}/*** VO --> Entity (不同的實體之間,不同的屬性字段進行復制)*/@Testpublic void testDozer2(){ProductVO product = new ProductVO();product.setId(10001L);product.setName("xiaobing");product.setDescription("酷斃了");DozerBeanMapper mapper = new DozerBeanMapper();ProductEntity productEntity = mapper.map(product, ProductEntity.class);assertEquals("xiaobing",productEntity.getProductName());}}
4. ?MapStrcut
MapStrcut屬于編譯期的對象復制方案,它能夠動態生成set/get代碼的class文件?,在運行時直接調用該class文件。該方式實際上扔會存在set/get代碼,只是不需要自己寫了。
@Mapper(componentModel = "spring")
public interface MonitorAppGroupIdcDTOMapper {MonitorAppGroupIdcDTOMapper MAPPER = Mappers.getMapper(MonitorAppGroupIdcDTOMapper.class);void mapping(MonitorAppGroupIdcDTO source, @MappingTarget MonitorAppGroupIdcDTO dest);
}
5. 自定義Pojoconvert
public J copyPojo( P src, J des) throws NoSuchMethodException,SecurityException, IllegalAccessException, IllegalArgumentException, InvocationTargetException {if(src == null || des==null){return null;}String name = null ;String sufix = null;Class<?> cls = des.getClass() ;Method[] methods = cls.getMethods();for(Method m: methods){name = m.getName();if(name!=null && name.startsWith("set") && m.getParameterTypes().length==1){sufix = name.substring(3);m.getParameterTypes() ;Method getM = cls.getMethod("get"+sufix);m.invoke(des, getM.invoke(src));}}return des ;
}
沒有那么多驗證,不是很安全但是性能不錯。
6.?BeanCopier
@Testpublic void test_convert_entity_to_model_performance_use_beancopier(){List<ShopCouponEntity> entityList = ...long start = System.currentTimeMillis();BeanCopier b = BeanCopier.create(ShopCouponEntity.class, ShopCouponModel.class, false);List<ShopCouponModel> modelList = new ArrayList<>();for (ShopCouponEntity src : entityList) {ShopCouponModel dest = new ShopCouponModel();b.copy(src, dest, null);modelList.add(dest);}System.out.printf("BeanCopier took time: %d(ms)%n",System.currentTimeMillis() - start);}
可以通過緩存BeanCopier的實例來提高性能。
BeanCopier b = getFromCache(sourceClass,targetClass); //從緩存中取long start = System.currentTimeMillis();List<ShopCouponModel> modelList = new ArrayList<>();for (ShopCouponEntity src : entityList) {ShopCouponModel dest = new ShopCouponModel();b.copy(src, dest, null);modelList.add(dest);}
7.?fastjson和GSON
使用fastjson和GSON主要是通過對象json序列化和反序列化來完成對象復制,這里只是提供一種不一樣的對象拷貝的思路,例子略。
8. 性能
對兩種BeanUtils、Gson以及自定義Pojoconvert測試了性能
NewNovelMode des = null ;
NewNovelMode ori = buildModel();
Gson gson = new Gson();
int count = 100000;
//org.springframework.beans.BeanUtils.copyProperties
long s = System.currentTimeMillis();
for(int i=0;i<count;i++){des = new NewNovelMode();org.springframework.beans.BeanUtils.copyProperties(ori, des);
}
System.out.println("springframework BeanUtils cost:"+(System.currentTimeMillis() - s));
// System.out.println(new Gson().toJson(des));//org.apache.commons.beanutils.BeanUtils
s = System.currentTimeMillis();
for(int i=0;i<count;i++){des = new NewNovelMode();org.apache.commons.beanutils.BeanUtils.copyProperties(des, ori);
}
System.out.println("apache BeanUtils cost:"+(System.currentTimeMillis() - s));
// System.out.println(new Gson().toJson(des));//gson轉換
s = System.currentTimeMillis();
for(int i=0;i<count;i++){des = gson.fromJson(gson.toJson(ori), NewNovelMode.class);
}
System.out.println("gson cost:"+(System.currentTimeMillis() - s));
// System.out.println(new Gson().toJson(des));//Pojo轉換類
s = System.currentTimeMillis();
PojoUtils<NewNovelMode, NewNovelMode> pojoUtils = new PojoUtils<NewNovelMode, NewNovelMode>();
for(int i=0;i<count;i++){des = new NewNovelMode();pojoUtils.copyPojo(ori,des);
}
System.out.println("Pojoconvert cost:"+(System.currentTimeMillis() - s));
// System.out.println(new Gson().toJson(des));
結果就不貼出來了,在這里總結一下
Spring的BeanUtils比較穩定,不會因為量大了,耗時明顯增加,但其實基準耗時比較長;apache的BeanUtils穩定性與效率都不行,不可取;Gson,因為做兩個gson轉換,所以正常項目中,可能耗時會更少一些;PojoUtils穩定不如spring,但是總耗時優勢明顯,原因是它只是根據項目的需求,實現的簡單的轉換模板,這個代碼在其它的幾個工具類均有。
而在網上的其他Blog中(參見Reference),對Apache的BeanUtils、PropertyUtils和CGLIB的BeanCopier作了性能測試。
測試結果:
性能對比: BeanCopier > BeanUtils. 其中BeanCopier的性能高出另外兩個100數量級。
綜上推薦使用:
1.?BeanUtils(簡單,易用)
2.?BeanCopier(加入緩存后和手工set的性能接近)
3. Dozer(深拷貝)
4. fastjson(特定場景下使用)