聲明:本文屬原創文章,始發於公號:程序員自學之道,並同步發佈於 https://blog.csdn.net/dadiyang,特此,同步發佈到 sf,轉載請註明出處。java
有一次開發過程當中,恰好看到一個小夥伴在調用 set 方法,將一個數據庫中查詢出來的 PO 對象的屬性拷貝到 Vo 對象中,相似這樣:程序員
能夠看出,Po 和 Vo 兩個類的字段絕大部分是同樣的,咱們一個個地調用 set 方法只是作了一些重複的冗長的操做。這種操做很是容易出錯,由於對象的屬性太多,有可能會漏掉一兩個,並且肉眼很難察覺。spring
相似這樣的操做,咱們能夠很容易想到,能夠經過反射來解決。其實,如此廣泛通用的功能,一個 BeanUtils 工具類就能夠搞定了。數據庫
因而我建議這位小夥伴使用了 Apache BeanUtils.copyProperties 進行屬性拷貝,這爲咱們的程序挖了一個坑!apache
當咱們開啓阿里代碼掃描插件時,若是你使用了 Apache BeanUtils.copyProperties
進行屬性拷貝,它會給你一個很是嚴重的警告。由於,Apache BeanUtils性能較差,可使用 Spring BeanUtils 或者 Cglib BeanCopier 來代替。markdown
看到這樣的警告,有點讓人有點不爽。大名鼎鼎的 Apache 提供的包,竟然會存在性能問題,以至於阿里給出了嚴重的警告。app
那麼,這個性能問題到底是有多嚴重呢?畢竟,在咱們的應用場景中,若是隻是很微小的性能損耗,可是能帶來很是大的便利性,仍是能夠接受的。框架
帶着這個問題。咱們來作一個實驗,驗證一下。ide
若是對具體的測試方式沒有興趣,能夠跳過直接看結果哦~工具
首先,爲了測試方便,讓咱們來定義一個接口,並將幾種實現統一塊兒來:
public interface PropertiesCopier { void copyProperties(Object source, Object target) throws Exception; } public class CglibBeanCopierPropertiesCopier implements PropertiesCopier { @Override public void copyProperties(Object source, Object target) throws Exception { BeanCopier copier = BeanCopier.create(source.getClass(), target.getClass(), false); copier.copy(source, target, null); } } // 全局靜態 BeanCopier,避免每次都生成新的對象 public class StaticCglibBeanCopierPropertiesCopier implements PropertiesCopier { private static BeanCopier copier = BeanCopier.create(Account.class, Account.class, false); @Override public void copyProperties(Object source, Object target) throws Exception { copier.copy(source, target, null); } } public class SpringBeanUtilsPropertiesCopier implements PropertiesCopier { @Override public void copyProperties(Object source, Object target) throws Exception { org.springframework.beans.BeanUtils.copyProperties(source, target); } } public class CommonsBeanUtilsPropertiesCopier implements PropertiesCopier { @Override public void copyProperties(Object source, Object target) throws Exception { org.apache.commons.beanutils.BeanUtils.copyProperties(target, source); } } public class CommonsPropertyUtilsPropertiesCopier implements PropertiesCopier { @Override public void copyProperties(Object source, Object target) throws Exception { org.apache.commons.beanutils.PropertyUtils.copyProperties(target, source); } }
而後寫一個參數化的單元測試:
@RunWith(Parameterized.class) public class PropertiesCopierTest { @Parameterized.Parameter(0) public PropertiesCopier propertiesCopier; // 測試次數 private static List<Integer> testTimes = Arrays.asList(100, 1000, 10_000, 100_000, 1_000_000); // 測試結果以 markdown 表格的形式輸出 private static StringBuilder resultBuilder = new StringBuilder("|實現|100|1,000|10,000|100,000|1,000,000|\n").append("|----|----|----|----|----|----|\n"); @Parameterized.Parameters public static Collection<Object[]> data() { Collection<Object[]> params = new ArrayList<>(); params.add(new Object[]{new StaticCglibBeanCopierPropertiesCopier()}); params.add(new Object[]{new CglibBeanCopierPropertiesCopier()}); params.add(new Object[]{new SpringBeanUtilsPropertiesCopier()}); params.add(new Object[]{new CommonsPropertyUtilsPropertiesCopier()}); params.add(new Object[]{new CommonsBeanUtilsPropertiesCopier()}); return params; } @Before public void setUp() throws Exception { String name = propertiesCopier.getClass().getSimpleName().replace("PropertiesCopier", ""); resultBuilder.append("|").append(name).append("|"); } @Test public void copyProperties() throws Exception { Account source = new Account(1, "test1", 30D); Account target = new Account(); // 預熱一次 propertiesCopier.copyProperties(source, target); for (Integer time : testTimes) { long start = System.nanoTime(); for (int i = 0; i < time; i++) { propertiesCopier.copyProperties(source, target); } resultBuilder.append((System.nanoTime() - start) / 1_000_000D).append("|"); } resultBuilder.append("\n"); } @AfterClass public static void tearDown() throws Exception { System.out.println("測試結果:"); System.out.println(resultBuilder); } }
時間單位毫秒
實現 | 100次 | 1,000次 | 10,000次 | 100,000次 | 1,000,000次 |
---|---|---|---|---|---|
StaticCglibBeanCopier | 0.055022 | 0.541029 | 0.999478 | 2.754824 | 9.88556 |
CglibBeanCopier | 5.320798 | 11.086323 | 61.037446 | 72.484607 | 333.384007 |
SpringBeanUtils | 5.180483 | 21.328542 | 30.021662 | 103.266375 | 966.439272 |
CommonsPropertyUtils | 9.729159 | 42.927356 | 74.063789 | 386.127787 | 1955.5437 |
CommonsBeanUtils | 24.99513 | 170.728558 | 572.335327 | 2970.3068 | 27563.3459 |
結果代表,Cglib 的 BeanCopier 的拷貝速度是最快的,即便是百萬次的拷貝也只須要 10 毫秒!
相比而言,最差的是 Commons 包的 BeanUtils.copyProperties 方法,100 次拷貝測試與表現最好的 Cglib 相差 400 倍之多。百萬次拷貝更是出現了 2800 倍的性能差別!
結果然是讓人大跌眼鏡。
可是它們爲何會有這麼大的差別呢?
查看源碼,咱們會發現 CommonsBeanUtils 主要有如下幾個耗時的地方:
public void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException { // 類型檢查 if (orig instanceof DynaBean) { ... } else if (orig instanceof Map) { ... } else { final PropertyDescriptor[] origDescriptors = ... for (PropertyDescriptor origDescriptor : origDescriptors) { ... // 這裏每一個屬性都調一次 copyProperty copyProperty(dest, name, value); } } } public void copyProperty(final Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException { ... // 這裏又進行一次類型檢查 if (target instanceof DynaBean) { ... } ... // 須要將屬性轉換爲目標類型 value = convertForCopy(value, type); ... } // 而這個 convert 方法在日誌級別爲 debug 的時候有不少的字符串拼接 public <T> T convert(final Class<T> type, Object value) { if (log().isDebugEnabled()) { log().debug("Converting" + (value == null ? "" : " '" + toString(sourceType) + "'") + " value '" + value + "' to type '" + toString(targetType) + "'"); } ... if (targetType.equals(String.class)) { return targetType.cast(convertToString(value)); } else if (targetType.equals(sourceType)) { if (log().isDebugEnabled()) { log().debug("No conversion required, value is already a " + toString(targetType)); } return targetType.cast(value); } else { // 這個 convertToType 方法裏也須要作類型檢查 final Object result = convertToType(targetType, value); if (log().isDebugEnabled()) { log().debug("Converted to " + toString(targetType) + " value '" + result + "'"); } return targetType.cast(result); } }
具體的性能和源碼分析,能夠參考這幾篇文章:
除了性能問題以外,在使用 CommonsBeanUtils 時還有其餘的坑須要特別當心!
在進行屬性拷貝時,雖然 CommonsBeanUtils 默認不會給原始包裝類賦默認值的,可是在使用低版本(1.8.0及如下)的時候,若是你的類有 Date 類型屬性,並且來源對象中該屬性值爲 null 的話,就會發生異常:
org.apache.commons.beanutils.ConversionException: No value specified for 'Date'
解決這個問題的辦法是註冊一個 DateConverter:
ConvertUtils.register(new DateConverter(null), java.util.Date.class);
然而這個語句,會致使包裝類型會被賦予原始類型的默認值,如 Integer 屬性默認賦值爲 0,儘管你的來源對象該字段的值爲 null。
在高版本(1.9.3)中,日期 null 值的問題和包裝類賦默認值的問題都被修復了。
這個在咱們的包裝類屬性爲 null 值時有特殊含義的場景,很是容易踩坑!例如搜索條件對象,通常 null 值表示該字段不作限制,而 0 表示該字段的值必須爲0。
當咱們看到阿里的提示,或者你看了這篇文章以後,知道了 CommonsBeanUtils 的性能問題,想要改用 Spring 的 BeanUtils 時,要當心:
org.apache.commons.beanutils.BeanUtils.copyProperties(Object target, Object source); org.springframework.beans.BeanUtils.copyProperties(Object source, Object target);
從方法簽名上能夠看出,這兩個工具類的名稱相同,方法名也相同,甚至連參數個數、類型、名稱都相同。可是參數的位置是相反的。所以,若是你想更改的時候,千萬要記得,將 target 和 source 兩個參數也調換過來!
另外,可能因爲種種緣由,你獲取的堆棧信息不完整找不到問題在哪,因此這裏順便提醒一下:
若是你遇到 java.lang.IllegalArgumentException: Source must not be null
或者 java.lang.IllegalArgumentException: Target must not be null
這樣的異常信息卻處處找不到緣由時,不用找了,這是因爲你在 copyProperties 的時候傳了 null 值致使的。