在日常開發中,經常涉及到 VO、DTO、DO等對象之間的屬性拷貝,為了避免使用原始的setter和getter方法,我們通常過借助一些三方工具,本文我們將聊聊某程序員使用BeanUtils.copyProperties工具,導致差點被開除的血淚史。
BeanUtils.copyProperties是一個對象拷貝的常用工具,Spring和Apache都提供了對應的靜態方法,兩者源碼如下:
// org.springframework.beans.BeanUtilspublic static void copyProperties(Object source, Object target) throws BeansException { copyProperties(source, target, null, (String[]) null);}// org.apache.commons.beanutils.BeanUtilspublic static void copyProperties(final Object dest, final Object orig) throws IllegalAccessException, InvocationTargetException { BeanUtilsBean.getInstance().copyProperties(dest, orig);}
通過上述兩個源碼方法可以發現:兩個方法中的入參源對象和目標對象 順序是反的,所以在使用時,一定要注意具體導入的是哪一個BeanUtils,切勿把入參順序搞反。
接著,分別解析兩種方式的源碼實現原理:
org.springframework.beans.BeanUtils的源碼實現如下圖:
整個源碼的實現邏輯總結成下面 7個步驟:
對于上述前 3種拷貝失敗的場景,編譯期間無法感知,一旦代碼上線大概率會出 bug,另外,因為使用的反射機制,性能略有影響。
org.apache.commons.beanutils.BeanUtils的源碼實現如下圖:
通過源碼我們能夠看出:Apache的實現其實是對Spring的一種增強,增加了DynaBean和Map兩種類型的拷貝,它們的實現都是采用反射機制。
另外,Spring和 Apache的兩種實現方案都是淺拷貝,也就是說,如果對象中還有內嵌對象,如果不做額外處理,拷貝會失敗。
所謂淺拷貝,淺拷貝是一種復制對象的方式,它創建一個新對象,這個新對象是原對象的副本,但對于對象中引用類型的字段,淺拷貝只復制它們的引用,而不復制它們所指向的實際對象。換句話說,淺拷貝只拷貝對象的第一層屬性,對于屬性中的引用類型,只拷貝引用地址。
如下示例,當source內部Inner對象的 address字段更改了,target的也跟著變更了:
在上面源碼分析的過程中可以發現:只有同時滿足下面 3個條件才能拷貝成功:
以上 3個條件缺失任何一個拷貝都會失敗,但是編譯器無法感知,對程序員不友好。
假如,在開發中忘記寫getter和setter,使用BeanUtils.copyProperties拷貝不會有異常,但是業務邏輯上沒有達到預期,所以這種異常要么在測試中發現,要么需要跑真實的業務邏輯才能發現。
還有一種場景,假如source中有個money字段一開始被程序員A定義成double類型,后面被程序員B 修改成了BigDecimal,程序員B發現代碼沒有報錯,而且是一個小修改就直接上線了。
1天后,有人反饋線上出問題了,經過好一番努力地排查發現,使用BeanUtils.copyProperties拷貝,source中的money字段是BigDecimal類型,而target的money字段是double類型,最終導致拷貝失敗,而這位差點被開除的程序員恰好是這種場景。
基于上述描述,BeanUtils.copyProperties無法在編譯期間對拷貝字段的修改及時感知錯誤,假如公司上線規范不嚴,或者回歸測試不全面,一旦出現上述字段名稱或者類型被修改,很大可能造成線上問題,所以需要慎用BeanUtils.copyProperties。
既然BeanUtils.copyProperties拷貝存在上述問題,那么,有沒有什么好的替代方案呢?
有,通常替代方案有 2種:使用原始的setter和getter方法 和 MapStruct。
使用原始的setter和getter方法進行拷貝,雖然會編寫一些看似啰嗦的代碼,但是它具備以下優點:
import java.util.UUID;public Target convetSourceToTarget(Source1 source1, Source2 source2) { Target target = new Target(); target.setId(UUID.randomUUID().toString()); target.setName(source1.getName()); target.setAge(source1.getAge()); target.setAddress(source2.getAddress());}
(1) 使用示例
MapStruct是一個很優秀的 Java庫,也是用于簡化對象之間的拷貝工作,其主要特點如下:
為了更好地說明 MapStruct,我們以一個示例進行說明:
首先,我們需要增加mapstruct的依賴:
// maven 依賴<dependency> <groupId>org.mapstruct</groupId> <artifactId>mapstruct</artifactId> <version>1.5.2.Final</version></dependency>// gradle依賴implementation 'org.mapstruct:mapstruct:1.5.2.Final'
然后,定義一個Mapper接口:
import org.mapstruct.Mapper;import org.mapstruct.Mapping;import org.mapstruct.factory.Mappers;@Mapperpublic interface TestMapper { TestMapper INSTANCE = Mappers.getMapper(TestMapper.class); /** * 在Mapping中定義對象的 source和 target字段, * 如果source和 target的類型不一樣,編譯期會報錯 */ @Mapping(source = "name", target = "fullName") UserDTO toDTO(UserEntity entity);}
接著,定義兩個實體類:
public class UserDTO { private String fullName; private int age;}public class UserEntity { private String name; private int age;}
最后,寫一個測試類:
public class MapStructTest { public static void main(String[] args) { UserEntity entity = new UserEntity(); entity.setName("John"); entity.setAge(30); UserDTO dto = TestMapper.INSTANCE.toDTO(entity); System.out.println(dto.getFullName()); // 輸出: John System.out.println(dto.getAge()); // 輸出: 30 }}
上述代碼,在編譯器會自動創建一個TestMapperImpl實現類,如下圖:
(2) 實現原理
最后,總結下MapStruct實現原理:
① 注解處理器機制
MapStruct使用了 Java的注解處理器機制,通過實現javax.annotation.processing.Processor接口,在編譯時掃描和處理特定的注解。
② 注解掃描與處理
MapStruct定義了@Mapper、@Mapping 等注解,編譯器會調用注解處理器來處理這些注解。
③ 代碼生成
MapStruct會根據注解信息,解析源類和目標類的結構,并生成相應的映射,大致有以下幾個步驟:
④ 類型安全與錯誤檢查
在代碼生成過程中,MapStruct會進行類型檢查,確保源字段和目標字段的類型匹配,如果發現類型不匹配會報編譯時錯誤。
⑤ 支持自定義
MapStruct允許用戶自定義映射邏輯,比如下面的示例,通過qualifiedByName和 @Named注解實現了一個自定義的方法:
@Mapping(target = "tags", source = "tagSet", qualifiedByName = "defaultToEmptySet")UserEntity fromDO(UserDTO dto);@Named("defaultToEmptySet")default Set<String> defaultToEmptySet(Set<String> items) { return items == null ? new LinkedHashSet<>() : items;}
原始的setter和getter方法簡單且靈活,mapstruct通過注解的方式,比起原始的setter和getter門檻會高一點。
兩種方式都是編譯行為,因此,一旦拷貝的字段發生改變能及時感知,對程序員比較友好。
具體如何選擇,可以根據團隊約定而定,如果是個人學習,優先推薦mapstruct,可以作為一個學習和實踐點。
本文通過分析BeanUtils.copyProperties的源碼,總結了它的幾個缺點,綜合評估,建議慎用!
接著,通過分析mapstruct的原理以及使用案例,它完美解決了BeanUtils.copyProperties的缺點,是對象拷貝很不錯的選擇。
對于原始的setter和getter也是對象拷貝很不錯的選擇。
溫馨建議:如果使用三方的工具類,一定要事先了解其優缺點和安全性問題,這樣才能在使用過程中能做到心中有譜,處事不亂,避免拆盲盒導致不必要的事故。如果有更多的精力,再去研究下其原理,吸收他人優秀的思維。
本文鏈接:http://www.www897cc.com/showinfo-26-92151-0.html為什么不推薦使用 BeanUtils.copyProperties?
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com