一、對象屬性拷貝工具類
”天下武功,唯快不破“。在互聯網行業中體現的更加淋淋盡致。我們在業務系統會經常遇到業務對象間屬性的拷貝,對如外接口一般都使用特定的DTO對象,而不會使用領域模型,以避免兩者的變動互相影響。我們不僅要關注“快”,還要注重CPU的穩定即避免CPU使用的大起大落現象。如何高效完成屬性的拷貝並降低對CPU的使用率或避免CPU的抖動。
相關博文已經有很多,為什么還要自己在一篇類似的哪?原因有二:一是加深理解二是比較各自優劣。目前對象間屬性的拷貝常用的方法大致如下:
- 手動拷貝(set)
- 動態代理
cglib版本:net.sf.cglib.beans.BeanCopier.copy(Object from, Object to, Converter converter)
- 反射機制
Spring版本:org.springframework.beans.BeanUtils.copyProperties(Object source, Object target)
Apache版本:org.apache.commons.beanutils.PropertyUtils.copyProperties(Object dest, Object orig)
org.apache.commons.beanutils.BeanUtils.copyProperties(Object dest, Object orig)
DozerMapper
二、實踐說明性能優劣
1、環境
WIN7 i5,12G內存,
JVM:
java version "1.8.0_51"
Java(TM) SE Runtime Environment (build 1.8.0_51-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.51-b03, mixed mode)
依賴jar及版本
2、代碼結構

package test; /** * @author wy * */ public interface IMethodCallBack { public String getMethodName(); public DestBean callBack(SourceBean sourceBean) throws Exception; }

package test; /** * * @author wy * */ public class CopyProcessor { public int count; public CopyProcessor(int count) { this.count = count; System.out.println("性能測試=========" + this.count + "========="); } public void processor(IMethodCallBack methodCallBack, SourceBean sourceBean) throws Exception { long begin = System.currentTimeMillis(); DestBean destBean = null; System.out.println(methodCallBack.getMethodName() + "開始進行測試"); for (int i = 0; i < count; i++) { destBean = methodCallBack.callBack(sourceBean); } long end = System.currentTimeMillis(); System.out.println(methodCallBack.getMethodName() + " 耗時 = " + (end - begin) + " 毫秒"); System.out.println(destBean.getPid()); System.out.println(destBean.getUserId()); System.out.println(destBean.getSubTitle()); System.out.println(destBean.getAlias()); System.out.println(destBean.getActor()); System.out.println(destBean.getShortDesc()); System.out.println("----------------------------------------"); } }

package test; import java.util.LinkedHashMap; import java.util.Map; import org.apache.commons.beanutils.PropertyUtils; import org.junit.Before; import org.junit.Test; import org.springframework.beans.BeanUtils; import net.sf.cglib.beans.BeanCopier; /** * * @author wy * */ public class PerformanceTest { public SourceBean sourceBean = null; public IMethodCallBack manualCopy = null; public IMethodCallBack cglib = null; public IMethodCallBack springBeanUtils = null; public IMethodCallBack apachePropertyUtils = null; public IMethodCallBack apacheBeanUtils = null; @Before public void init() { // 初始化數據 sourceBean = new SourceBean(); sourceBean.setPid(Long.valueOf(1001)); sourceBean.setUserId(Long.valueOf(123)); sourceBean.setSubTitle("人再囧途之港囧"); sourceBean.setAlias("港囧"); Map<String, String> map = new LinkedHashMap<String, String>(); map.put("主演1", "徐崢"); map.put("主演2", "包貝爾"); map.put("主演3", "趙薇"); sourceBean.setActor(map); sourceBean.setShortDesc("徐來和小舅子抱着各自不同目的來到香港,展開了一段陰差陽錯、啼笑皆非的旅程,最終兩人獲得友誼並懂得了人生真諦。"); // 手動設置屬性 manualCopy = new IMethodCallBack() { @Override public String getMethodName() { return "manual copy"; } @Override public DestBean callBack(SourceBean sourceBean) throws Exception { DestBean destBean = new DestBean(); destBean.setActor(sourceBean.getActor()); destBean.setPid(sourceBean.getPid()); destBean.setUserId(sourceBean.getUserId().intValue()); destBean.setShortDesc(sourceBean.getShortDesc()); destBean.setSubTitle(sourceBean.getSubTitle()); destBean.setAlias(sourceBean.getAlias()); return destBean; } }; // Cglib cglib = new IMethodCallBack() { BeanCopier beanCopier = BeanCopier.create(SourceBean.class, DestBean.class, false); @Override public String getMethodName() { return "net.sf.cglib.beans.BeanCopier.create"; } @Override public DestBean callBack(SourceBean sourceBean) throws Exception { DestBean destBean = new DestBean(); beanCopier.copy(sourceBean, destBean, null); return destBean; } }; // Spring BeanUtils springBeanUtils = new IMethodCallBack() { @Override public String getMethodName() { return "org.springframework.beans.BeanUtils.copyProperties"; } @Override public DestBean callBack(SourceBean sourceBean) throws Exception { DestBean destBean = new DestBean(); BeanUtils.copyProperties(sourceBean, destBean); return destBean; } }; // Apache PropertyUtils apachePropertyUtils = new IMethodCallBack() { @Override public String getMethodName() { return "org.apache.commons.beanutils.PropertyUtils.copyProperties"; } @Override public DestBean callBack(SourceBean sourceBean) throws Exception { DestBean destBean = new DestBean(); PropertyUtils.copyProperties(destBean, sourceBean); return destBean; } }; // Apache BeanUtils apacheBeanUtils = new IMethodCallBack() { @Override public String getMethodName() { return "org.apache.commons.beanutils.BeanUtils.copyProperties"; } @Override public DestBean callBack(SourceBean sourceBean) throws Exception { DestBean destBean = new DestBean(); org.apache.commons.beanutils.BeanUtils.copyProperties(destBean, sourceBean); return destBean; } }; } // 測試一百次性能測試 @Test public void perform100() throws Exception { CopyProcessor processor100 = new CopyProcessor(100); processor100.processor(manualCopy, sourceBean); processor100.processor(cglib, sourceBean); processor100.processor(springBeanUtils, sourceBean); processor100.processor(apachePropertyUtils, sourceBean); processor100.processor(apacheBeanUtils, sourceBean); CopyProcessor processor100R = new CopyProcessor(100); processor100R.processor(apacheBeanUtils, sourceBean); processor100R.processor(apachePropertyUtils, sourceBean); processor100R.processor(springBeanUtils, sourceBean); processor100R.processor(cglib, sourceBean); processor100R.processor(manualCopy, sourceBean); } // 測試一千性能測試 @Test public void perform1000() throws Exception { CopyProcessor processor1000 = new CopyProcessor(1000); processor1000.processor(manualCopy, sourceBean); processor1000.processor(cglib, sourceBean); processor1000.processor(springBeanUtils, sourceBean); processor1000.processor(apachePropertyUtils, sourceBean); processor1000.processor(apacheBeanUtils, sourceBean); CopyProcessor processor1000R = new CopyProcessor(1000); processor1000R.processor(apacheBeanUtils, sourceBean); processor1000R.processor(apachePropertyUtils, sourceBean); processor1000R.processor(springBeanUtils, sourceBean); processor1000R.processor(cglib, sourceBean); processor1000R.processor(manualCopy, sourceBean); } // 測試一萬次性能測試 @Test public void perform10000() throws Exception { CopyProcessor processor10000 = new CopyProcessor(10000); processor10000.processor(manualCopy, sourceBean); processor10000.processor(cglib, sourceBean); processor10000.processor(springBeanUtils, sourceBean); processor10000.processor(apachePropertyUtils, sourceBean); processor10000.processor(apacheBeanUtils, sourceBean); CopyProcessor processor10000R = new CopyProcessor(10000); processor10000R.processor(apacheBeanUtils, sourceBean); processor10000R.processor(apachePropertyUtils, sourceBean); processor10000R.processor(springBeanUtils, sourceBean); processor10000R.processor(cglib, sourceBean); processor10000R.processor(manualCopy, sourceBean); } // 測試十萬次性能測試 @Test public void perform100000() throws Exception { CopyProcessor processor100000 = new CopyProcessor(100000); processor100000.processor(manualCopy, sourceBean); processor100000.processor(cglib, sourceBean); processor100000.processor(springBeanUtils, sourceBean); processor100000.processor(apachePropertyUtils, sourceBean); processor100000.processor(apacheBeanUtils, sourceBean); processor100000.processor(apacheBeanUtils, sourceBean); processor100000.processor(apachePropertyUtils, sourceBean); processor100000.processor(springBeanUtils, sourceBean); processor100000.processor(cglib, sourceBean); processor100000.processor(manualCopy, sourceBean); } // 測試一百萬次性能測試 @Test public void perform1000000() throws Exception { CopyProcessor processor1000000 = new CopyProcessor(1000000); processor1000000.processor(manualCopy, sourceBean); processor1000000.processor(cglib, sourceBean); processor1000000.processor(springBeanUtils, sourceBean); processor1000000.processor(apachePropertyUtils, sourceBean); processor1000000.processor(apacheBeanUtils, sourceBean); CopyProcessor processor1000000R = new CopyProcessor(1000000); processor1000000R.processor(apacheBeanUtils, sourceBean); processor1000000R.processor(apachePropertyUtils, sourceBean); processor1000000R.processor(springBeanUtils, sourceBean); processor1000000R.processor(cglib, sourceBean); processor1000000R.processor(manualCopy, sourceBean); } }
3、結果比較
一百次性能測試 | 第一次(毫秒) | 第二次(毫秒) | 第三次(毫秒) | 每次平均值(毫秒) |
manualCopy | 0 | 1 | 1 | 0.0066666666666667 |
cglib | 1 | 2 | 2 | 0.0166666666666667 |
springBeanUtils | 177 | 181 | 192 | 1.833333333333333 |
apachePropertyUtils | 179 | 207 | 192 | 1.926666666666667 |
apacheBeanUtils | 96 | 94 | 89 | 0.93 |
一千次性能測試 | 第一次(毫秒) | 第二次(毫秒) | 第三次(毫秒) | 每次平均值(毫秒) |
manualCopy | 1 | 1 | 2 | 0.0013333333333333 |
cglib | 13 | 11 | 12 | 0.012 |
springBeanUtils | 272 | 261 | 286 | 0.273 |
apachePropertyUtils | 450 | 431 | 444 | 0.4416666666666667 |
apacheBeanUtils | 349 | 353 | 360 | 0.354 |
一萬次性能測試 | 第一次(毫秒) | 第二次(毫秒) | 第三次(毫秒) | 每次平均值(毫秒) |
manualCopy | 2 | 3 | 4 | 0.0003 |
cglib | 16 | 18 | 17 | 0.0016 |
springBeanUtils | 526 | 554 | 532 | 0.0537333333333333 |
apachePropertyUtils | 1888 | 1848 | 1832 | 0.1856 |
apacheBeanUtils | 2210 | 2150 | 2162 | 0.2174 |
十萬次性能測試 | 第一次(毫秒) | 第二次(毫秒) | 第三次(毫秒) | 每次平均值(毫秒) |
manualCopy | 26 | 24 | 26 | 0.00025333 |
cglib | 48 | 51 | 48 | 0.00049 |
springBeanUtils | 1949 | 1956 | 1881 | 0.0192866666666667 |
apachePropertyUtils | 14741 | 15478 | 15065 | 0.1509466666666667 |
apacheBeanUtils | 19506 | 19800 | 19753 | 0.1968633333333333 |
輸出結果: manualCopy > cglibCopy > springBeanUtils > apachePropertyUtils > apacheBeanUtils 可以理解為: 手工復制 > cglib > 反射。
對於最求速度的屬性拷貝,建議使用手動設置拷貝,雖然代碼會變得臃腫不堪。
4、原理說明
反射類型:
都使用靜態類調用,最終轉化虛擬機中兩個單例的工具對象。
public BeanUtilsBean()
{
this(new ConvertUtilsBean(), new PropertyUtilsBean());
}
ConvertUtilsBean可以通過ConvertUtils全局自定義注冊。
ConvertUtils.register(new DateConvert(), java.util.Date.class);
PropertyUtilsBean的copyProperties方法實現了拷貝的算法。
1、 動態bean:orig instanceof DynaBean:Object value = ((DynaBean)orig).get(name);然后把value復制到動態bean類
2、 Map類型:orig instanceof Map:key值逐個拷貝
3、 其他普通類::從beanInfo【每一個對象都有一個緩存的bean信息,包含屬性字段等】取出name,然后把sourceClass和targetClass逐個拷貝
Cglib類型:BeanCopier
copier = BeanCopier.create(source.getClass(), target.getClass(), false);
copier.copy(source, target, null);
Create對象過程:產生sourceClass-》TargetClass的拷貝代理類,放入jvm中,所以創建的代理類的時候比較耗時。最好保證這個對象的單例模式,可以參照最后一部分的優化方案。
創建過程:源代碼見jdk:net.sf.cglib.beans.BeanCopier.Generator.generateClass(ClassVisitor)
1、 獲取sourceClass的所有public get 方法-》PropertyDescriptor[] getters
2、 獲取TargetClass 的所有 public set 方法-》PropertyDescriptor[] setters
3、 遍歷setters的每一個屬性,執行4和5
4、 按setters的name生成sourceClass的所有setter方法-》PropertyDescriptor getter【不符合javabean規范的類將會可能出現空指針異常】
5、 PropertyDescriptor[] setters-》PropertyDescriptor setter
6、 將setter和getter名字和類型 配對,生成代理類的拷貝方法。
Copy屬性過程:調用生成的代理類,代理類的代碼和手工操作的代碼很類似,效率非常高。
Apache BeanUtils.copyProperties會進行類型轉換,而Apache PropertyUtils.copyProperties不會。 既然進行了類型轉換,那BeanUtils.copyProperties的速度比不上PropertyUtils.copyProperties。我們從上面的實踐中得到了驗證。
三、注意事項
注意事項 | 是否支持擴展useConvete功能 | 相同屬性名,且類型不匹配時候的處理 | Set和Get方法不匹配的處理 | 對於空字段的處理 | |
manualCopy | ---- | ---- | ---- | ---- | |
cglib | 支持 | 只拷貝名稱和類型都相同的屬性, 名稱相同而類型不同的屬性不會被拷貝 |
OK | 包裝類型未設置值字段,默認設置null |
|
springBeanUtils | 支持 | 能正常拷貝並進行初級轉換,Long和Integer互轉 |
OK | ||
apachePropertyUtils | 不支持 | 異常 java.lang.IllegalArgumentException: argument type mismatch | OK | ||
apacheBeanUtils | 支持 | 能正常拷貝並進行初級轉換,Long和Integer互轉 | OK |
對於未設置值的field字段,無論是基本數據類型還是包裝類型、集合的值都是各自的默認值。
四、總結
1、追求高效率的屬性拷貝請使用手工設置屬性(set)
2、在使用工具進行屬性拷貝時,要注意程序的健壯性即日期Date、各種類型的變量的初始值。
參考:http://grefr.iteye.com/blog/1880008
由於本人經驗有限,文章中難免會有錯誤,請瀏覽文章的您指正或有不同的觀點共同探討!