下面給大家介紹下Java對象屬性復制組件(MapStruct),以及項目中引入遇到的坑。
1. 問題背景
日常編程中,經常會碰到對象屬性復制的場景,就比如下面這樣一個常見的三層MVC架構。
前端請求通過VO對象接收,並通過DTO對象進行流轉,最后轉換成DO對象與數據庫DAO層進行交互,反之亦然。
當業務簡單的時候,可以通過手動編碼getter/setter函數來復制對象屬性。但是當業務變的復雜,對象屬性變得很多,那么手寫復制屬性代碼不僅十分繁瑣,非常耗時間,並且還可能容易出錯。
為了解決這個痛點,在項目初期,小輝項目的解決方法是隨手寫的轉換工具函數:根據變量名進行反射,對基礎類型和枚舉的變量進行賦值。
總結下目前該工具函數的優缺點:
優點:
-
開發效率高,隨時想要轉換的時候,傳入源對象以及指定class,調用下函數即可。
缺點:
-
項目中大量的反射會嚴重影響代碼執行效率
-
由於使用了反射,所以成員變量的使用被追蹤就很麻煩
-
轉換失敗只有在運行中報錯才會發現
-
對於嵌套對象字段的情況無能為力
-
只能對基礎類型進行復制
-
對字段名不一致的屬性無法賦值
2. 開源組件選擇
那如果想要更強大的功能,有哪些開源組件可以選擇呢?
下面小輝收集並盤點下相關開源組件的特點。
1. Apache BeanUtils
-
底層原理運用反射。
-
嵌套對象字段,將會與源對象使用同一對象,即使用淺拷貝。
-
字段名不一致的屬性無法被復制。
-
類型不一致的字段,將會進行默認類型轉化。
2. Spring BeanUtils:
-
底層原理同樣運用反射,但相比Apache BeanUtils減少了反射校驗,同時增加了緩存,所以提升了轉換速度。
-
嵌套對象字段,將會與源對象使用同一對象,即使用淺拷貝。
-
字段名不一致,屬性無法復制。
-
類型不一致的字段,將會進行默認類型轉化。
3. Cglib BeanCopier
-
字節碼技術動態生成一個代理類,代理類實現get和set方法。生成代理類過程存在一定開銷,但是一旦生成,我們可以緩存起來重復使用。相比前兩個更好用。
-
嵌套對象字段,將會與源對象使用同一對象,即使用淺拷貝。
-
字段名不一致,屬性無法復制。
-
類型不一致的字段,將會進行默認類型轉化。
4. Dozer
-
運用反射。
-
嵌套對象字段,不會與源對象使用同一對象,即深拷貝。
-
默認支持類型不一致(基本類型/包裝類型)轉換。
-
通過配置字段名的映射關系,不一樣字段的屬性也被復制。
5. orika
-
底層其使用了javassist生成字段屬性的映射的字節碼,然后直接動態加載執行字節碼文件,相比於使用反射的工具類,速度上會快很多。
-
支持深拷貝。
-
默認支持類型不一致(基本類型/包裝類型)轉換。
-
通過配置字段名的映射關系,不一樣字段的屬性也被復制。
上面介紹的這些工具類,不管使用反射,還是使用字節碼技術,這些都需要在代碼運行期間動態執行,所以相對於手寫硬編碼這種方式,上面這些工具類執行速度都會慢很多。
而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. 項目改造與踩坑提示
這次改造中相關依賴的版本:
-
lombok版本1.16.22,改造時升級為1.18.12
-
項目原有依賴fastjson版本1.2.62
-
引入MapStruct版本為1.4.1.Final
說明:
-
之所以要升級lombok版本,是因為上面UserDto對象轉化為LoginEventDto對象時,原有項目只在UserDto上添加@Builder,但是繼承類LoginEventDto無法繼承@Builder,導致MapStruct實例化的時候實例一個UserDto對象。
解決方法:在繼承層次結構的所有類(即LoginEventDto和UserDto)都需要使用@SuperBuilder可以,(類UserDto的@Builder要去掉)但這個@SuperBuilder只在更高的lombok版本才有,所以才升級了lombok版本。 -
項目中使用了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
-
在項目引入MapStruct時,有人會提出現在反射的性能消耗已經很低了,Spring、Mybatis等各種框架中大量使用反射,為什么還要使用MapStruct這種編譯期生成代碼的組件?
主要有如下考慮:
1.反射本身的性能損耗還是很大的,但由於開源庫對反射進行了緩存等優化處理,才減少反射對性能損耗的影響。然而,相比調用MapStruct生成的方法,優化后的性能還是差很多。
2.開源庫使用反射是為了通用性考慮,但在具體的業務場景,對象之間的轉換是很確定的。
3.MapStruct組件本身使用很簡單(看完這篇博客之后,可以解決大部分應用場景)。同時, MapStruct組件還能處理一些反射無法處理或者更加靈活解決一些應用問題。
總結:
本文給大家帶來Java對象屬性復制組件-Mapstruct項目改造指南希望大家能夠喜歡,期待下期有更好的文章給大家