网站首页 文章专栏 不要再使用 BeanUtils 来复制实体类属性了,试试MapStruct吧
不要再使用 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目录下,可以看到,帮我们多生成了一个实现类:

image.pngimage.png

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等等,但就日常的场景,我上面的几种已经足够了。

有任何意见可以下方评论!





版权声明:本文由星尘阁原创出品,转载请注明出处!

本文链接:http://www.52xingchen.cn/detail/86




赞助本站,网站的发展离不开你们的支持!
来说两句吧
大侠留个名吧,或者可以使用QQ登录。
: 您已登陆!可以继续留言。
最新评论