焦點關(guān)注:避免用Apache Beanutils進行屬性的copy。why?讓我們一起一探究竟

2023-01-13 10:21:32 來源:51CTO博客

在實際的項目開發(fā)中,對象間賦值普遍存在,隨著雙十一、秒殺等電商過程愈加復(fù)雜,數(shù)據(jù)量也在不斷攀升,效率問題,浮出水面。

問:如果是你來寫對象間賦值的代碼,你會怎么做?

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

問:下圖這樣?


【資料圖】

答:對啊,你怎么能把我的代碼放到網(wǎng)上?

問:沒,我只是舉個例子

答:這涉及到商業(yè)機密,是很嚴(yán)重的問題

問:我發(fā)現(xiàn)你挺能扯皮啊,直接回答問題行嗎?

答:OK,OK,我也覺得這樣寫很low,上次這么寫之后,差點挨打
對象太多,ctrl c + strl v,鍵盤差點沒敲壞;而且很容易出錯,一不留神,屬性沒對應(yīng)上,賦錯值了;代碼看起來很傻缺,一個類好幾千行,全是get、set復(fù)制,還起個了自以為很優(yōu)雅的名字transfer;如果屬性名不能見名知意,還得加上每個屬性的含義注釋(基本這種賦值操作,都是要加的,注釋很重要,注釋很重要,注釋很重要);代碼維護起來很麻煩;如果對象過多,會產(chǎn)生類爆炸問題,如果屬性過多,會嚴(yán)重違背阿里巴巴代碼規(guī)約(一個方法的實際代碼最多20行);

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

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

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

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

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

答:沒報錯,只是嚴(yán)重警告而已,??代碼能跑就行,有問題再優(yōu)化唄??

問:你這什么態(tài)度?人事在哪劃拉的人,為啥會出現(xiàn)嚴(yán)重警告?

答:拿多少錢,干多少活,我又不是XXX,應(yīng)該是性能問題吧

問:具體什么原因?qū)е碌哪兀?/p>

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

通過單例模式調(diào)用??copyProperties??,但是,每一個方法對應(yīng)一個??BeanUtilsBean.getInstance()??實例,每一個類實例對應(yīng)一個實例,這不算一個真正的單例模式。

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 提供了可以動態(tài)修改實現(xiàn)他的類的屬性名稱、屬性值、屬性類型的功能        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();                // 判斷源對象是否可讀、判斷目標(biāo)對象是否可寫                if (this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) {                    // 獲取對應(yīng)的值                    value = ((DynaBean)orig).get(name);                    // 每個屬性都調(diào)用一次copyProperty                    this.copyProperty(dest, name, value);                }            }        } else if (orig instanceof Map) {            Map propMap = (Map)orig;            Iterator var13 = propMap.entrySet().iterator();            while(var13.hasNext()) {                Map.Entry 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??赫然在列,穩(wěn)居耗時Top1。

問:還有其它好的方式嗎?性能好一點的
答:當(dāng)然有,據(jù)我了解有 4 種工具類,實際上,可能會有更多,話不多說,先簡單介紹一下。
org.apache.commons.beanutils.BeanUtils;org.apache.commons.beanutils.PropertyUtils;org.springframework.cglib.beans.BeanCopier;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);    }}
"四大金剛" 性能統(tǒng)計

方法

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?? 優(yōu)于 ??spring copyProperties?? 優(yōu)于 ??apache PropertyUtils?? 優(yōu)于 ??apache BeanUtils??

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

Apache PropertyUtils 源碼分析

從源碼可以清晰的看到,類型檢查變成了非空校驗,去掉了每一次copy的日志記錄,性能肯定更好了。

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

DanyBean 提供了可以動態(tài)修改實現(xiàn)他的類的屬性名稱、屬性值、屬性類型的功能。

/** * org.apache.commons.beanutils.PropertyUtils方式源碼解析 * @author 哪吒編程 * @time 2023-01-07 */public void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {    // 判斷數(shù)據(jù)源和目標(biāo)對象不是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();                // 判斷源對象是否可讀、判斷目標(biāo)對象是否可寫                if (this.isReadable(orig, name) && this.isWriteable(dest, name)) {                    try {                        // 獲取對應(yīng)的值                        value = ((DynaBean)orig).get(name);                        // 相對于org.apache.commons.beanutils.BeanUtils.copyProperties此處有優(yōu)化                        // DanyBean 提供了可以動態(tài)修改實現(xiàn)他的類的屬性名稱、屬性值、屬性類型的功能                        if (dest instanceof DynaBean) {                            ((DynaBean)dest).set(name, value);                        } else {                            // 每個屬性都調(diào)用一次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 源碼分析

判斷數(shù)據(jù)源和目標(biāo)對象的非空判斷改為了斷言;每次copy沒有日志記錄;沒有??if (orig instanceof DynaBean) {??這個類型檢查;增加了放開權(quán)限的步驟;
/** * 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 {    // 判斷數(shù)據(jù)源和目標(biāo)對象不是null    Assert.notNull(source, "Source must not be null");    Assert.notNull(target, "Target must not be null");    /**     * 若target設(shè)置了泛型,則默認(rèn)使用泛型     * 若是 editable 是 null,則此處忽略     * 一般情況下editable都默認(rèn)為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 ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null);    for (PropertyDescriptor targetPd : targetPds) {        Method writeMethod = targetPd.getWriteMethod();        // 目標(biāo)對象存在寫入方法、屬性不被忽略        if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {            PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());            if (sourcePd != null) {                Method readMethod = sourcePd.getReadMethod();                /**                 * 源對象存在讀取方法、數(shù)據(jù)是可復(fù)制的                 * writeMethod.getParameterTypes()[0]:獲取 writeMethod 的第一個入?yún)㈩愋?                * readMethod.getReturnType():獲取 readMethod 的返回值類型                 * 判斷返回值類型和入?yún)㈩愋褪欠翊嬖诶^承關(guān)系,只有是繼承關(guān)系或相等的情況下,才會進行注入                 */                if (readMethod != null &&                        ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {                    try {                        // 放開讀取方法的權(quán)限                        if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {                            readMethod.setAccessible(true);                        }                        // 通過反射獲取值                        Object value = readMethod.invoke(source);                        // 放開寫入方法的權(quán)限                        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);                    }                }            }        }    }}

總結(jié)

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

??Apache Beanutils?? 的性能問題出現(xiàn)在類型校驗和每一次copy的日志記錄;

Apache PropertyUtils 進行了如下優(yōu)化:
類型檢查變成了非空校驗去掉了每一次copy的日志記錄實際賦值的地方由copyProperty變成了DanyBean + setSimpleProperty;
Spring copyProperties 進行了如下優(yōu)化:
判斷數(shù)據(jù)源和目標(biāo)對象的非空判斷改為了斷言;每次copy沒有日志記錄;沒有??if (orig instanceof DynaBean) {??這個類型檢查;增加了放開權(quán)限的步驟;

標(biāo)簽: 目標(biāo)對象 單例模式 是否可寫

上一篇:ip查詢地址要怎么做(怎么看別人的IP地址)
下一篇:焦點資訊:FPGA:邏輯函數(shù)的代數(shù)法化簡