推薦一款Java對象映射神器,別再傻傻手動轉(zhuǎn)換了!

原文地址:https://nullpointer.pw/mapstruct最佳實踐.html
前幾天發(fā)的《一份熱乎的 SpringBoot 前后端分離后臺管理系統(tǒng)分析!分模塊開發(fā)、RBAC 權限控制...》這篇文章中我推薦了 MapStruct 來做對象映射。這篇文章就帶著小伙伴們詳細的看一下這個好用的工具庫。
前言
按照日常開發(fā)習慣,對于不同領域?qū)邮褂貌煌?code style="font-size: 14px;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(150, 84, 181);">JavaBean對象傳輸數(shù)據(jù),避免相互影響,因此基于數(shù)據(jù)庫實體對象User衍生出比如UserDto、UserVo等對象,于是在不同層之間進行數(shù)據(jù)傳輸時,不可避免地需要將這些對象進行互相轉(zhuǎn)換操作。
常見的轉(zhuǎn)換方式有:
調(diào)用 getter/setter方法進行屬性賦值調(diào)用 BeanUtil.copyPropertie進行反射屬性賦值
第一種方式不必說,屬性多了就需要寫一大坨getter/setter代碼。第二種方式比第一種方式要簡便很多,但是坑巨多,比如 sources 與 target 寫反,難以定位某個字段在哪里進行的賦值,同時因為用到反射,導致性能也不佳。
鑒于此,今天寫一寫第三種對象轉(zhuǎn)換方式,本文使用的是 MapStruct 工具進行轉(zhuǎn)換,MapStruct 原理也很簡單,就是在代碼編譯階段生成對應的賦值代碼,底層原理還是調(diào)用getter/setter方法,但是這是由工具替我們完成,MapStruct在不影響性能的情況下,解決了前面兩種方式弊端,很贊~
準備工作
為了講解 MapStruct 工具的使用,本文使用常見的 User 類以及對應 UserDto 對象來演示。
@Data
@Accessors(chain?=?true)
public?class?User?{
????private?Long?id;
????private?String?username;
????private?String?password;?//?密碼
????private?Integer?sex;??//?性別
????private?LocalDate?birthday;?//?生日
????private?LocalDateTime?createTime;?//?創(chuàng)建時間
????private?String?config;?//?其他擴展信息,以JSON格式存儲
???private?String?test;?//?測試字段
}
@Data
@Accessors(chain?=?true)
public?class?UserVo?{
????private?Long?id;
????private?String?username;
????private?String?password;
????private?Integer?gender;
????private?LocalDate?birthday;
????private?String?createTime;
????private?List?config;
???private?String?test;?//?測試字段
????@Data
????public?static?class?UserConfig?{
????????private?String?field1;
????????private?Integer?field2;
????}
}
注意觀察這兩個類的區(qū)別。
一、MapStruct 配置以及基礎使用
項目中引入 MapStruct 的依賴
<dependency>
??<groupId>org.mapstructgroupId>
??<artifactId>mapstructartifactId>
??<version>1.3.1.Finalversion>
dependency>
<dependency>
??<groupId>org.mapstructgroupId>
??<artifactId>mapstruct-processorartifactId>
??<version>1.3.1.Finalversion>
dependency>
因為項目中的對象轉(zhuǎn)換操作基本都一樣,因此抽取除了一個轉(zhuǎn)換基類,不同對象如果只是簡單轉(zhuǎn)換可以直接繼承該基類,而無需覆寫基類任何方法,即只需要一個空類即可。如果子類覆寫了基類的方法,則基類上的 @Mapping 會失效。
@MapperConfig
public?interface?BaseMapping<SOURCE,?TARGET>?{
????/**
?????*?映射同名屬性
?????*/
????@Mapping(target?=?"createTime",?dateFormat?=?"yyyy-MM-dd?HH:mm:ss")
????TARGET?sourceToTarget(SOURCE?var1);
????/**
?????*?反向,映射同名屬性
?????*/
????@InheritInverseConfiguration(name?=?"sourceToTarget")
????SOURCE?targetToSource(TARGET?var1);
????/**
?????*?映射同名屬性,集合形式
?????*/
????@InheritConfiguration(name?=?"sourceToTarget")
????List?sourceToTarget(List?var1) ;
????/**
?????*?反向,映射同名屬性,集合形式
?????*/
????@InheritConfiguration(name?=?"targetToSource")
????List?targetToSource(List?var1) ;
????/**
?????*?映射同名屬性,集合流形式
?????*/
????List?sourceToTarget(Stream?stream) ;
????/**
?????*?反向,映射同名屬性,集合流形式
?????*/
????List?targetToSource(Stream?stream) ;
}
實現(xiàn) User 與 UserVo 對象的轉(zhuǎn)換器
import?org.mapstruct.Mapper;
import?org.mapstruct.Mapping;
@Mapper(componentModel?=?"spring")
public?interface?UserMapping?extends?BaseMapping<User,?UserVo>?{
????@Mapping(target?=?"gender",?source?=?"sex")
????@Mapping(target?=?"createTime",?dateFormat?=?"yyyy-MM-dd?HH:mm:ss")
????@Override
????UserVo?sourceToTarget(User?var1);
????@Mapping(target?=?"sex",?source?=?"gender")
????@Mapping(target?=?"password",?ignore?=?true)
????@Mapping(target?=?"createTime",?dateFormat?=?"yyyy-MM-dd?HH:mm:ss")
????@Override
????User?targetToSource(UserVo?var1);
????default?List?strConfigToListUserConfig(String?config)? {
????????return?JSON.parseArray(config,?UserConfig.class);
????}
????default?String?listUserConfigToStrConfig(List?list) ?{
????????return?JSON.toJSONString(list);
????}
}
本文示例使用的是 Spring 的方式,@Mapper注解的 componentModel 屬性值為 spring,不過應該大多數(shù)都用的此模式進行開發(fā)。
@Mapping用于配置對象的映射關系,示例中 User 對象性別屬性名為 sex,而UserVo對象性別屬性名為gender,因此需要配置 target 與 source 屬性。
password 字段不應該返回到前臺,可以采取兩種方式不進行轉(zhuǎn)換,第一種就是在 vo 對象中不出現(xiàn) password 字段,第二種就是在@Mapping中設置該字段ignore = true。
MapStruct 提供了時間格式化的屬性 dataFormat,支持Date、LocalDate、LocalDateTime等時間類型與String的轉(zhuǎn)換。示例中birthday 屬性為 LocalDate 類型,可以無需指定dataFormat自動完成轉(zhuǎn)換,而LocalDateTime類型默認使用的是 ISO 格式時間,在國內(nèi)往往不符合需求,因此需要手動指定一下 dataFormat。
二、自定義屬性類型轉(zhuǎn)換方法
一般常用的類型字段轉(zhuǎn)換 MapStruct都能替我們完成,但是有一些是我們自定義的對象類型,MapStruct就不能進行字段轉(zhuǎn)換,這就需要我們編寫對應的類型轉(zhuǎn)換方法,筆者使用的是 JDK8,支持接口中的默認方法,可以直接在轉(zhuǎn)換器中添加自定義類型轉(zhuǎn)換方法。
示例中User對象的 config 屬性是一個 JSON 字符串,UserVo對象中是 List 類型的,這需要實現(xiàn) JSON 字符串與對象的互轉(zhuǎn)。
default?List?strConfigToListUserConfig(String?config)? {
??return?JSON.parseArray(config,?UserConfig.class);
}
default?String?listUserConfigToStrConfig(List?list) ?{
??return?JSON.toJSONString(list);
}
如果是 JDK8 以下的,不支持默認方法,可以另外定義一個 轉(zhuǎn)換器,然后再當前轉(zhuǎn)換器的 @Mapper 中通過 uses = XXX.class 進行引用。
定義好方法之后,MapStruct當匹配到合適類型的字段時,會調(diào)用我們自定義的轉(zhuǎn)換方法進行轉(zhuǎn)換。
三、單元測試
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest
public?class?MapStructTest?{
??@Resource
??private?UserMapping?userMapping;
??@Test
??public?void?tetDomain2DTO()?{
????User?user?=?new?User()
??????.setId(1L)
??????.setUsername("zhangsan")
??????.setSex(1)
??????.setPassword("abc123")
??????.setCreateTime(LocalDateTime.now())
??????.setBirthday(LocalDate.of(1999,?9,?27))
??????.setConfig("[{\"field1\":\"Test?Field1\",\"field2\":500}]");
????UserVo?userVo?=?userMapping.sourceToTarget(user);
????log.info("User:?{}",?user);
????//????????User:?User(id=1,?username=zhangsan,?password=abc123,?sex=1,?birthday=1999-09-27,?createTime=2020-01-17T17:46:20.316,?config=[{"field1":"Test?Field1","field2":500}])
????log.info("UserVo:?{}",?userVo);
????//????????UserVo:?UserVo(id=1,?username=zhangsan,?gender=1,?birthday=1999-09-27,?createTime=2020-01-17?17:46:20,?config=[UserVo.UserConfig(field1=Test?Field1,?field2=500)])
??}
??@Test
??public?void?testDTO2Domain()?{
????UserConfig?userConfig?=?new?UserConfig();
????userConfig.setField1("Test?Field1");
????userConfig.setField2(500);
????UserVo?userVo?=?new?UserVo()
??????.setId(1L)
??????.setUsername("zhangsan")
??????.setGender(2)
??????.setCreateTime("2020-01-18?15:32:54")
??????.setBirthday(LocalDate.of(1999,?9,?27))
??????.setConfig(Collections.singletonList(userConfig));
????User?user?=?userMapping.targetToSource(userVo);
????log.info("UserVo:?{}",?userVo);
????//????????UserVo:?UserVo(id=1,?username=zhangsan,?gender=2,?birthday=1999-09-27,?createTime=2020-01-18?15:32:54,?config=[UserVo.UserConfig(field1=Test?Field1,?field2=500)])
????log.info("User:?{}",?user);
????//????????User:?User(id=1,?username=zhangsan,?password=null,?sex=2,?birthday=1999-09-27,?createTime=2020-01-18T15:32:54,?config=[{"field1":"Test?Field1","field2":500}])
??}
四、常見問題
當兩個對象屬性不一致時,比如 User對象中某個字段不存在與UserVo當中時,在編譯時會有警告提示,可以在@Mapping中配置ignore = true,當字段較多時,可以直接在@Mapper中設置unmappedTargetPolicy屬性或者unmappedSourcePolicy屬性為ReportingPolicy.IGNORE即可。如果項目中也同時使用到了 Lombok,一定要注意 Lombok 的版本要等于或者高于1.18.10,否則會有編譯不通過的情況發(fā)生,筆者掉進這個坑很久才爬了出來,希望各位不要重復踩坑。
代碼下載
本文涉及代碼已上傳到 Github,以供參考。
mapstruct 最佳實踐示例代碼[1]
參考
官方文檔[2] 官方 FAQ[3] 官方 Examples[4] 機翻中文版文檔[5] 5 種常見 Bean 映射工具的性能比對[6]
參考資料
mapstruct 最佳實踐示例代碼: https://github.com/Mosiki/learning-modules/tree/master/learning-mapstruct
[2]官方文檔: https://mapstruct.org/documentation/stable/reference/html/
[3]官方 FAQ: https://mapstruct.org/faq/
[4]官方 Examples: https://github.com/mapstruct/mapstruct-examples
[5]機翻中文版文檔: http://www.kailing.pub/MapStruct1.3/index.html
[6]5 種常見 Bean 映射工具的性能比對: https://www.cnblogs.com/javaguide/p/11861749.html
最近寫的一些干貨,每篇都很用心,歡迎各位小伙伴閱讀/點贊/分享:
我是Guide哥,Java后端開發(fā),會一點前端知識,喜歡烹飪,自由的少年。一個三觀比主角還正的技術人。我們下期再見!
