Java對象屬性復制組件-Mapstruct項目改造指南


 

 

 

下面給大家介紹下Java對象屬性復制組件(MapStruct),以及項目中引入遇到的坑。

 

1. 問題背景

 

日常編程中,經常會碰到對象屬性復制的場景,就比如下面這樣一個常見的三層MVC架構。

 

 

前端請求通過VO對象接收,並通過DTO對象進行流轉,最后轉換成DO對象與數據庫DAO層進行交互,反之亦然。

 

當業務簡單的時候,可以通過手動編碼getter/setter函數來復制對象屬性。但是當業務變的復雜,對象屬性變得很多,那么手寫復制屬性代碼不僅十分繁瑣,非常耗時間,並且還可能容易出錯。

 

為了解決這個痛點,在項目初期,小輝項目的解決方法是隨手寫的轉換工具函數:根據變量名進行反射,對基礎類型和枚舉的變量進行賦值。

 

總結下目前該工具函數的優缺點:

 

優點:

 

  1. 開發效率高,隨時想要轉換的時候,傳入源對象以及指定class,調用下函數即可。

 

缺點:

 

  1. 項目中大量的反射會嚴重影響代碼執行效率

  2. 由於使用了反射,所以成員變量的使用被追蹤就很麻煩

  3. 轉換失敗只有在運行中報錯才會發現

  4. 對於嵌套對象字段的情況無能為力

  5. 只能對基礎類型進行復制

  6. 對字段名不一致的屬性無法賦值

 

2. 開源組件選擇

 

那如果想要更強大的功能,有哪些開源組件可以選擇呢?

 

下面小輝收集並盤點下相關開源組件的特點。

 

1. Apache BeanUtils

 

  1. 底層原理運用反射。

  2. 嵌套對象字段,將會與源對象使用同一對象,即使用淺拷貝。

  3. 字段名不一致的屬性無法被復制。

  4. 類型不一致的字段,將會進行默認類型轉化。

 

2. Spring BeanUtils:

 

  1. 底層原理同樣運用反射,但相比Apache BeanUtils減少了反射校驗,同時增加了緩存,所以提升了轉換速度。

  2. 嵌套對象字段,將會與源對象使用同一對象,即使用淺拷貝。

  3. 字段名不一致,屬性無法復制。

  4. 類型不一致的字段,將會進行默認類型轉化。

 

3. Cglib BeanCopier

 

  1. 字節碼技術動態生成一個代理類,代理類實現get和set方法。生成代理類過程存在一定開銷,但是一旦生成,我們可以緩存起來重復使用。相比前兩個更好用。

  2. 嵌套對象字段,將會與源對象使用同一對象,即使用淺拷貝。

  3. 字段名不一致,屬性無法復制。

  4. 類型不一致的字段,將會進行默認類型轉化。

 

4. Dozer

 

  1. 運用反射。

  2. 嵌套對象字段,不會與源對象使用同一對象,即深拷貝。

  3. 默認支持類型不一致(基本類型/包裝類型)轉換。

  4. 通過配置字段名的映射關系,不一樣字段的屬性也被復制。

 

5. orika

 

  1. 底層其使用了javassist生成字段屬性的映射的字節碼,然后直接動態加載執行字節碼文件,相比於使用反射的工具類,速度上會快很多。

  2. 支持深拷貝。

  3. 默認支持類型不一致(基本類型/包裝類型)轉換。

  4. 通過配置字段名的映射關系,不一樣字段的屬性也被復制。

 

上面介紹的這些工具類,不管使用反射,還是使用字節碼技術,這些都需要在代碼運行期間動態執行,所以相對於手寫硬編碼這種方式,上面這些工具類執行速度都會慢很多。

 

而MapStruct與上面五個組件原理都不同。

 

以上提到的屬性無法復制,都是在不使用手動寫Convert函數的情況下進行討論的

 

3. MapStruct

 

1. 為什么選擇MapStruct

 

接下來就要介紹MapStruct 這個工具類,這個工具類之所以運行速度與硬編碼差不多,這是因為MapStruct在編譯期間就生成屬性復制的代碼,運行期間就無需使用反射或者字節碼技術,從而確保了高性能。

 

 

另外,由於編譯期間就生成了代碼,所以如果有任何問題,編譯期間就可以提前暴露,這對於開發人員來講就可以提前解決問題,而不用等到代碼應用上線了,運行之后才發現錯誤。

 

所以,為了克服項目中當前函數的被提到的五個缺點,筆者引入了MapStruct。

 

2. 如何引入MapStruct

 

只需要引入MapStruct的依賴,同時由於MapStruct需要在編譯器期間生成代碼,所以我們需要maven-compiler-plugin插件中配置。

 

如果項目中沒有用到lombok,下面的lombok相關配置可以刪除;如果用到lombok,由於MapStruct和Lombok都會在編譯期間生成代碼,為解決沖突使用如下配置即可。

 

// pom.xml
       <dependency>
           <groupId>org.MapStruct</groupId>
           <artifactId>MapStruct</artifactId>
           <version>1.4.1.Final</version>
       </dependency>

 

// pom.xml
// 為了防止lombok和MapStruct的沖突,在pom.xml加入如下配置
<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>${plugin.compiler.version}</version>
           <configuration>
               <source>1.8</source>            
               <target>1.8</target>
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.MapStruct</groupId>
                       <artifactId>MapStruct-processor</artifactId>
                       <version>${MapStruct.version}</version>
                   </path>
                   <path>
                       <groupId>org.projectlombok</groupId>
                       <artifactId>lombok</artifactId>
                       <version>${lombok.version}</version>
                   </path>
                   <!-- other annotation processors -->
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>

 

3. MapStruct的常見使用方法

 

使用MapStruct很簡單,只需要創建一個mapper文件,然后在需要使用轉換的地方,注入調用即可。

 

下面列舉了兩個文件,涵蓋項目中絕大多數的mapper文件寫法。

 

DO轉成DTO的mapper:

 

/**
* componentModel = "spring":表明該類是一個 spring 組件,之后調用處只需要使用@Autowired,即可引入該類實例
* NullValuePropertyMappingStrategy.IGNORE:如果遇到舊對象屬性為null,則跳過該屬性賦值給新對象
*/
@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface UserTransMapper {
   /**
    * 這個對象可用於非Spring環境下獲取當前對象實例。如果在Spring環境下,該行代碼可刪除
    */
   UserTransMapper INSTANCE = Mappers.getMapper(UserTransMapper.class);
   
   /**
    * 將Userinfo對象中非null的屬性轉化為UserDto的對象
    * @param userInfo 從數據庫讀取的用戶信息
    * @return
    */
   UserDto userInfo2userDto(UserInfo userInfo);

   /**
    * 將Userinfo對象中非null的屬性更新到UserDto的對象
    * @param userInfo 從數據庫讀取的用戶信息
    * @param userDto 用戶信息的dto
    * 如果改void為UserDto,則函數會返回更新后的UserDto對象
    */
   void updateUserInfo2userDto(UserInfo userInfo, @MappingTarget UserDto userDto);

   /**
    * 將UserDto對象中非null的屬性轉化為LoginEventDto的對象
    * @param userDto 用戶信息的dto
    * @return LoginEventDto繼承UserDto
    */
   LoginEventDto userDto2loginEventDto(UserDto userDto);
}

 

DTO轉成VO的mapper:

 

@Mapper(componentModel = "spring", nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
public interface UserTransMapper {

   /**
    * UserDto對象中非null的屬性轉化為UserInfoVo的對象
    * @param userDto 用戶信息的dto
    * @return UserInfoVo繼承與UserBaseInfoVo,都是用了@Data,沒有異常報錯。
    */
   UserInfoVo userDto2userVo(UserDto userDto);

   /**
    * 直接寫嵌套List等集合類,同樣可以生效
    * @param userDtoList
    * @return
    */
   List<UserInfoVo> userDto2userVo(List<UserDto> userDtoList);

   /**
    * 如果UserDto存在成員變量是類UserSubDto,而UserInfoVo存在成員變量是類UserSubVo,想在上面轉化的同時,讓這兩個成員變量進行賦值,只需要定義下面的函數即可。
    *
    * @param userSubDto 用戶信息的dto中的成員變量,類型為UserSubDto
    * @return
    */
   UserSubVo userSubDto2userSubVo(UserSubDto userSubDto);

   /**
    * UserDto對象和FollowInfoDto對象中非null的屬性轉化為UserInfoVo的對象
    * @param userDto 用戶信息的dto
    * @param followInfoDto 關注粉絲的dto
    * @param hn 房子數量
    * @return
    */
   @Mappings({
           @Mapping(source = "userDto.regionId",target = "regionId"),
           @Mapping(source = "followInfoDto.price", target = "price", numberFormat = "0.00"),
           @Mapping(source = "hn",target = "houseNumber")
   })
   /**
    * @Mapping也就是手動映射字段的操作,使用簡單,讀者可自行研究
    */
   UserInfoVo userDto2userVo(UserDto userDto, FollowInfoDto followInfoDto, Integer hn);
   /**
    * 假設從映射Person到PersonDto需要一些MapStruct無法生成的特殊邏輯,可以定義一個default函數
    */
   default PersonDto personToPersonDto(Person person) {
       // 手動寫映射邏輯
   }
}

 

4. 項目改造與踩坑提示

 

這次改造中相關依賴的版本:

 

  1. lombok版本1.16.22,改造時升級為1.18.12

  2. 項目原有依賴fastjson版本1.2.62

  3. 引入MapStruct版本為1.4.1.Final

 

說明:

 

  1. 之所以要升級lombok版本,是因為上面UserDto對象轉化為LoginEventDto對象時,原有項目只在UserDto上添加@Builder,但是繼承類LoginEventDto無法繼承@Builder,導致MapStruct實例化的時候實例一個UserDto對象。
    解決方法:在繼承層次結構的所有類(即LoginEventDto和UserDto)都需要使用@SuperBuilder可以,(類UserDto的@Builder要去掉)但這個@SuperBuilder只在更高的lombok版本才有,所以才升級了lombok版本。

  2. 項目中使用了fastjson,因此業務代碼中出現很多處需要反射調用無參構造函數。但在上面一步升級lombok的過程中,lombok對於@Builder的實現出現了一些修改:在1.16.22的生成代碼中,是存在private級別的無參構造函數;而在1.18.12的生成代碼中,並沒有私有無參構造函數,從而導致了業務代碼大量出現缺少默認構造函數的報錯。
    解決方法:@Builder注解跟構造函數之間的沖突很常見。最佳實踐是:在所有使用@Builder或者@SupserBuilder的類,增加@NoArgsConstructor和@AllArgsConstructor。

 

雖然本文極力推薦MapStruct,但如果是老項目的話,尤其是大項目的話,還是考慮下改造后的測試成本。本人在第一次引入的時候,過於自信,在父pom引入MapStruct並提升了lombok版本,直接導致開發環境的微服務集體報錯。后來改為在單個微服務實驗,並且放在開發環境長期觀察(主要這個改動影響測試覆蓋面太大,也不想讓QA為了技術優化來加班),之后才敢放到生產。

 

當然如果是新項目,非常推薦嘗試下MapStruct。

 

5. Q&A

 

  1. 在項目引入MapStruct時,有人會提出現在反射的性能消耗已經很低了,Spring、Mybatis等各種框架中大量使用反射,為什么還要使用MapStruct這種編譯期生成代碼的組件?
    主要有如下考慮:
    1.反射本身的性能損耗還是很大的,但由於開源庫對反射進行了緩存等優化處理,才減少反射對性能損耗的影響。然而,相比調用MapStruct生成的方法,優化后的性能還是差很多。
    2.開源庫使用反射是為了通用性考慮,但在具體的業務場景,對象之間的轉換是很確定的。
    3.MapStruct組件本身使用很簡單(看完這篇博客之后,可以解決大部分應用場景)。同時, MapStruct組件還能處理一些反射無法處理或者更加靈活解決一些應用問題。

 

總結:

 

本文給大家帶來Java對象屬性復制組件-Mapstruct項目改造指南希望大家能夠喜歡,期待下期有更好的文章給大家

 

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM