【代碼優化】Bean映射之MapStruct
一、背景
領域模型互相轉換就隻能靠手工的 get()/set()
?
普遍的做法有以下幾種:
- 手工
;get()/set()
- 構造器;
-
工具類(BeanUtils
和Apache
都包含該工具類,使用方式稍稍不同);Spring
-
模式。Builder
這些方式都存在一些缺點:耦合性強,手工
get()/set()
經常丢參數,或者搞錯參數值....
本文推薦一種效率較高的方式:MapStruct
二、理論基礎
MapStruct
是一個自動生成
Bean
映射類的代碼生成器,
MapStruct
還能夠在不同的資料類型之間進行轉換。
2.1 pom.xml
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.4.2.Final</version>
</dependency>
2.2 注解關鍵詞
-
:隻有在接口加上這個注解,@Mapper
才會去實作該接口;MapStruct
-
:屬性映射,若源對象屬性與目标對象名字一緻,會自動映射對應屬性:@Mapping
-
:源屬性;source
-
:目标屬性;target
-
:字元串與日期之間互相轉換;dateFormat
-
: 某個屬性不想映射,可以加上ignore
ignore=true
-
:自定義指定的映射方法;expression
-
-
:配置多個@Mappings
@Mapping
-
:映射到現有示例。@MappingTarget
2.3 工作原理
我們要做的就是定義一個
mapper
接口,該接口聲明任何所需的映射方法。在編譯期間,
MapStruct
将生成此接口的實作。此實作使用普通的
Java
方法調用來在源對象和目标對象之間進行映射。
三、MapStruct 實踐
3.1 基本準備
- 新增三個資料庫
類:DO
使用者資訊:
@Data
public class UserInfoDO {
private Long id;
private String userName;
private String password;
private String phoneNum;
private Date gmtBroth;
private RoleDO role;
public UserInfoDO() {
}
public UserInfoDO(RoleDO role,Long id,String userName,String password,String phoneNum,Date gmtBroth) {
this.role = role;
this.id = id;
this.userName = userName;
this.password = password;
this.phoneNum = phoneNum;
this.gmtBroth = gmtBroth;
}
}
使用者補充資訊:
@Data
public class UserExtInfoDO {
private String favorite;
public UserExtInfoDO() {
}
public UserExtInfoDO(String favorite) {
this.favorite = favorite;
}
}
角色資訊:
@Data
public class RoleDO {
private Long id;
private String roleName;
private String description;
public RoleDO() {
}
public RoleDO(Long id, String roleName, String description) {
this.id = id;
this.roleName = roleName;
this.description = description;
}
}
- 新增一個資料傳輸
DTO
@Data
public class UserInfoDTO {
/**
* 使用者id
*/
private Long userId;
/**
* 使用者名
*/
private String userName;
/**
* 使用者名
*/
private String password;
/**
* 生日
*/
private String brothStr;
/**
* 手機号
*/
private String phoneNum;
/**
* 角色名
*/
private String roleName;
/**
* 喜好
*/
private String favorite;
}
- 新增一個加密工具類
public class Base64Util {
public static String encode(String str) {
BASE64Encoder encoder = new BASE64Encoder();
String encode = encoder.encode(str.getBytes());
return encode;
}
}
- 添加映射接口
@Mapper
public interface MapstructConvert {
/**
* 擷取該類自動生成的實作類的執行個體
*/
MapstructConvert INSTANCE = Mappers.getMapper(MapstructConvert.class);
}
- 添加一個
接口,使用interface
的MapStruct
注解修飾;@Mapper
- 使用
Mappers
執行個體(也可以使用INSTANCE
注入,後面會擴充)。Spring
3.2 一對一映射
- 自定義轉換時間格式
通過
dateFormat = "xx"
指定映射的日期格式。
- 指定預設值
如果該值為空,則使用指定的預設值,如:
defaultValue = "-"
- 忽略不映射的字段
可以通過
ignore = true
指定不需要映射的屬性,如:
@Mapping(target = "password", ignore = true)
。
- 嵌套映射
如果一個
DTO
中的值都是從一個對象中的多個嵌套對象映射時,如果不想一個個寫映射,目标可以用
.
表示,如:
@Mapping(source = "role.roleName", target = "roleName")
- 自定義映射
當我們映射
DTO
的時候,如果某些參數的值
MapStruct
的映射配置不能滿足要求,可以使用自定義方法,例如我們對手機号字段借助工具類進行加密後傳回:
@Mapping(target = "phoneNum", expression = "java(cn.van.spring.copy.mapstruct.util.Base64Util.encode(userInfoDO.getPhoneNum()))")
- 完整代碼如下:
@Mappings({
@Mapping(source = "id", target = "userId"),
// 自定義轉換時間格式
@Mapping(source = "gmtBroth", target = "brothStr", dateFormat = "yyyy-MM-dd",defaultValue = "-"),
// 嵌套映射
@Mapping(source = "role.roleName", target = "roleName"),
// 忽略不映射的字段
@Mapping(target = "password", ignore = true),
// 自定義映射
@Mapping(target = "phoneNum", expression = "java(cn.van.spring.copy.mapstruct.util.Base64Util.encode(userInfoDO.getPhoneNum()))"),
})
UserInfoDTO doToDTO(UserInfoDO userInfoDO);
3.3 多參數映射
MapStruct
可以将幾種類型的對象映射為另外一種類型,比如将多個
DO
對象轉換為一個
DTO
@Mappings({
@Mapping(source = "userInfoDO.id", target = "userId"),
@Mapping(source = "userInfoDO.gmtBroth", target = "brothStr", dateFormat = "yyyy-MM-dd",defaultValue = "-"),
@Mapping(source = "userInfoDO.role.roleName", target = "roleName"),
// 忽略不映射的字段
@Mapping(target = "password", ignore = true),
// 自定義映射
@Mapping(target = "phoneNum", expression = "java(cn.van.spring.copy.mapstruct.util.Base64Util.encode(userInfoDO.getPhoneNum()))"),
@Mapping(source = "userExtInfoDO.favorite", target = "favorite"),
})
UserInfoDTO doToDtoMulti(UserInfoDO userInfoDO, UserExtInfoDO userExtInfoDO);
這樣,我們就可以把
UserInfoDO
映射為
UserExtInfoDO
UserInfoDTO
3.4 集合映射
屬性映射關系基于一對一的映射關系。
List<UserInfoDTO> doSToDTOS(List<UserInfoDO> userInfoDOS);
3.5 映射到現有執行個體
上面都是映射并生成一個新的執行個體,如果是想映射到已有的現有執行個體呢?
我們隻需用
@MappingTarget
修飾。
3.6 注入 Spring
上面的示例調用時都是手動建立了一個
MapstructConvert
執行個體,
現在都是
Spring
的生态,
MapStruct
也可以通過
Spring
注入
@Mapper(componentModel = "spring")
public interface SpringMapstructConvert {
/**
* 一對一映射
* @param userInfoDO
* @return
*/
@Mappings({
@Mapping(source = "id", target = "userId"),
// 自定義轉換時間格式,如果為空,給予預設值 "-"
@Mapping(source = "gmtBroth", target = "brothStr", dateFormat = "yyyy-MM-dd",defaultValue = "-"),
// 嵌套映射
@Mapping(source = "role.roleName", target = "roleName"),
// 忽略不映射的字段
@Mapping(target = "password", ignore = true),
// 自定義映射
@Mapping(target = "phoneNum", expression = "java(cn.van.spring.copy.mapstruct.util.Base64Util.encode(userInfoDO.getPhoneNum()))"),
})
UserInfoDTO doToDTO(UserInfoDO userInfoDO);
}
相較于前者:幹掉了初始化的
INSTANCE
,
@Mapper
注解加入了
componentModel = "spring"
注意:預設是以覆寫原有值的方式映射的,如果要保留原有的值,使用 ignore
忽略字段即可。
四、總結
- 與手工編寫映射代碼相比
MapStruct
通過生成繁瑣且易于編寫的代碼來節省時間。遵循約定優于配置方法,
MapStruct
使用合理的預設值,但在配置或實作特殊行為時