如何避免Apache?Beanutils屬性copy
引言
在實(shí)際的項(xiàng)目開(kāi)發(fā)中,對(duì)象間賦值普遍存在,隨著雙十一、秒殺等電商過(guò)程愈加復(fù)雜,數(shù)據(jù)量也在不斷攀升,效率問(wèn)題,浮出水面。
問(wèn):如果是你來(lái)寫對(duì)象間賦值的代碼,你會(huì)怎么做?
答:想都不用想,直接代碼走起來(lái),get、set即可。
問(wèn):下圖這樣?
答:對(duì)啊,你怎么能把我的代碼放到網(wǎng)上?
問(wèn):沒(méi),我只是舉個(gè)例子
答:這涉及到商業(yè)機(jī)密,是很嚴(yán)重的問(wèn)題
問(wèn):我發(fā)現(xiàn)你挺能扯皮啊,直接回答問(wèn)題行嗎?
答:OK,OK,我也覺(jué)得這樣寫很low,上次這么寫之后,差點(diǎn)挨打
- 對(duì)象太多,ctrl c + strl v,鍵盤差點(diǎn)沒(méi)敲壞;
- 而且很容易出錯(cuò),一不留神,屬性沒(méi)對(duì)應(yīng)上,賦錯(cuò)值了;
- 代碼看起來(lái)很傻缺,一個(gè)類好幾千行,全是get、set復(fù)制,還起個(gè)了自以為很優(yōu)雅的名字transfer;
- 如果屬性名不能見(jiàn)名知意,還得加上每個(gè)屬性的含義注釋(基本這種賦值操作,都是要加的,注釋很重要,注釋很重要,注釋很重要);
- 代碼維護(hù)起來(lái)很麻煩;
- 如果對(duì)象過(guò)多,會(huì)產(chǎn)生類爆炸問(wèn)題,如果屬性過(guò)多,會(huì)嚴(yán)重違背阿里巴巴代碼規(guī)約(一個(gè)方法的實(shí)際代碼最多20行);
問(wèn):行了,行了,說(shuō)說(shuō),怎么解決吧。
答:很簡(jiǎn)單啊,可以通過(guò)工具類Beanutils直接賦值啊
問(wèn):我聽(tīng)說(shuō)工具類最近很卷,你用的哪個(gè)???
答:就Apache
自帶的那個(gè)啊,賊簡(jiǎn)單。我手寫一個(gè),給你欣賞一下。
問(wèn):你這代碼報(bào)錯(cuò)啊,避免用Apache Beanutils進(jìn)行屬性的copy。
答:沒(méi)報(bào)錯(cuò),只是嚴(yán)重警告而已,代碼能跑就行,有問(wèn)題再優(yōu)化唄
問(wèn):為啥會(huì)出現(xiàn)嚴(yán)重警告?
答:拿多少錢,干多少活,我又不是XXX,應(yīng)該是性能問(wèn)題吧
問(wèn):具體什么原因?qū)е碌哪兀?/p>
答:3000塊錢還得手撕一下 apache copyProperties
的源代碼唄?
通過(guò)單例模式調(diào)用copyProperties
,但是,每一個(gè)方法對(duì)應(yīng)一個(gè)BeanUtilsBean.getInstance()
實(shí)例,每一個(gè)類實(shí)例對(duì)應(yīng)一個(gè)實(shí)例,這不算一個(gè)真正的單例模式。
public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException { BeanUtilsBean.getInstance().copyProperties(dest, orig); }
性能瓶頸 --> 日志太多也是病
通過(guò)源碼可以看到,每一個(gè)copyProperties
都要進(jìn)行多次類型檢查,還要打印日志。
/** * 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 提供了可以動(dòng)態(tài)修改實(shí)現(xiàn)他的類的屬性名稱、屬性值、屬性類型的功能 if (orig instanceof DynaBean) { // 獲取源對(duì)象所有屬性 DynaProperty[] origDescriptors = ((DynaBean)orig).getDynaClass().getDynaProperties(); DynaProperty[] var4 = origDescriptors; var5 = origDescriptors.length; for(var6 = 0; var6 < var5; ++var6) { DynaProperty origDescriptor = var4[var6]; // 獲取源對(duì)象屬性名 name = origDescriptor.getName(); // 判斷源對(duì)象是否可讀、判斷目標(biāo)對(duì)象是否可寫 if (this.getPropertyUtils().isReadable(orig, name) && this.getPropertyUtils().isWriteable(dest, name)) { // 獲取對(duì)應(yīng)的值 value = ((DynaBean)orig).get(name); // 每個(gè)屬性都調(diào)用一次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) { } } } } } }
通過(guò) jvisualvm.exe 檢測(cè)代碼性能
再通過(guò)jvisualvm.exe
檢測(cè)一下運(yùn)行情況,果然,logging.log4j
赫然在列,穩(wěn)居耗時(shí)Top1。
問(wèn):還有其它好的方式嗎?性能好一點(diǎn)的
答:當(dāng)然有,據(jù)我了解有 4 種工具類,實(shí)際上,可能會(huì)有更多,話不多說(shuō),先簡(jiǎn)單介紹一下。
org.apache.commons.beanutils.BeanUtils;
org.apache.commons.beanutils.PropertyUtils;
org.springframework.cglib.beans.BeanCopier;
org.springframework.beans.BeanUtils;
問(wèn):那你怎么不用?
答:OK,我來(lái)演示一下
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+"個(gè)user對(duì)象,耗時(shí):"+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+"個(gè)user對(duì)象,耗時(shí):"+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+"個(gè)user對(duì)象,耗時(shí):"+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+"個(gè)user對(duì)象,耗時(shí):"+stopWatch.getLastTaskTimeMillis()+"毫秒"); } /** * org.springframework.beans.BeanUtils.copyProperties方式 */ private static void springBeanCopy(User source, User target) { org.springframework.beans.BeanUtils.copyProperties(source, target); } }
"四大金剛" 性能統(tǒng)計(jì)
方法 | 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毫秒 |
不測(cè)不知道,一測(cè)嚇一跳,差的還真的多。
spring cglib BeanCopier
性能最好,apache BeanUtils
性能最差。
性能走勢(shì) --> spring cglib BeanCopier
優(yōu)于 spring copyProperties
優(yōu)于 apache PropertyUtils
優(yōu)于 apache BeanUtils
避免用Apache Beanutils進(jìn)行屬性的copy的問(wèn)題 上面分析完了,下面再看看其它的方法做了哪些優(yōu)化。
Apache PropertyUtils 源碼分析
從源碼可以清晰的看到,類型檢查變成了非空校驗(yàn),去掉了每一次copy的日志記錄,性能肯定更好了。
- 類型檢查變成了非空校驗(yàn)
- 去掉了每一次copy的日志記錄
- 實(shí)際賦值的地方由copyProperty變成了DanyBean + setSimpleProperty;
DanyBean 提供了可以動(dòng)態(tài)修改實(shí)現(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)對(duì)象不是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中最為耗時(shí)的log日志記錄 int var5; int var6; String name; Object value; // 類型檢查 if (orig instanceof DynaBean) { // 獲取源對(duì)象所有屬性 DynaProperty[] origDescriptors = ((DynaBean)orig).getDynaClass().getDynaProperties(); DynaProperty[] var4 = origDescriptors; var5 = origDescriptors.length; for(var6 = 0; var6 < var5; ++var6) { DynaProperty origDescriptor = var4[var6]; // 獲取源對(duì)象屬性名 name = origDescriptor.getName(); // 判斷源對(duì)象是否可讀、判斷目標(biāo)對(duì)象是否可寫 if (this.isReadable(orig, name) && this.isWriteable(dest, name)) { try { // 獲取對(duì)應(yīng)的值 value = ((DynaBean)orig).get(name); // 相對(duì)于org.apache.commons.beanutils.BeanUtils.copyProperties此處有優(yōu)化 // DanyBean 提供了可以動(dòng)態(tài)修改實(shí)現(xiàn)他的類的屬性名稱、屬性值、屬性類型的功能 if (dest instanceof DynaBean) { ((DynaBean)dest).set(name, value); } else { // 每個(gè)屬性都調(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); } } } } } } }
通過(guò) jvisualvm.exe 檢測(cè)代碼性能
再通過(guò)jvisualvm.exe檢測(cè)一下運(yùn)行情況,果然,logging.log4j
沒(méi)有了,其他的基本不變。
Spring copyProperties 源碼分析
- 判斷數(shù)據(jù)源和目標(biāo)對(duì)象的非空判斷改為了斷言;
- 每次copy沒(méi)有日志記錄;
- 沒(méi)有
if (orig instanceof DynaBean) {
這個(gè)類型檢查; - 增加了放開(kāi)權(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)對(duì)象不是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<String> ignoreList = (ignoreProperties != null ? Arrays.asList(ignoreProperties) : null); for (PropertyDescriptor targetPd : targetPds) { Method writeMethod = targetPd.getWriteMethod(); // 目標(biāo)對(duì)象存在寫入方法、屬性不被忽略 if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) { PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName()); if (sourcePd != null) { Method readMethod = sourcePd.getReadMethod(); /** * 源對(duì)象存在讀取方法、數(shù)據(jù)是可復(fù)制的 * writeMethod.getParameterTypes()[0]:獲取 writeMethod 的第一個(gè)入?yún)㈩愋? * readMethod.getReturnType():獲取 readMethod 的返回值類型 * 判斷返回值類型和入?yún)㈩愋褪欠翊嬖诶^承關(guān)系,只有是繼承關(guān)系或相等的情況下,才會(huì)進(jìn)行注入 */ if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) { try { // 放開(kāi)讀取方法的權(quán)限 if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) { readMethod.setAccessible(true); } // 通過(guò)反射獲取值 Object value = readMethod.invoke(source); // 放開(kāi)寫入方法的權(quán)限 if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) { writeMethod.setAccessible(true); } // 通過(guò)反射寫入值 writeMethod.invoke(target, value); } catch (Throwable ex) { throw new FatalBeanException( "Could not copy property '" + targetPd.getName() + "' from source to target", ex); } } } } } }
總結(jié)
阿里的友情提示,避免用Apache Beanutils
進(jìn)行對(duì)象的copy
,還是很有道理的。
Apache Beanutils
的性能問(wèn)題出現(xiàn)在類型校驗(yàn)和每一次copy的日志記錄;
Apache PropertyUtils 進(jìn)行了如下優(yōu)化:
- 類型檢查變成了非空校驗(yàn)
- 去掉了每一次copy的日志記錄
- 實(shí)際賦值的地方由copyProperty變成了DanyBean + setSimpleProperty;
Spring copyProperties 進(jìn)行了如下優(yōu)化:
- 判斷數(shù)據(jù)源和目標(biāo)對(duì)象的非空判斷改為了斷言;
- 每次copy沒(méi)有日志記錄;
- 沒(méi)有
if (orig instanceof DynaBean) {
這個(gè)類型檢查; - 增加了放開(kāi)權(quán)限的步驟;
以上就是如何避免Apache Beanutils屬性copy的詳細(xì)內(nèi)容,更多關(guān)于Apache Beanutils屬性copy的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot開(kāi)發(fā)項(xiàng)目,引入JPA找不到findOne方法的解決
這篇文章主要介紹了SpringBoot開(kāi)發(fā)項(xiàng)目,引入JPA找不到findOne方法的解決方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11淺談Mybatis+mysql 存儲(chǔ)Date類型的坑
這篇文章主要介紹了淺談Mybatis+mysql 存儲(chǔ)Date類型的坑,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-11-11java開(kāi)發(fā)使用BigDecimal避坑四則
這篇文章主要為大家介紹了java開(kāi)發(fā)使用BigDecimal的避坑四則,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-07-07Java選擇結(jié)構(gòu)與循環(huán)結(jié)構(gòu)的使用詳解
循環(huán)結(jié)構(gòu)是指在程序中需要反復(fù)執(zhí)行某個(gè)功能而設(shè)置的一種程序結(jié)構(gòu)。它由循環(huán)體中的條件,判斷繼續(xù)執(zhí)行某個(gè)功能還是退出循環(huán),選擇結(jié)構(gòu)用于判斷給定的條件,根據(jù)判斷的結(jié)果判斷某些條件,根據(jù)判斷的結(jié)果來(lái)控制程序的流程2022-03-03Java非法字符: ‘\ufeff‘問(wèn)題及說(shuō)明
這篇文章主要介紹了Java非法字符: ‘\ufeff‘問(wèn)題及說(shuō)明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02IntelliJ Idea 2020.1 正式發(fā)布,官方支持中文(必看)
這篇文章主要介紹了IntelliJ Idea 2020.1 正式發(fā)布,官方支持中文了,本文通過(guò)截圖的形式給大家展示,對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-04-04Java讀取文件的簡(jiǎn)單實(shí)現(xiàn)方法
這篇文章主要介紹了Java讀取文件的簡(jiǎn)單實(shí)現(xiàn)方法,通過(guò)一個(gè)讀取txt格式的log文件為例,詳細(xì)的講述了Java讀取文件的方法及原理,需要的朋友可以參考下2014-09-09Java中的StringTokenizer實(shí)現(xiàn)字符串切割詳解
這篇文章主要介紹了Java中的StringTokenizer實(shí)現(xiàn)字符串切割詳解,java.util工具包提供了字符串切割的工具類StringTokenizer,Spring等常見(jiàn)框架的字符串工具類(如Spring的StringUtils),需要的朋友可以參考下2024-01-01