网站首页 文章专栏 不要再使用 BeanUtils 来复制实体类属性了,试试MapStruct吧
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目录下,可以看到,帮我们多生成了一个实现类:
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; } } }
其实就是帮我们做了set的实现,当字段很多时,就会节省很多时间。
实际工作中,虽然很多时候我们也只需要相同字段映射就行了,但是也会有一些复杂的场景,比如字段名称不一致,字段类型不一致,字段需要转变其他值,字段需要默认值等等,其实这些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等等,但就日常的场景,我上面的几种已经足够了。
有任何意见可以下方评论!
版权声明:本文由星尘阁原创出品,转载请注明出处!