爲何阿里代碼規約要求避免使用 Apache BeanUtils 進行屬性的拷貝

聲明:本文屬原創文章,始發於公號:程序員自學之道,並同步發佈於 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);
        }
    }

具體的性能和源碼分析,能夠參考這幾篇文章:

One more thing

除了性能問題以外,在使用 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 值致使的。

相關文章
相關標籤/搜索