摘要:于是我建議這位小伙伴使用了進(jìn)行屬性拷貝,這為我們的程序挖了一個(gè)坑阿里代碼規(guī)約當(dāng)我們開(kāi)啟阿里代碼掃描插件時(shí),如果你使用了進(jìn)行屬性拷貝,它會(huì)給你一個(gè)非常嚴(yán)重的警告。大名鼎鼎的提供的包,居然會(huì)存在性能問(wèn)題,以致于阿里給出了嚴(yán)重的警告。
聲明:本文屬原創(chuàng)文章,始發(fā)于公號(hào):程序員自學(xué)之道,并同步發(fā)布于 https://blog.csdn.net/dadiyang,特此,同步發(fā)布到 sf,轉(zhuǎn)載請(qǐng)注明出處。
緣起有一次開(kāi)發(fā)過(guò)程中,剛好看到一個(gè)小伙伴在調(diào)用 set 方法,將一個(gè)數(shù)據(jù)庫(kù)中查詢出來(lái)的 PO 對(duì)象的屬性拷貝到 Vo 對(duì)象中,類似這樣:
可以看出,Po 和 Vo 兩個(gè)類的字段絕大部分是一樣的,我們一個(gè)個(gè)地調(diào)用 set 方法只是做了一些重復(fù)的冗長(zhǎng)的操作。這種操作非常容易出錯(cuò),因?yàn)閷?duì)象的屬性太多,有可能會(huì)漏掉一兩個(gè),而且肉眼很難察覺(jué)。
類似這樣的操作,我們可以很容易想到,可以通過(guò)反射來(lái)解決。其實(shí),如此普遍通用的功能,一個(gè) BeanUtils 工具類就可以搞定了。
于是我建議這位小伙伴使用了 Apache BeanUtils.copyProperties 進(jìn)行屬性拷貝,這為我們的程序挖了一個(gè)坑!
阿里代碼規(guī)約當(dāng)我們開(kāi)啟阿里代碼掃描插件時(shí),如果你使用了 Apache BeanUtils.copyProperties 進(jìn)行屬性拷貝,它會(huì)給你一個(gè)非常嚴(yán)重的警告。因?yàn)椋?strong>Apache BeanUtils性能較差,可以使用 Spring BeanUtils 或者 Cglib BeanCopier 來(lái)代替。
看到這樣的警告,有點(diǎn)讓人有點(diǎn)不爽。大名鼎鼎的 Apache 提供的包,居然會(huì)存在性能問(wèn)題,以致于阿里給出了嚴(yán)重的警告。
那么,這個(gè)性能問(wèn)題究竟是有多嚴(yán)重呢?畢竟,在我們的應(yīng)用場(chǎng)景中,如果只是很微小的性能損耗,但是能帶來(lái)非常大的便利性,還是可以接受的。
帶著這個(gè)問(wèn)題。我們來(lái)做一個(gè)實(shí)驗(yàn),驗(yàn)證一下。
如果對(duì)具體的測(cè)試方式?jīng)]有興趣,可以跳過(guò)直接看結(jié)果哦~
測(cè)試方法接口和實(shí)現(xiàn)定義首先,為了測(cè)試方便,讓我們來(lái)定義一個(gè)接口,并將幾種實(shí)現(xiàn)統(tǒng)一起來(lái):
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); } } // 全局靜態(tài) BeanCopier,避免每次都生成新的對(duì)象 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); } }單元測(cè)試
然后寫(xiě)一個(gè)參數(shù)化的單元測(cè)試:
@RunWith(Parameterized.class) public class PropertiesCopierTest { @Parameterized.Parameter(0) public PropertiesCopier propertiesCopier; // 測(cè)試次數(shù) private static List測(cè)試結(jié)果testTimes = Arrays.asList(100, 1000, 10_000, 100_000, 1_000_000); // 測(cè)試結(jié)果以 markdown 表格的形式輸出 private static StringBuilder resultBuilder = new StringBuilder("|實(shí)現(xiàn)|100|1,000|10,000|100,000|1,000,000| ").append("|----|----|----|----|----|----| "); @Parameterized.Parameters public static Collection
時(shí)間單位毫秒
實(shí)現(xiàn) | 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 |
結(jié)果表明,Cglib 的 BeanCopier 的拷貝速度是最快的,即使是百萬(wàn)次的拷貝也只需要 10 毫秒!
相比而言,最差的是 Commons 包的 BeanUtils.copyProperties 方法,100 次拷貝測(cè)試與表現(xiàn)最好的 Cglib 相差 400 倍之多。百萬(wàn)次拷貝更是出現(xiàn)了 2800 倍的性能差異!
結(jié)果真是讓人大跌眼鏡。
但是它們?yōu)槭裁磿?huì)有這么大的差異呢?
原因分析查看源碼,我們會(huì)發(fā)現(xiàn) CommonsBeanUtils 主要有以下幾個(gè)耗時(shí)的地方:
輸出了大量的日志調(diào)試信息
重復(fù)的對(duì)象類型檢查
類型轉(zhuǎn)換
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) { ... // 這里每個(gè)屬性都調(diào)一次 copyProperty copyProperty(dest, name, value); } } } public void copyProperty(final Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException { ... // 這里又進(jìn)行一次類型檢查 if (target instanceof DynaBean) { ... } ... // 需要將屬性轉(zhuǎn)換為目標(biāo)類型 value = convertForCopy(value, type); ... } // 而這個(gè) convert 方法在日志級(jí)別為 debug 的時(shí)候有很多的字符串拼接 publicT convert(final Class 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 { // 這個(gè) convertToType 方法里也需要做類型檢查 final Object result = convertToType(targetType, value); if (log().isDebugEnabled()) { log().debug("Converted to " + toString(targetType) + " value "" + result + """); } return targetType.cast(result); } }
具體的性能和源碼分析,可以參考這幾篇文章:
幾種copyProperties工具類性能比較:https://www.jianshu.com/p/bcb...
CGLIB中BeanCopier源碼實(shí)現(xiàn):https://www.jianshu.com/p/f8b...
Java Bean Copy框架性能對(duì)比:https://yq.aliyun.com/article...
One more thing除了性能問(wèn)題之外,在使用 CommonsBeanUtils 時(shí)還有其他的坑需要特別小心!
包裝類默認(rèn)值在進(jìn)行屬性拷貝時(shí),雖然 CommonsBeanUtils 默認(rèn)不會(huì)給原始包裝類賦默認(rèn)值的,但是在使用低版本(1.8.0及以下)的時(shí)候,如果你的類有 Date 類型屬性,而且來(lái)源對(duì)象中該屬性值為 null 的話,就會(huì)發(fā)生異常:
org.apache.commons.beanutils.ConversionException: No value specified for "Date"
解決這個(gè)問(wèn)題的辦法是注冊(cè)一個(gè) DateConverter:
ConvertUtils.register(new DateConverter(null), java.util.Date.class);
然而這個(gè)語(yǔ)句,會(huì)導(dǎo)致包裝類型會(huì)被賦予原始類型的默認(rèn)值,如 Integer 屬性默認(rèn)賦值為 0,盡管你的來(lái)源對(duì)象該字段的值為 null。
在高版本(1.9.3)中,日期 null 值的問(wèn)題和包裝類賦默認(rèn)值的問(wèn)題都被修復(fù)了。
這個(gè)在我們的包裝類屬性為 null 值時(shí)有特殊含義的場(chǎng)景,非常容易踩坑!例如搜索條件對(duì)象,一般 null 值表示該字段不做限制,而 0 表示該字段的值必須為0。
改用其他工具時(shí)當(dāng)我們看到阿里的提示,或者你看了這篇文章之后,知道了 CommonsBeanUtils 的性能問(wèn)題,想要改用 Spring 的 BeanUtils 時(shí),要小心:
org.apache.commons.beanutils.BeanUtils.copyProperties(Object target, Object source); org.springframework.beans.BeanUtils.copyProperties(Object source, Object target);
從方法簽名上可以看出,這兩個(gè)工具類的名稱相同,方法名也相同,甚至連參數(shù)個(gè)數(shù)、類型、名稱都相同。但是參數(shù)的位置是相反的。因此,如果你想更改的時(shí)候,千萬(wàn)要記得,將 target 和 source 兩個(gè)參數(shù)也調(diào)換過(guò)來(lái)!
另外,可能由于種種原因,你獲取的堆棧信息不完整找不到問(wèn)題在哪,所以這里順便提醒一下:
如果你遇到 java.lang.IllegalArgumentException: Source must not be null 或者 java.lang.IllegalArgumentException: Target must not be null 這樣的異常信息卻到處找不到原因時(shí),不用找了,這是由于你在 copyProperties 的時(shí)候傳了 null 值導(dǎo)致的。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/74742.html
摘要:前言作為一名全干打字員,干活時(shí)經(jīng)常會(huì)被要求使用各種各樣的語(yǔ)言去實(shí)現(xiàn)各種各樣的需求,來(lái)回切換起來(lái)寫(xiě)的代碼就會(huì)或多或少有點(diǎn)不規(guī)范。今天我們以為例,講講在代碼中,我們需要注意的某些規(guī)范。 前言 作為一名全干打字員,干活時(shí)經(jīng)常會(huì)被要求使用各種各樣的語(yǔ)言去實(shí)現(xiàn)各種各樣的需求,來(lái)回切換起來(lái)寫(xiě)的代碼就會(huì)或多或少有點(diǎn)不規(guī)范。今天我們以JAVA為例,講講在代碼中,我們需要注意的某些規(guī)范。(本文標(biāo)準(zhǔn)依賴于...
摘要:拷貝操作又一個(gè)非常好用的工具類和中分別存在一個(gè),提供了對(duì)。除了支持基本類型以及基本類型的數(shù)組之外,還支持這些類的對(duì)象,其余一概不支持。而且,由于這些類都是采用反射機(jī)制實(shí)現(xiàn)的,對(duì)程序的效率也會(huì)有影響。因此,慎用或者使用看效果如何 java bean拷貝操作又一個(gè)非常好用的工具類 BeanUitls :spring (org.springframework.beans.BeanUtils)...
摘要:說(shuō)明這篇文章是我第一次認(rèn)真閱讀阿里巴巴開(kāi)發(fā)手冊(cè)終極版的筆記。說(shuō)明本手冊(cè)明確防止是調(diào)用者的責(zé)任。一年半載后,那么單元測(cè)試幾乎處于廢棄狀態(tài)。好的單元測(cè)試能夠最大限度地規(guī)避線上故障。 說(shuō)明 這篇文章是我第一次(認(rèn)真)閱讀《阿里巴巴 Java 開(kāi)發(fā)手冊(cè)(終極版)》的筆記。手冊(cè)本身對(duì)規(guī)范的講解已經(jīng)非常詳細(xì)了,如果你已經(jīng)有一定的開(kāi)發(fā)經(jīng)驗(yàn)并且有良好的編碼習(xí)慣和意識(shí),會(huì)發(fā)現(xiàn)大部分規(guī)范是符合常識(shí)的。所以...
摘要:熟悉和遵守阿里巴巴開(kāi)發(fā)手冊(cè)的編程風(fēng)格,那只是標(biāo),而代碼可讀性的本可以追溯到軟件設(shè)計(jì)階段。何為條設(shè)計(jì)規(guī)約是根據(jù)阿里巴巴實(shí)際項(xiàng)目架構(gòu)經(jīng)驗(yàn)提煉而成,共條。本次新增的不單是條新的設(shè)計(jì)規(guī)約,還是千萬(wàn)阿里人的技術(shù)之心。 摘要:2018年6月,《阿里巴巴Java開(kāi)發(fā)手冊(cè)》再次刷新代碼規(guī)范認(rèn)知,我們新增了16條設(shè)計(jì)規(guī)約!現(xiàn)免費(fèi)開(kāi)放下載,不可錯(cuò)過(guò)!《阿里巴巴Java開(kāi)發(fā)手冊(cè)》是阿里內(nèi)部Java工程師所遵...
摘要:在中,工具類定義了一組公共方法,這篇文章將介紹中使用最頻繁及最通用的工具類。另外,工具類,根據(jù)阿里開(kāi)發(fā)手冊(cè),包名如果要使用不能帶,工具類命名為 在Java中,工具類定義了一組公共方法,這篇文章將介紹Java中使用最頻繁及最通用的Java工具類。以下工具類、方法按使用流行度排名,參考數(shù)據(jù)來(lái)源于Github上隨機(jī)選取的5萬(wàn)個(gè)開(kāi)源項(xiàng)目源碼。 一. org.apache.commons.io....
閱讀 1599·2021-11-22 09:34
閱讀 1690·2019-08-29 16:36
閱讀 2668·2019-08-29 15:43
閱讀 3113·2019-08-29 13:57
閱讀 1298·2019-08-28 18:05
閱讀 1875·2019-08-26 18:26
閱讀 3243·2019-08-26 10:39
閱讀 3455·2019-08-23 18:40