【程式碼優化】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
使用合理的預設值,但在配置或實現特殊行為時