避免用Apache Beanutils進行屬性的copy。why?讓我們一起一探究竟
大家好,我是哪吒。
今天,透過程式碼例項、原始碼解讀、四大工具類橫向對比的方式,和大家一起聊一聊物件賦值
的問題。
在實際的專案開發中,物件間賦值普遍存在,隨著雙十一、秒殺等電商過程愈加複雜,資料量也在不斷攀升,效率問題,浮出水面。
問:如果是你來寫物件間賦值的程式碼,你會怎麼做?
答:想都不用想,直接程式碼走起來,get、set即可。
問:下圖這樣?
答:對啊,你怎麼能把我的程式碼放到網上?
問:沒,我只是舉個例子
答:這涉及到商業機密,是很嚴重的問題
問:我發現你挺能扯皮啊,直接回答問題行嗎?
答:OK,OK,我也覺得這樣寫很low,上次這麼寫之後,差點捱打
物件太多,ctrl c + strl v,鍵盤差點沒敲壞; 而且很容易出錯,一不留神,屬性沒對應上,賦錯值了; 程式碼看起來很傻缺,一個類好幾千行,全是get、set複製,還起個了自以為很優雅的名字transfer; 如果屬性名不能見名知意,還得加上每個屬性的含義註釋(基本這種賦值操作,都是要加的,註釋很重要,註釋很重要,註釋很重要); 程式碼維護起來很麻煩; 如果物件過多,會產生類爆炸問題,如果屬性過多,會嚴重違背阿里巴巴程式碼規約(一個方法的實際程式碼最多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
都要進行多次型別檢查,還要列印日誌。
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。
問:還有其它好的方式嗎?效能好一點的
答:當然有,據我瞭解有 4 種工具類,實際上,可能會有更多,話不多說,先簡單介紹一下。
org.apache.commons.beanutils.BeanUtils; org.apache.commons.beanutils.PropertyUtils; org.springframework.cglib.beans.BeanCopier; 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.class, User.class, false);
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);
}
}
}
"四大金剛" 效能統計
方法 | 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的日誌記錄,效能肯定更好了。
型別檢查變成了非空校驗 去掉了每一次copy的日誌記錄 實際賦值的地方由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
沒有了,其他的基本不變。
Spring copyProperties 原始碼分析
判斷資料來源和目標物件的非空判斷改為了斷言; 每次copy沒有日誌記錄; 沒有 if (orig instanceof DynaBean) {
這個型別檢查;增加了放開許可權的步驟;
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 進行了如下最佳化:
型別檢查變成了非空校驗 去掉了每一次copy的日誌記錄 實際賦值的地方由copyProperty變成了DanyBean + setSimpleProperty;
Spring copyProperties 進行了如下最佳化:
判斷資料來源和目標物件的非空判斷改為了斷言; 每次copy沒有日誌記錄; 沒有 if (orig instanceof DynaBean) {
這個型別檢查;增加了放開許可權的步驟;
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024923/viewspace-2931879/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java:org.apache.commons.beanutils.BeanUtils拷貝物件屬性JavaApacheBean物件
- 我為什麼不推薦使用BeanUtils屬性轉換工具Bean
- 讓我們一起啃演算法----二進位制求和演算法
- 讓我們一起啃演算法----加一演算法
- 讓我們一起啃演算法----移除元素演算法
- 讓我們一起啃演算法----有效的數獨演算法
- 讓我們一起啃演算法----有效的括號演算法
- 今天,我們來探一探WebSocket原理Web
- 讓我們一起啃演算法----盛最多水的容器演算法
- 讓我們一起啃演算法----x 的平方根演算法
- 讓我們一起啃演算法----兩數相加演算法
- 讓我們一起啃演算法----三數之和演算法
- 讓我們一起啃演算法----爬樓梯演算法
- 讓我們一起啃演算法----迴文數演算法
- 《我們一起進大廠》系列- Redis基礎Redis
- 讓我們一起啃演算法----整數反轉演算法
- 讓我們一起啃演算法----最長公共字首演算法
- 讓我們一起啃演算法----搜尋插入位置演算法
- 讓我們一起解密組播、IGMP、IGMP監聽解密
- 快來稿定,一探究竟
- 《我們一起進大廠》系列-Redis雙寫一致性、併發競爭、執行緒模型Redis執行緒模型
- 宣告 NSString 型別的屬性,到底用 strong 還是 copy ?型別
- 我們們一起聊聊Java異常Java
- 多樣性日記:我們如何促進STEM的多樣性?
- 這篇文章,我們來談一談Spring中的屬性注入Spring
- 一起鑽進 Linux 核心看個究竟Linux
- Deno原理詳解,讓我們一起從原始碼分析開始原始碼
- 讓我們一起啃演算法----字母異位詞分組演算法
- 讓我們一起搖擺,AR應用DanceReality助你記住舞步
- Vue的進階屬性Vue
- python-進階教程-使用物件屬性進行排序Python物件排序
- 《我們一起進大廠》系列-快取雪崩、擊穿、穿透快取穿透
- 讓我們來做一個屬於自己的瀏覽器主頁吧!瀏覽器
- 我們究竟應不應該使用框架?框架
- 讓我們一起啃演算法----最後一個單詞的長度演算法
- 讓我們一起啃演算法----無重複字元的最長子串演算法字元
- 【一探究竟】Flutter到底是什麼?Flutter
- 別讓“防禦性程式設計”毀了我們的職業程式設計