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

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

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

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

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

問:下圖這樣?

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

問:沒,我只是舉個例子

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

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

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

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

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

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

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

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

問:你這程式碼報錯啊,避免用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都要進行多次型別檢查,還要列印日誌。

/**
 * org.apache.commons.beanutils.BeanUtils.copyProperties方法原始碼解析
 * @author 哪吒程式設計
 * @time 2023-01-07
 */
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) {
            Map<String, Object> propMap = (Map)orig;
            Iterator var13 = propMap.entrySet().iterator();

            while(var13.hasNext()) {
                Map.Entry<String, Object> entry = (Map.Entry)var13.next();
                String name = (String)entry.getKey();
                if (this.getPropertyUtils().isWriteable(dest, name)) {
                    this.copyProperty(dest, name, entry.getValue());
                }
            }
        } else {
            PropertyDescriptor[] origDescriptors = this.getPropertyUtils().getPropertyDescriptors(orig);
            PropertyDescriptor[] var14 = origDescriptors;
            var5 = origDescriptors.length;

            for(var6 = 0; var6 < var5; ++var6) {
                PropertyDescriptor origDescriptor = var14[var6];
                name = origDescriptor.getName();
                if (!"class".equals(name) && this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {
                    try {
                        value = this.getPropertyUtils().getSimpleProperty(orig, name);
                        this.copyProperty(dest, name, value);
                    } catch (NoSuchMethodException var10) {
                    }
                }
            }
        }

    }
}

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

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

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

答:當然有,據我瞭解有 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,我來演示一下

package com.nezha.copy;

import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
import org.springframework.cglib.beans.BeanCopier;
import org.springframework.util.StopWatch;

public class Test {

    public static void main(String[] args) {
        User user = new User();
        user.setUserId("1");
        user.setUserName("哪吒程式設計");
        user.setCardId("123");
        user.setCreateTime("2023-01-03");
        user.setEmail("666666666@qq.com");
        user.setOperate("哪吒");
        user.setOrgId("46987916");
        user.setPassword("123456");
        user.setPhone("10086");
        user.setRemark("456");
        user.setSex(1);
        user.setStatus("1");
        user.setTel("110");
        user.setType("0");
        user.setUpdateTime("2023-01-05");

        User target = new User();
        int sum = 10000000;
        apacheBeanUtilsCopyTest(user,target,sum);
        commonsPropertyCopyTest(user,target,sum);
        cglibBeanCopyTest(user,target,sum);
        springBeanCopyTest(user,target,sum);
    }

    private static void apacheBeanUtilsCopyTest(User source, User target, int sum) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        for (int i = 0; i < sum; i++) {
            apacheBeanUtilsCopy(source,target);
        }
        stopWatch.stop();
        System.out.println("使用org.apache.commons.beanutils.BeanUtils方式賦值"+sum+"個user物件,耗時:"+stopWatch.getLastTaskTimeMillis()+"毫秒");
    }

    /**
     * org.apache.commons.beanutils.BeanUtils方式
     */
    private static void apacheBeanUtilsCopy(User source, User target) {
        try {
            BeanUtils.copyProperties(source, target);
        } catch (Exception e) {
        }
    }

    private static void commonsPropertyCopyTest(User source, User target, int sum) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        for (int i = 0; i < sum; i++) {
            commonsPropertyCopy(source,target);
        }
        stopWatch.stop();
        System.out.println("使用org.apache.commons.beanutils.PropertyUtils方式賦值"+sum+"個user物件,耗時:"+stopWatch.getLastTaskTimeMillis()+"毫秒");
    }

    /**
     * org.apache.commons.beanutils.PropertyUtils方式
     */
    private static void commonsPropertyCopy(User source, User target) {
        try {
            PropertyUtils.copyProperties(target, source);
        } catch (Exception e) {
        }
    }

    private static void cglibBeanCopyTest(User source, User target, int sum) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        for (int i = 0; i < sum; i++) {
            cglibBeanCopy(source,target);
        }
        stopWatch.stop();
        System.out.println("使用org.springframework.cglib.beans.BeanCopier方式賦值"+sum+"個user物件,耗時:"+stopWatch.getLastTaskTimeMillis()+"毫秒");
    }

    /**
     * org.springframework.cglib.beans.BeanCopier方式
     */
    static BeanCopier copier = BeanCopier.create(User.class, User.class, false);
    private static void cglibBeanCopy(User source, User target) {
        copier.copy(source, target, null);
    }

    private static void springBeanCopyTest(User source, User target, int sum) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        for (int i = 0; i < sum; i++) {
            springBeanCopy(source,target);
        }
        stopWatch.stop();
        System.out.println("使用org.springframework.beans.BeanUtils.copyProperties方式賦值"+sum+"個user物件,耗時:"+stopWatch.getLastTaskTimeMillis()+"毫秒");
    }

    /**
     * org.springframework.beans.BeanUtils.copyProperties方式
     */
    private static void springBeanCopy(User source, User target) {
        org.springframework.beans.BeanUtils.copyProperties(source, target);
    }
}

"四大金剛" 效能統計

方法 1000 10000 100000 1000000
apache BeanUtils 906毫秒 807毫秒 1892毫秒 11049毫秒
apache PropertyUtils 17毫秒 96毫秒 648毫秒 5896毫秒
spring cglib BeanCopier 0毫秒 1毫秒 3毫秒 10毫秒
spring copyProperties 87毫秒 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 提供了可以動態修改實現他的類的屬性名稱、屬性值、屬性型別的功能。

/**
 * org.apache.commons.beanutils.PropertyUtils方式原始碼解析
 * @author 哪吒程式設計
 * @time 2023-01-07
 */
public void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    // 判斷資料來源和目標物件不是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)) {
                    try {
                        // 獲取對應的值
                        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);
                        }
                    } catch (NoSuchMethodException var12) {
                        if (this.log.isDebugEnabled()) {
                            this.log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", var12);
                        }
                    }
                }
            }
        } else if (orig instanceof Map) {
            Iterator entries = ((Map)orig).entrySet().iterator();

            while(true) {
                Map.Entry entry;
                String name;
                do {
                    if (!entries.hasNext()) {
                        return;
                    }

                    entry = (Map.Entry)entries.next();
                    name = (String)entry.getKey();
                } while(!this.isWriteable(dest, name));

                try {
                    if (dest instanceof DynaBean) {
                        ((DynaBean)dest).set(name, entry.getValue());
                    } else {
                        this.setSimpleProperty(dest, name, entry.getValue());
                    }
                } catch (NoSuchMethodException var11) {
                    if (this.log.isDebugEnabled()) {
                        this.log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", var11);
                    }
                }
            }
        } else {
            PropertyDescriptor[] origDescriptors = this.getPropertyDescriptors(orig);
            PropertyDescriptor[] var16 = origDescriptors;
            var5 = origDescriptors.length;

            for(var6 = 0; var6 < var5; ++var6) {
                PropertyDescriptor origDescriptor = var16[var6];
                name = origDescriptor.getName();
                if (this.isReadable(orig, name) && this.isWriteable(dest, name)) {
                    try {
                        value = this.getSimpleProperty(orig, name);
                        if (dest instanceof DynaBean) {
                            ((DynaBean)dest).set(name, value);
                        } else {
                            this.setSimpleProperty(dest, name, value);
                        }
                    } catch (NoSuchMethodException var10) {
                        if (this.log.isDebugEnabled()) {
                            this.log.debug("Error writing to '" + name + "' on class '" + dest.getClass() + "'", var10);
                        }
                    }
                }
            }
        }

    }
}

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

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

Spring copyProperties 原始碼分析

  1. 判斷資料來源和目標物件的非空判斷改為了斷言;
  2. 每次copy沒有日誌記錄;
  3. 沒有if (orig instanceof DynaBean) {這個型別檢查;
  4. 增加了放開許可權的步驟;
/**
 * org.springframework.beans.BeanUtils.copyProperties方法原始碼解析
 * @author 哪吒程式設計
 * @time 2023-01-07
 */
private static void copyProperties(Object source, Object target, @Nullable Class<?> editable,
                                   @Nullable String... ignoreProperties) throws BeansException {

    // 判斷資料來源和目標物件不是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())) {
                    try {
                        // 放開讀取方法的許可權
                        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);
                    }
                    catch (Throwable ex) {
                        throw new FatalBeanException(
                                "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
                    }
                }
            }
        }
    }
}

總結

阿里的友情提示,避免用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. 增加了放開許可權的步驟;

相關文章