一、MapStruct
開發(fā)中,我們經(jīng)常需要將PO轉(zhuǎn)DTO、DTO轉(zhuǎn)PO等一些實(shí)體間的轉(zhuǎn)換。比較出名的有BeanUtil 和ModelMapper等,它們使用簡(jiǎn)單,但是在稍顯復(fù)雜的業(yè)務(wù)場(chǎng)景下力不從心。MapStruct這個(gè)插件可以用來處理domin實(shí)體類與model類的屬性映射,可配置性強(qiáng)。只需要定義一個(gè) Mapper 接口,MapStruct 就會(huì)自動(dòng)實(shí)現(xiàn)這個(gè)映射接口,避免了復(fù)雜繁瑣的映射實(shí)現(xiàn)。MapStruct官網(wǎng)地址: http://mapstruct.org/
二、MapStruct優(yōu)勢(shì)
為什么推薦這個(gè)框架呢,下面說說原因和他的優(yōu)勢(shì)
原因一: 很多項(xiàng)目大量映射通過手動(dòng)get、set,這種寫法非常繁瑣無味,而且沒有技術(shù)含量。甚至中間還牽涉了很多類型轉(zhuǎn)換,嵌套之類的繁瑣操作,非常的愚蠢。
原因二: 有人說apache的BeanUtil. copyProperties可以實(shí)現(xiàn),但是性能差而且容易出異常,很多規(guī)范嚴(yán)禁使用這種途徑。以下是對(duì)幾種對(duì)象映射框架的對(duì)比,大多數(shù)情況下MapStruct性能最高。原理類似于lombok,MapStruct都是在編譯期進(jìn)行實(shí)現(xiàn),而且基于Getter、Setter,,沒有 使用反射所以-般不存在運(yùn)行時(shí)性能問題。
原因三: 方便的映射操作,對(duì)我們平臺(tái)統(tǒng)一代碼有極其重大的意義。簡(jiǎn)化一套代碼真正兼容多個(gè)平臺(tái)的代碼實(shí)現(xiàn)。使得開發(fā)人員多年對(duì)于多個(gè)聯(lián)網(wǎng)平臺(tái)統(tǒng)一代碼的幻想又進(jìn)了一步。
原因四: 在一個(gè)成熟的工程中,尤其是現(xiàn)在的分布式系統(tǒng)中,應(yīng)用與應(yīng)用之間,還有單獨(dú)的應(yīng)用細(xì)分模塊之后,DO 一般不會(huì)讓外部依賴,這時(shí)候需要在提供對(duì)外接口的模塊里放 DTO 用于對(duì)象傳輸,也即是 DO 對(duì)象對(duì)內(nèi),DTO對(duì)象對(duì)外,DTO 可以根據(jù)業(yè)務(wù)需要變更,并不需要映射 DO 的全部屬性。MapStruct 就是這樣的一個(gè)屬性映射工具,只需要定義一個(gè) Mapper 接口,MapStruct 就會(huì)自動(dòng)實(shí)現(xiàn)這個(gè)映射接口,避免了復(fù)雜繁瑣的映射實(shí)現(xiàn)。
原因五: 對(duì)于包裝類是自動(dòng)拆箱封箱操作的,并且是線程安全的。MapStruct不單單有這些功能,還有其他一些復(fù)雜的功能:設(shè)置轉(zhuǎn)換默認(rèn)值和常量。當(dāng)目標(biāo)值是null時(shí)我們可以設(shè)置其默認(rèn)值。
三、MapStruct使用
3.1 單個(gè)對(duì)象轉(zhuǎn)換
3.1.1 新建maven項(xiàng)目,引入依賴MapStruct、Lombok
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.alanchen</groupId>
<artifactId>mapstruct</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<!-- lombok要與mapstruct版本匹配,用同一時(shí)間的版本,不然會(huì)出現(xiàn)各種問題 -->
<org.mapstruct.version>1.3.0.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.6</org.projectlombok.version>
</properties>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-jdk8</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- 要與mapstruct版本匹配,用同一時(shí)間的版本,不然會(huì)出現(xiàn)各種問題 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${org.projectlombok.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
3.1.2 編寫代碼
entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
private Integer id;
private String name;
private Integer age;
private String sex;
}
vo
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserVO {
private Integer id;
/**
* User的屬性是name
*/
private String userName;
private Integer age;
private String sex;
}
convert
import com.alanchen.mapstruct.entity.User;
import com.alanchen.mapstruct.vo.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
/**
* @Mapper 定義這是一個(gè)MapStruct對(duì)象屬性轉(zhuǎn)換接口,在這個(gè)類里面規(guī)定轉(zhuǎn)換規(guī)則
* 在項(xiàng)目構(gòu)建時(shí),會(huì)自動(dòng)生成改接口的實(shí)現(xiàn)類,這個(gè)實(shí)現(xiàn)類將實(shí)現(xiàn)對(duì)象屬性值復(fù)制
*/
@Mapper
public interface UserConvert {
/**
* 獲取該類自動(dòng)生成的實(shí)現(xiàn)類的實(shí)例
* 接口中的屬性都是 public static final 的 方法都是public abstract的
*/
UserConvert instance = Mappers.getMapper(UserConvert.class);
/**
* 這個(gè)方法就是用于實(shí)現(xiàn)對(duì)象屬性復(fù)制的方法
*
* @Mapping 用來定義屬性復(fù)制規(guī)則 source 指定源對(duì)象屬性 target指定目標(biāo)對(duì)象屬性
*
* @param user 這個(gè)參數(shù)就是源對(duì)象,也就是需要被復(fù)制的對(duì)象
* @return 返回的是目標(biāo)對(duì)象,就是最終的結(jié)果對(duì)象
*/
@Mappings({@Mapping(source = "name",target = "userName")})
UserVO toVO(User user);
@Mappings({@Mapping(source = "userName",target = "name")})
User toEntity(UserVO userVO);
}
@Mapper 只有在接口加上這個(gè)注解, MapStruct 才會(huì)去實(shí)現(xiàn)該接口
@Mapper 里有個(gè) componentModel 屬性,主要是指定實(shí)現(xiàn)類的類型,一般用到兩個(gè):
default: 默認(rèn),可以通過 Mappers.getMapper(Class) 方式獲取實(shí)例對(duì)象
spring: 在接口的實(shí)現(xiàn)類上自動(dòng)添加注解
@Component,可通過@Autowired方式注入
@Mapping:屬性映射,若源對(duì)象屬性與目標(biāo)對(duì)象名字一致,會(huì)自動(dòng)映射對(duì)應(yīng)屬性
測(cè)試Client
public class Client {
public static void main(String[] args) {
poToVO();
System.out.println();
voTOPo();
}
private static void poToVO(){
User user = User.builder()
.id(1)
.name("AlanChen")
.age(18)
.sex("1")
.build();
System.out.println("user:"+user);
UserVO userVO = UserConvert.instance.toVO(user);
System.out.println("userVO:"+userVO);
}
private static void voTOPo(){
UserVO userVO = UserVO.builder()
.id(1)
.userName("AlanChen")
.age(18)
.sex("1")
.build();
System.out.println("userVO:"+userVO);
User user = UserConvert.instance.toEntity(userVO);
System.out.println("user:"+user);
}
}
運(yùn)行結(jié)果
user:User(id=1, name=AlanChen, age=18, sex=1)
userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1)
userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1)
user:User(id=1, name=AlanChen, age=18, sex=1)
啟動(dòng)運(yùn)行,會(huì)在target目錄下自動(dòng)生成轉(zhuǎn)換實(shí)現(xiàn)類

3.1.3 跳坑
一開始沒有注意MapStruct與Lombok匹配的問題,導(dǎo)出出現(xiàn)了一些問題,遇到的問題有以下兩個(gè)
問題一:MapStruct生成的實(shí)現(xiàn)類缺失Entity轉(zhuǎn)VO的具體的實(shí)現(xiàn)

問題二:出現(xiàn)java: No property named “XXX“ exists in source parameter(s). Did you mean “null“
MapStruct與Lombok的版本怎么匹配,我也不太清楚,我嘗試著采用MapStruct、Lombok同一時(shí)期的版本,如mapstruct V1.3.0.Final 和 projectlombok V1.18.6都是2019年二月份的版本,問題得到解決。
<!-- lombok要與mapstruct版本匹配,用同一時(shí)間的版本,不然會(huì)出現(xiàn)各種問題 -->
<org.mapstruct.version>1.3.0.Final</org.mapstruct.version>
<org.projectlombok.version>1.18.6</org.projectlombok.version>
問題三:如果對(duì)象有繼承其他父類,轉(zhuǎn)換也會(huì)失敗
3.2 對(duì)象轉(zhuǎn)換返回List
轉(zhuǎn)化 List<> 集合時(shí)必須有 實(shí)體轉(zhuǎn)化,因?yàn)樵趯?shí)現(xiàn)中,List 轉(zhuǎn)換是 for循環(huán)調(diào)用 實(shí)體轉(zhuǎn)化的。所以當(dāng)屬性名不對(duì)應(yīng)時(shí),應(yīng)該在 實(shí)體轉(zhuǎn)化進(jìn)行 @Mappings 的屬性名映射配置,然后list的轉(zhuǎn)換也會(huì)繼承這和屬性的映射。
@Mapper
public interface UserConvert {
/**
* 獲取該類自動(dòng)生成的實(shí)現(xiàn)類的實(shí)例
* 接口中的屬性都是 public static final 的 方法都是public abstract的
*/
UserConvert instance = Mappers.getMapper(UserConvert.class);
/**
* 轉(zhuǎn)換成List
* @param users
* @return
*/
List<UserVO> toVOList(List<User> users);
}
完整代碼為
import com.alanchen.mapstruct.entity.User;
import com.alanchen.mapstruct.vo.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* @Mapper 定義這是一個(gè)MapStruct對(duì)象屬性轉(zhuǎn)換接口,在這個(gè)類里面規(guī)定轉(zhuǎn)換規(guī)則
* 在項(xiàng)目構(gòu)建時(shí),會(huì)自動(dòng)生成改接口的實(shí)現(xiàn)類,這個(gè)實(shí)現(xiàn)類將實(shí)現(xiàn)對(duì)象屬性值復(fù)制
*/
@Mapper
public interface UserConvert {
/**
* 獲取該類自動(dòng)生成的實(shí)現(xiàn)類的實(shí)例
* 接口中的屬性都是 public static final 的 方法都是public abstract的
*/
UserConvert instance = Mappers.getMapper(UserConvert.class);
/**
* 這個(gè)方法就是用于實(shí)現(xiàn)對(duì)象屬性復(fù)制的方法
*
* @Mapping 用來定義屬性復(fù)制規(guī)則 source 指定源對(duì)象屬性 target指定目標(biāo)對(duì)象屬性
*
* @param user 這個(gè)參數(shù)就是源對(duì)象,也就是需要被復(fù)制的對(duì)象
* @return 返回的是目標(biāo)對(duì)象,就是最終的結(jié)果對(duì)象
*/
@Mappings({@Mapping(source = "name",target = "userName")})
UserVO toVO(User user);
@Mappings({@Mapping(source = "userName",target = "name")})
User toEntity(UserVO userVO);
/**
* 轉(zhuǎn)換成List
* @param users
* @return
*/
List<UserVO> toVOList(List<User> users);
}
測(cè)試Client
public class Client {
public static void main(String[] args) {
toList();
}
private static void toList(){
List<User> userList = new ArrayList<User>();
User user1 = User.builder()
.id(1)
.name("AlanChen")
.age(18)
.sex("1")
.build();
userList.add(user1);
User user2 = User.builder()
.id(2)
.name("AlanChen2")
.age(20)
.sex("0")
.build();
userList.add(user2);
List<UserVO> userVOList = UserConvert.instance.toVOList(userList);
System.out.println("userVOList:"+userVOList);
}
}
運(yùn)行結(jié)果
userVOList:[UserVO(id=1, userName=AlanChen, age=18, sex=1), UserVO(id=2, userName=AlanChen2, age=20, sex=0)]
3.3 屬性類型不同,自定義轉(zhuǎn)換類
現(xiàn)在在User里加一個(gè)是否停用的屬性private boolean stop;為boolean類型 ,但UserVO里是否停用的屬性private String stop;為String類型,二者屬性不同,我們需要自己寫一個(gè)轉(zhuǎn)換類
public class UserTransform {
public String booleanToString(boolean value){
if(value){
return "停用";
}
return "未停用";
}
public boolean strToBoolean(String str){
if ("停用".equals(str)) {
return true;
}
return false;
}
}
UserConvert加注解參數(shù)
@Mapper(uses = UserTransform.class)
import com.alanchen.mapstruct.entity.User;
import com.alanchen.mapstruct.vo.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* @Mapper 定義這是一個(gè)MapStruct對(duì)象屬性轉(zhuǎn)換接口,在這個(gè)類里面規(guī)定轉(zhuǎn)換規(guī)則
* 在項(xiàng)目構(gòu)建時(shí),會(huì)自動(dòng)生成改接口的實(shí)現(xiàn)類,這個(gè)實(shí)現(xiàn)類將實(shí)現(xiàn)對(duì)象屬性值復(fù)制
*/
@Mapper(uses = UserTransform.class)
public interface UserConvert {
/**
* 獲取該類自動(dòng)生成的實(shí)現(xiàn)類的實(shí)例
* 接口中的屬性都是 public static final 的 方法都是public abstract的
*/
UserConvert instance = Mappers.getMapper(UserConvert.class);
/**
* 這個(gè)方法就是用于實(shí)現(xiàn)對(duì)象屬性復(fù)制的方法
*
* @Mapping 用來定義屬性復(fù)制規(guī)則 source 指定源對(duì)象屬性 target指定目標(biāo)對(duì)象屬性
*
* @param user 這個(gè)參數(shù)就是源對(duì)象,也就是需要被復(fù)制的對(duì)象
* @return 返回的是目標(biāo)對(duì)象,就是最終的結(jié)果對(duì)象
*/
@Mappings({@Mapping(source = "name",target = "userName")})
UserVO toVO(User user);
@Mappings({@Mapping(source = "userName",target = "name")})
User toEntity(UserVO userVO);
/**
* 轉(zhuǎn)換成List
* @param users
* @return
*/
List<UserVO> toVOList(List<User> users);
}
Client測(cè)試代碼
public class Client {
public static void main(String[] args) {
poToVO();
System.out.println();
voTOPo();
}
private static void poToVO(){
User user = User.builder()
.id(1)
.name("AlanChen")
.age(18)
.sex("1")
.stop(false)
.build();
System.out.println("user:"+user);
UserVO userVO = UserConvert.instance.toVO(user);
System.out.println("userVO:"+userVO);
}
private static void voTOPo(){
UserVO userVO = UserVO.builder()
.id(1)
.userName("AlanChen")
.age(18)
.sex("1")
.stop("停用")
.build();
System.out.println("userVO:"+userVO);
User user = UserConvert.instance.toEntity(userVO);
System.out.println("user:"+user);
}
}
運(yùn)行結(jié)果
user:User(id=1, name=AlanChen, age=18, sex=1, stop=false)
userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1, stop=未停用)
userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1, stop=停用)
user:User(id=1, name=AlanChen, age=18, sex=1, stop=true)
3.4 dateFormat配置日期格式
在User類里繼續(xù)加一個(gè)生日字段private Date birthday;,UserVO類里加生日字段private String birthday;
在UserConvert里指定dateFormat
import com.alanchen.mapstruct.entity.User;
import com.alanchen.mapstruct.vo.UserVO;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;
import java.util.List;
/**
* @Mapper 定義這是一個(gè)MapStruct對(duì)象屬性轉(zhuǎn)換接口,在這個(gè)類里面規(guī)定轉(zhuǎn)換規(guī)則
* 在項(xiàng)目構(gòu)建時(shí),會(huì)自動(dòng)生成改接口的實(shí)現(xiàn)類,這個(gè)實(shí)現(xiàn)類將實(shí)現(xiàn)對(duì)象屬性值復(fù)制
*/
@Mapper(uses = UserTransform.class)
public interface UserConvert {
/**
* 獲取該類自動(dòng)生成的實(shí)現(xiàn)類的實(shí)例
* 接口中的屬性都是 public static final 的 方法都是public abstract的
*/
UserConvert instance = Mappers.getMapper(UserConvert.class);
/**
* 這個(gè)方法就是用于實(shí)現(xiàn)對(duì)象屬性復(fù)制的方法
*
* @Mapping 用來定義屬性復(fù)制規(guī)則 source 指定源對(duì)象屬性 target指定目標(biāo)對(duì)象屬性
*
* @param user 這個(gè)參數(shù)就是源對(duì)象,也就是需要被復(fù)制的對(duì)象
* @return 返回的是目標(biāo)對(duì)象,就是最終的結(jié)果對(duì)象
*/
@Mappings({@Mapping(source = "name",target = "userName"),
@Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")})
UserVO toVO(User user);
@Mappings({@Mapping(source = "userName",target = "name"),
@Mapping(source = "birthday",target = "birthday",dateFormat = "yyyy-MM-dd")})
User toEntity(UserVO userVO);
/**
* 轉(zhuǎn)換成List
* @param users
* @return
*/
List<UserVO> toVOList(List<User> users);
}
Client測(cè)試類
public class Client {
public static void main(String[] args) {
poToVO();
System.out.println();
voTOPo();
}
private static void poToVO(){
User user = User.builder()
.id(1)
.name("AlanChen")
.age(18)
.sex("1")
.stop(false)
.birthday(new Date())
.build();
System.out.println("user:"+user);
UserVO userVO = UserConvert.instance.toVO(user);
System.out.println("userVO:"+userVO);
}
private static void voTOPo(){
UserVO userVO = UserVO.builder()
.id(1)
.userName("AlanChen")
.age(18)
.sex("1")
.stop("停用")
.birthday("1990-05-20")
.build();
System.out.println("userVO:"+userVO);
User user = UserConvert.instance.toEntity(userVO);
System.out.println("user:"+user);
}
}
運(yùn)行結(jié)果
user:User(id=1, name=AlanChen, age=18, sex=1, stop=false, birthday=Mon Nov 22 19:02:44 CST 2021)
userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1, stop=未停用, birthday=2021-11-22)
userVO:UserVO(id=1, userName=AlanChen, age=18, sex=1, stop=停用, birthday=1990-05-20)
user:User(id=1, name=AlanChen, age=18, sex=1, stop=true, birthday=Sun May 20 00:00:00 CDT 1990)
3.5 ignore
ignore: 忽略這個(gè)字段
3.6 多對(duì)一映射
MapStruct 可以將幾種類型的對(duì)象映射為另外一種類型,比如將多個(gè)entity對(duì)象轉(zhuǎn)換為VO。例如:兩個(gè)entity對(duì)象 Item 和 Sku,一個(gè)VO對(duì)象SkuVO
entity
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Item {
private Long id;
private String title;
}
@NoArgsConstructor
@AllArgsConstructor
@Data
public class Sku {
private String code;
private Integer price;
}
convert
@Mapper
public interface ItemConvert {
ItemConvert instance = Mappers.getMapper(ItemConvert.class);
@Mappings({
@Mapping(source = "sku.id",target = "skuId"),
@Mapping(source = "sku.code",target = "skuCode"),
@Mapping(source = "sku.price",target = "skuPrice"),
@Mapping(source = "item.id",target = "itemId"),
@Mapping(source = "item.title",target = "itemName")
})
SkuVO toVO(Item item, Sku sku);
}
四、常用代碼
4.1 expression
@Mapper
public interface DynamicConvert {
DynamicConvert instance = Mappers.getMapper(DynamicConvert.class);
@Mapping(target = "id", expression="java(calendarEntity.getId().toString())" )
CalendarDetailDTO toCalendarDTO(CalendarEntity calendarEntity);
}
@Data
@Document(collection = "calendar")
public class CalendarEntity{
@JsonDeserialize(using = ObjectIdJsonDeserializer.class)
@JsonSerialize(using = ObjectIdJsonSerializer.class)
@JSONField(serializeUsing = ObjectIDSerializer.class, deserializeUsing = ObjectIDSerializer.class)
@MongoId
private ObjectId id;
@Indexed
private Long memberId;
@ApiModelProperty(value = "mobile")
private String mobile;
}
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CalendarDetailDTO implements Serializable {
@ApiModelProperty(value = "動(dòng)態(tài)ID")
private String id;
@ApiModelProperty(value = "會(huì)員ID")
private Long memberId;
@ApiModelProperty(value = "會(huì)員手機(jī)號(hào)")
private String mobile;
}