避免用Apache Beanutils進行屬性的copy。why?讓我們一起一探究竟

哪吒程式設計發表於2023-01-13

大家好,我是哪吒。

今天,透過程式碼例項、原始碼解讀、四大工具類橫向對比的方式,和大家一起聊一聊物件賦值的問題。

在實際的專案開發中,物件間賦值普遍存在,隨著雙十一、秒殺等電商過程愈加複雜,資料量也在不斷攀升,效率問題,浮出水面。

問:如果是你來寫物件間賦值的程式碼,你會怎麼做?

答:想都不用想,直接程式碼走起來,get、set即可。

問:下圖這樣?

避免用Apache Beanutils進行屬性的copy。why?讓我們一起一探究竟

答:對啊,你怎麼能把我的程式碼放到網上?

問:沒,我只是舉個例子

答:這涉及到商業機密,是很嚴重的問題

問:我發現你挺能扯皮啊,直接回答問題行嗎?

答:OK,OK,我也覺得這樣寫很low,上次這麼寫之後,差點捱打

  1. 物件太多,ctrl c + strl v,鍵盤差點沒敲壞;
  2. 而且很容易出錯,一不留神,屬性沒對應上,賦錯值了;
  3. 程式碼看起來很傻缺,一個類好幾千行,全是get、set複製,還起個了自以為很優雅的名字transfer;
  4. 如果屬性名不能見名知意,還得加上每個屬性的含義註釋(基本這種賦值操作,都是要加的,註釋很重要,註釋很重要,註釋很重要);
  5. 程式碼維護起來很麻煩;
  6. 如果物件過多,會產生類爆炸問題,如果屬性過多,會嚴重違背阿里巴巴程式碼規約(一個方法的實際程式碼最多20行);

問:行了,行了,說說,怎麼解決吧。

答:很簡單啊,可以透過工具類Beanutils直接賦值啊

問:我聽說工具類最近很卷,你用的哪個啊?

答:就Apache自帶的那個啊,賊簡單。我手寫一個,給你欣賞一下。

避免用Apache Beanutils進行屬性的copy。why?讓我們一起一探究竟

問:你這程式碼報錯啊,避免用Apache Beanutils進行屬性的copy。

答:沒報錯,只是嚴重警告而已,程式碼能跑就行,有問題再最佳化唄

問:你這什麼態度?人事在哪劃拉的人,為啥會出現嚴重警告?

答:拿多少錢,幹多少活,我又不是XXX,應該是效能問題吧

問:具體什麼原因導致的呢?

答:3000塊錢還得手撕一下 apache copyProperties 的原始碼唄?

透過單例模式呼叫copyProperties,但是,每一個方法對應一個BeanUtilsBean.getInstance()例項,每一個類例項對應一個例項,這不算一個真正的單例模式。

public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
 BeanUtilsBean.getInstance().copyProperties(dest, orig);
}

效能瓶頸 --> 日誌太多也是病

透過原始碼可以看到,每一個copyProperties都要進行多次型別檢查,還要列印日誌。

public void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
    // 型別檢查
    if (dest == null) {
        throw new IllegalArgumentException("No destination bean specified");
    } else if (orig == null) {
        throw new IllegalArgumentException("No origin bean specified");
    } else {
        // 列印日誌
        if (this.log.isDebugEnabled()) {
            this.log.debug("BeanUtils.copyProperties(" + dest + ", " + orig + ")");
        }

        int var5;
        int var6;
        String name;
        Object value;
        // 型別檢查
        // DanyBean 提供了可以動態修改實現他的類的屬性名稱、屬性值、屬性型別的功能
        if (orig instanceof DynaBean) {
            // 獲取源物件所有屬性
            DynaProperty[] origDescriptors = ((DynaBean)orig).getDynaClass().getDynaProperties();
            DynaProperty[] var4 = origDescriptors;
            var5 = origDescriptors.length;

            for(var6 = 0; var6 < var5; ++var6) {
                DynaProperty origDescriptor = var4[var6];
                // 獲取源物件屬性名
                name = origDescriptor.getName();
                // 判斷源物件是否可讀、判斷目標物件是否可寫
                if (this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {
                    // 獲取對應的值
                    value = ((DynaBean)orig).get(name);
                    // 每個屬性都呼叫一次copyProperty
                    this.copyProperty(dest, name, value);
                }
            }
        } else if (orig instanceof Map) {
            ...
        } else {
            ...
        }

    }
}

透過 jvisualvm.exe 檢測程式碼效能

再透過jvisualvm.exe檢測一下執行情況,果然,logging.log4j赫然在列,穩居耗時Top1。

避免用Apache Beanutils進行屬性的copy。why?讓我們一起一探究竟

問:還有其它好的方式嗎?效能好一點的

答:當然有,據我瞭解有 4 種工具類,實際上,可能會有更多,話不多說,先簡單介紹一下。

  1. org.apache.commons.beanutils.BeanUtils;
  2. org.apache.commons.beanutils.PropertyUtils;
  3. org.springframework.cglib.beans.BeanCopier;
  4. org.springframework.beans.BeanUtils;

問:那你怎麼不用?

答:OK,我來演示一下

public class Test {

    private static void apacheBeanUtilsCopyTest(User source, User target, int sum) {
        for (int i = 0; i < sum; i++) {
            org.apache.commons.beanutils.BeanUtils.copyProperties(source, target);
        }
    }

    private static void commonsPropertyCopyTest(User source, User target, int sum) {
        for (int i = 0; i < sum; i++) {
            org.apache.commons.beanutils.PropertyUtils.copyProperties(target, source);
        }
    }

    static BeanCopier copier = BeanCopier.create(User.classUser.classfalse);
    private static void cglibBeanCopyTest(User source, User target, int sum) {
        for (int i = 0; i < sum; i++) {
            org.springframework.cglib.beans.BeanCopier.copier.copy(source, target, null);
        }
    }

    private static void springBeanCopy(User source, User target, int sum) {
        for (int i = 0; i < sum; i++) {
            org.springframework.beans.BeanUtils.copyProperties(source, target);
        }
    }
}

"四大金剛" 效能統計

方法1000100001000001000000
apache BeanUtils906毫秒807毫秒1892毫秒11049毫秒
apache PropertyUtils17毫秒96毫秒648毫秒5896毫秒
spring cglib BeanCopier0毫秒1毫秒3毫秒10毫秒
spring copyProperties87毫秒90毫秒123毫秒482毫秒

不測不知道,一測嚇一跳,差的還真的多。

spring cglib BeanCopier效能最好,apache BeanUtils效能最差。

效能走勢 --> spring cglib BeanCopier 優於 spring copyProperties 優於 apache PropertyUtils 優於 apache BeanUtils

避免用Apache Beanutils進行屬性的copy的問題 上面分析完了,下面再看看其它的方法做了哪些最佳化。

Apache PropertyUtils 原始碼分析

從原始碼可以清晰的看到,型別檢查變成了非空校驗,去掉了每一次copy的日誌記錄,效能肯定更好了。

  1. 型別檢查變成了非空校驗
  2. 去掉了每一次copy的日誌記錄
  3. 實際賦值的地方由copyProperty變成了DanyBean  + setSimpleProperty;

DanyBean 提供了可以動態修改實現他的類的屬性名稱、屬性值、屬性型別的功能。

public void copyProperties(Object dest, Object orig) {
    // 判斷資料來源和目標物件不是null
    if (dest == null) {
        throw new IllegalArgumentException("No destination bean specified");
    } else if (orig == null) {
        throw new IllegalArgumentException("No origin bean specified");
    } else {
        // 刪除了org.apache.commons.beanutils.BeanUtils.copyProperties中最為耗時的log日誌記錄
        int var5;
        int var6;
        String name;
        Object value;
        // 型別檢查
        if (orig instanceof DynaBean) {
            // 獲取源物件所有屬性
            DynaProperty[] origDescriptors = ((DynaBean)orig).getDynaClass().getDynaProperties();
            DynaProperty[] var4 = origDescriptors;
            var5 = origDescriptors.length;

            for(var6 = 0; var6 < var5; ++var6) {
                DynaProperty origDescriptor = var4[var6];
                // 獲取源物件屬性名
                name = origDescriptor.getName();
                // 判斷源物件是否可讀、判斷目標物件是否可寫
                if (this.isReadable(orig, name) && this.isWriteable(dest, name)) {
                    // 獲取對應的值
                    value = ((DynaBean)orig).get(name);
                    // 相對於org.apache.commons.beanutils.BeanUtils.copyProperties此處有最佳化
                    // DanyBean 提供了可以動態修改實現他的類的屬性名稱、屬性值、屬性型別的功能
                    if (dest instanceof DynaBean) {
                        ((DynaBean)dest).set(name, value);
                    } else {
                        // 每個屬性都呼叫一次copyProperty
                        this.setSimpleProperty(dest, name, value);
                    }
                }
            }
        } else if (orig instanceof Map) {
            ...
        } else {
            ...
        }

    }
}

透過 jvisualvm.exe 檢測程式碼效能

再透過jvisualvm.exe檢測一下執行情況,果然,logging.log4j沒有了,其他的基本不變。

避免用Apache Beanutils進行屬性的copy。why?讓我們一起一探究竟

Spring copyProperties 原始碼分析

  1. 判斷資料來源和目標物件的非空判斷改為了斷言;
  2. 每次copy沒有日誌記錄;
  3. 沒有if (orig instanceof DynaBean) {這個型別檢查;
  4. 增加了放開許可權的步驟;
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
                                   @Nullable String... ignoreProperties)
 
{

    // 判斷資料來源和目標物件不是null
    Assert.notNull(source, "Source must not be null");
    Assert.notNull(target, "Target must not be null");

    /**
     * 若target設定了泛型,則預設使用泛型
     * 若是 editable 是 null,則此處忽略
     * 一般情況下editable都預設為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;
    }

    // 獲取target中全部的屬性描述
    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();
                /**
                 * 源物件存在讀取方法、資料是可複製的
                 * writeMethod.getParameterTypes()[0]:獲取 writeMethod 的第一個入參型別
                 * readMethod.getReturnType():獲取 readMethod 的返回值型別
                 * 判斷返回值型別和入參型別是否存在繼承關係,只有是繼承關係或相等的情況下,才會進行注入
                 */

                if (readMethod != null &&
                        ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
                    // 放開讀取方法的許可權
                    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);
                }
            }
        }
    }
}

總結

阿里的友情提示,避免用Apache Beanutils進行物件的copy,還是很有道理的。

Apache Beanutils 的效能問題出現在型別校驗和每一次copy的日誌記錄;

Apache PropertyUtils 進行了如下最佳化:

  1. 型別檢查變成了非空校驗
  2. 去掉了每一次copy的日誌記錄
  3. 實際賦值的地方由copyProperty變成了DanyBean  + setSimpleProperty;

Spring copyProperties 進行了如下最佳化:

  1. 判斷資料來源和目標物件的非空判斷改為了斷言;
  2. 每次copy沒有日誌記錄;
  3. 沒有if (orig instanceof DynaBean) {這個型別檢查;
  4. 增加了放開許可權的步驟;


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2931879/,如需轉載,請註明出處,否則將追究法律責任。

相關文章