本文介紹下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組件還能處理一些反射無法處理或者更加靈活解決一些應用問題,
參考
https://github.com/MapStruct/MapStruct-examples
http://www.kailing.pub/MapStruct1.3/index.html
https://mapstruct.org/documentation/stable/reference/html/
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/232555.html
標籤:java
上一篇:Spring Boot 引數校驗
