一. 它是什么?
MapStruct是一个代码生成器,它基于约定优于配置的方法极大地简化了Java bean类型之间映射的实现。
生成的映射代码使用简单的方法调用,因此快速,类型安全且易于理解。
二. 为什么要用它?
多层应用程序通常需要在不同的对象模型(例如实体和DTO)之间进行映射。编写此类映射代码是一项繁琐且容易出错的任务。MapStruct旨在通过使其尽可能自动化来简化这项工作。
与其他映射框架相比,MapStruct在编译时生成Bean映射,以确保高性能,允许快速的开发人员反馈和彻底的错误检查。
三. 极简快速接入使用
1. 引入maven依赖:
<properties>
<java.version>1.8</java.version>
<org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
</properties>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
2. 定义实体类dto,po
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodsInfoDTO {
private Integer id;
private Long hotelId;
private Long goodsId;
private String goodsName;
private Integer goodsStatus;
private Integer confirmType;
private String createTime;
private Date updateTime;
}
@Data
public class GoodsInfoPO {
private Integer id;
private Long hotelId;
private Long goodsId;
private String goodsName;
private Integer goodsStatus;
private Integer goodsType;
private Integer confirmType;
private String createTime;
private Date updateTime;
}
注意:这两个类属性字段一模一样,且po中多个了 goodsStatus字段
3. 编写转换接口
如果你想对GoodsInfo类的dto,po,vo之间进行转换,那么可以新建一个GoodsInfoMapper接口,里面可以进行该类的各种转换,这也是mapstract的一个优势,所有的实体类之间的转换全都维护在一个地方,方便维护,以及复用,像传统的set,BeanUtils 等都会写大量的重复代码,无法很好的复用。
@Mapper(componentModel = "spring")
public interface GoodsInfoMapper {
/**
* 无状态且线程安全
*/
GoodsInfoMapper INSTANCE = Mappers.getMapper( GoodsInfoMapper.class );
/**
* dto转为po,什么条件都不加,默认两个类字段一样的数据转移
*/
GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto);
}
如此便写好了一个GoodsInfoDTO转GoodsInfoPO的方法。
4. 测试
public static void main(String[] args) {
GoodsInfoDTO goodsInfoDTO = GoodsInfoDTO.builder()
.id(1)
.hotelId(1L)
.goodsId(1L)
.goodsName("测试商品")
.goodsStatus(2)
.confirmType(3)
.createTime("2021-4-24 11:03")
.updateTime(new Date())
.build();
GoodsInfoPO goodsInfoPO = GoodsInfoMapper.INSTANCE.goodsInfoDtoToPo(goodsInfoDTO);
System.out.println("转换后goodsInfoPO:" + goodsInfoPO);
}
我们在Mapper里面实例化一个单例,通过该单例进行方法调用,这种方法无需spring等CI框架即可使用,如果想要spring注入,也是支持的,可自行查询方式。
看下结果:
转换后goodsInfoPO:GoodsInfoPO(id=1, hotelId=1, goodsId=1, goodsName=测试商品, goodsStatus=2, goodsType=null, confirmType=3, createTime=2021-4-24 11:03, updateTime=Sat Apr 24 11:13:19 CST 2021)
可以发现,字段已经全被映射过来了,且dto中没有的 goodsStatus字段,没有映射,为null
5. 分析下原理
我们只是定义了一个接口,mapstarct就帮我们实现了具体的转换,那么是怎么实现的呢?其实是类似与 lombok技术,在编译器帮我们生成了一个实现类。我们看下上面我们定义的接口,mapstract生成的实现类是什么样。
在target目录下,可以看到,帮我们多生成了一个实现类:
其实就是帮我们做了set的实现,当字段很多时,就会节省很多时间。
public class GoodsInfoMapperImpl implements GoodsInfoMapper {
public GoodsInfoMapperImpl() {
}
public GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto) {
if (goodsInfoDto == null) {
return null;
} else {
GoodsInfoPO goodsInfoPO = new GoodsInfoPO();
goodsInfoPO.setId(goodsInfoDto.getId());
goodsInfoPO.setHotelId(goodsInfoDto.getHotelId());
goodsInfoPO.setGoodsId(goodsInfoDto.getGoodsId());
goodsInfoPO.setGoodsName(goodsInfoDto.getGoodsName());
goodsInfoPO.setGoodsStatus(goodsInfoDto.getGoodsStatus());
goodsInfoPO.setConfirmType(goodsInfoDto.getConfirmType());
goodsInfoPO.setCreateTime(goodsInfoDto.getCreateTime());
goodsInfoPO.setUpdateTime(goodsInfoDto.getUpdateTime());
return goodsInfoPO;
}
}
}
四. 复杂场景的应用
实际工作中,虽然很多时候我们也只需要相同字段映射就行了,但是也会有一些复杂的场景,比如字段名称不一致,字段类型不一致,字段需要转变其他值,字段需要默认值等等,其实这些mapstarct都是支持的,且很方便。
1. 字段名称不一致
比如:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodsInfoDTO {
private Integer id;
private String name;
private Integer status;
}
@Data
public class GoodsInfoPO {
private Integer id;
private String goodsName;
private Integer goodsStatus;
}
上面 name -> goodsName,status -> goodsStatus
则只需要在mapper中加上对应的映射即可,source为源数据中的字段,target为目标类中的字段:
@Mapper
public interface GoodsInfoMapper {
GoodsInfoMapper INSTANCE = Mappers.getMapper( GoodsInfoMapper.class );
@Mapping(source = "name", target = "goodsName")
@Mapping(source = "status", target = "goodsStatus")
GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto);
}
2. 字段类型不一致
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodsInfoDTO {
private Integer id;
private String createTime;
private LocalDateTime updateTime;
}
@Data
public class GoodsInfoPO {
private Integer id;
private LocalDateTime createDate;
private String updateDate;
}
如上,字符串类型时间 与 localDateTime 的互相转换,且字段也不一致
@Mapper
public interface GoodsInfoMapper {
GoodsInfoMapper INSTANCE = Mappers.getMapper( GoodsInfoMapper.class );
@Mapping(source = "createTime", target = "createDate", dateFormat = "yyyy-MM-dd HH:mm")
@Mapping(source = "updateTime", target = "updateDate", dateFormat = "yyyy-MM-dd HH:mm")
GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto);
}
使用 dateFormat 指定格式即可转换,其实mapstract可以自己处理大部分的转换,例如,如果某个属性int在源Bean中是类型但String在目标Bean中是类型,则生成的代码将分别通过分别调用String#valueOf(int)和来透明地执行转换Integer#parseInt(String)
当前,以下转换将自动应用:
- 在所有Java原语数据类型及其对应的包装器类型之间(例如在int和之间Integer,boolean以及Boolean等)之间。生成的代码是已知的null,即,当将包装器类型转换为相应的原语类型时,null将执行检查。
- 在所有Java原语数字类型和包装器类型之间,例如在int和long或byte和之间Integer(从较大的数据类型转换为较小的数据类型(例如从long到int)可能会导致值或精度损失)。
- 所有Java基本类型之间(包括其包装)和String之间,例如int和String或Boolean和String。java.text.DecimalFormat可以指定理解的格式字符串。
格式化内部原理,其实时用了 Da'teTimeFormatter 帮我们格式话的,如果是 Date 类型的话,则是用 SimpleDateFormat 来格式化的。
3. 嵌套对象
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodsInfoDTO {
private Integer id;
private String name;
private Integer status;
private BookRuleDTO bookRule;
}
@Data
public class GoodsInfoPO {
private Integer id;
private String goodsName;
private Integer goodsStatus;
private BookRuleDTO bookRule;
}
如上,dto -> po中,有一个别的对象,类型一致,且字段名称一致,则无需动,直接就能映射过去
如过待转的对象类型不一致呢?
@Data
public class GoodsInfoPO {
private Integer id;
private String goodsName;
private Integer goodsStatus;
private BookRulePO bookRule;
}
如上,类型不一致,但是 BookRuleDTO 与 BookRulePO 中字段名称且类型一致的话,也无需变动,直接映射。
如果嵌套的对象需要特殊的映射呢?
@Data
@Builder
public class BookRuleDTO {
private Integer checkinMin;
private Integer checkinMax;
private String countMin;
private String countMax;
}
@Data
public class BookRulePO {
private Integer checkinMin;
private Integer checkinMax;
private Integer roomCountMin;
private Integer roomCountMax;
}
如上,嵌套的对象,string类型的 countMin countMax -> integer 类型的 roomCountMin roomCountMax
@Mapping(source = "name", target = "goodsName")
@Mapping(source = "status", target = "goodsStatus")
@Mapping(source = "bookRule.countMin", target = "bookRule.roomCountMin")
@Mapping(source = "bookRule.countMax", target = "bookRule.roomCountMax")
GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto);
可以通过用 字段.嵌套对象字段 这种方式来指定映射,同时这种方式也可以让嵌套对象的属性,映射到外层对象上
或者可以新建一个方法指定嵌套对象的映射规则,为引用的对象类型定义一个映射方法。
@Mapping(source = "name", target = "goodsName")
@Mapping(source = "status", target = "goodsStatus")
GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto);
@Mapping(source = "countMin", target = "roomCountMin")
@Mapping(source = "countMax", target = "roomCountMax")
BookRulePO bookRuleDtoToPo(BookRuleDTO bookRuleDto);
如上,也可以,这样就可以映射任意深的对象。
4. 特殊值的映射,比如 1 -> 可用 2 -> 不可用
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodsInfoDTO {
private Integer id;
private String name;
private Integer status;
}
@Data
public class GoodsInfoPO {
private Integer id;
private String goodsName;
private Integer goodsStatus;
private String goodsStatusDesc;
}
比如,在po中多了一个状态的描述信息,我知道映射规则,应当怎么处理呢?这就用到了mapstract中自定义表达式了
@Mapping(source = "name", target = "goodsName")
@Mapping(target = "goodsStatus", ignore = true)
@Mapping(target = "goodsStatusDesc", expression = "java(com.yx.transfer.GoodsStatusConvent.statusConvent(goodsInfoDto.getStatus()))")
GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto);
package com.yx.transfer;
public class GoodsStatusConvent {
public static String statusConvent(Integer status){
if (status == null) return "";
if (status == 1) return "可用";
if (status == 2) return "不可用";
return "";
}
}
在表达式中,用全限定名+方法来实现自定义转换,自己写一个转换的方法,注意的是,表达式中自定义的方法入参就不能直接写 字段名了,要用 goodsInfoDto.getStatus(),如果有的值我们不想映射,可以用ignore = true,来忽略映射,如上 goodsStatus字段。
我们看下生成的实现类:
public GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto) {
if (goodsInfoDto == null) {
return null;
} else {
GoodsInfoPO goodsInfoPO = new GoodsInfoPO();
goodsInfoPO.setGoodsName(goodsInfoDto.getName());
goodsInfoPO.setId(goodsInfoDto.getId());
goodsInfoPO.setGoodsStatusDesc(GoodsStatusConvent.statusConvent(goodsInfoDto.getStatus()));
return goodsInfoPO;
}
}
其实就是导入该类,并在set时,调用转化的方法,自定义表达式,可以很大程度的自己拓展。
5. 映射添加默认值
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodsInfoDTO {
private Integer id;
private String name;
private Integer status;
}
@Data
public class GoodsInfoPO {
private Integer id;
private String goodsName;
private Integer goodsStatus;
}
如上,我们想要当 name字段为null时,让其映射的值为 "默认商品",status字段为null时,映射的值为1,则可以加上 defaultValue:
@Mapping(source = "name", target = "goodsName", defaultValue = "默认商品")
@Mapping(source = "status", target = "goodsStatus", defaultValue = "1")
GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto);
如果直接就想映射的值为某个值,那么可以用 constant,则不管源字段值为啥,目标映射字段都是指定的值
@Mapping(target = "goodsName", constant = "默认商品")
@Mapping(target = "goodsStatus", constant = "1")
GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto);
6. 映射集合
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodsInfoDTO {
private Integer id;
private String name;
private Integer status;
}
@Data
public class GoodsInfoPO {
private Integer id;
private String goodsName;
private Integer goodsStatus;
}
字段名称不一致的两个实体,集合转换其实一样,当字段名称一致时:
List goodsInfoDtoListToPoList(List goodsInfoDTOList);
一行搞定
但是如果不一样,则需要加一个类型转换的规则:
@Mapping(source = "name", target = "goodsName", defaultValue = "默认商品")
@Mapping(source = "status", target = "goodsStatus", defaultValue = "1")
GoodsInfoPO goodsInfoDtoToPo(GoodsInfoDTO goodsInfoDto);
List goodsInfoDtoListToPoList(List goodsInfoDTOList);
他会根据入参,出参的类型,自动识别,在遍历集合的时候,调用实体类的转换规则。
7. 多个源的映射,即两个源对象,各自有一些属性需要映射到目标对象
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodsInfoDTO {
private Integer id;
private String name;
private Integer status;
}
@Data
@Builder
public class BookRuleDTO {
private Integer checkinMin;
private Integer checkinMax;
private String countMin;
private String countMax;
}
@Data
public class GoodsInfoPO {
private Integer id;
private String goodsName;
private Integer goodsStatus;
private Integer checkinMin;
private Integer checkinMax;
private Integer roomCountMin;
private Integer roomCountMax;
}
如上,要将 GoodsInfoDTO 和 BookRuleDTO 两个类的字段映射到 GoodsInfoPO 中
@Mapping(source = "goodsInfoDto.name", target = "goodsName", defaultValue = "默认商品")
@Mapping(source = "goodsInfoDto.status", target = "goodsStatus", defaultValue = "1")
@Mapping(source = "bookRuleDto.checkinMin", target = "checkinMin")
@Mapping(source = "bookRuleDto.checkinMax", target = "checkinMax")
@Mapping(source = "bookRuleDto.countMin", target = "roomCountMin")
@Mapping(source = "bookRuleDto.countMax", target = "roomCountMax")
GoodsInfoPO goodsInfoDtoAndBookRuleDtoToPo(GoodsInfoDTO goodsInfoDto,BookRuleDTO bookRuleDto);
可以在接口入参,增加多个对象,分别指定映射的字段
8. 更新现有的对象
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class GoodsInfoDTO {
private Integer id;
private String name;
private Integer status;
}
@Data
public class GoodsInfoPO {
private Integer id;
private String goodsName;
private Integer goodsStatus;
private Integer checkinMin;
private Integer checkinMax;
private Integer roomCountMin;
private Integer roomCountMax;
}
如上,将 dto -> po
po本身已具备 checkInMin checkInMax roomCountMin roomCountMax四个属性,只需要将dto中的几个字段映射更新
@Mapping(source = "name", target = "goodsName", defaultValue = "默认商品")
@Mapping(source = "status", target = "goodsStatus", defaultValue = "1")
void updateGoodsInfoPoFromDto(GoodsInfoDTO goodsInfoDto, @MappingTarget GoodsInfoPO goodsInfoPO);
public static void main(String[] args) {
GoodsInfoDTO goodsInfoDTO = GoodsInfoDTO.builder()
.id(1)
.name("测试商品")
.status(2)
.build();
GoodsInfoPO goodsInfoPO = new GoodsInfoPO();
goodsInfoPO.setCheckinMin(1);
goodsInfoPO.setCheckinMax(2);
goodsInfoPO.setRoomCountMin(1);
goodsInfoPO.setRoomCountMax(2);
GoodsInfoMapper.INSTANCE.updateGoodsInfoPoFromDto(goodsInfoDTO,goodsInfoPO);
System.out.println("转换后goodsInfoPO:" + goodsInfoPO);
}
如上即可完成更新已有对象。
五. 总结
mapstract是一个很好用的工具,熟悉了后可以很快的copy各种对象属性,而且其是在编译器生成代码,使用原生的set。所以对比 BeanUtils的反射,性能要高得多。
mapstract还有一些更高级的用法,比如自定义注解,映射配置继承,共享配置,spi等等,但就日常的场景,上面的几种已经足够了。