还在用 BeanUtils ?

2,547 阅读8分钟

前言

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。后端程序经常和 对象 你来我往,你来我往就算了,每个对象都还是 new 的,这怎一个 字了得啊?!哈哈哈~大家别误会哈,我说的对象是 Object。谈到对象,那就不得不说说对象与对象之间的互相转换,这时就需要有一个专门用来解决转换问题的工具,毕竟每一个字段都 get/set 会很麻烦,麻烦是其次,很影响代码的优雅性,就让人感觉很 low;可能有人就会说了,为什么不用 org.springframework.beans.BeanUtils 浅拷贝完成对象与对象之间的转换呢?我只能说 BeanUtils 不够灵活,如果属性名不同就需要手动赋值,个人感觉没有 MapStruct 香,不信请接着看正文 ☟

须知

何为 VO、DTO、DO以及PO?

答:

  1. VO(View Object):视图对象,用于视图页面层,将制定页面或组件中的数据封装起来组合成一个对象
  2. DTO(Data Transfer Object):数据传输对象,这个概念来源于 J2EE 的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,用于展示层与服务层之间的数据传输对象
  3. DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体类
  4. PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系

MapStruct

What

官方地说,MapStruct 是一个代码生成器,它遵循约定优于配置的原则,极大地简化了 Java bean 类型之间映射的实现,只需要定义一个 Mapper 接口,MapStruct 就会自动实现这个映射接口,避免了复杂繁琐的映射实现。

Why

  • 生成的映射代码使用简单的方法调用,因此速度快类型安全易于理解

  • MapStruct 旨在通过尽可能自动化来简化对象与对象模型之间相互转换这一项乏味且容易出错的任务

  • 与其他映射框架相比,MapStruct 在编译时生成 bean 映射,以确保 高性能,允许快速的开发人员 反馈彻底的错误检查

How

第一步:引 MapStruct 依赖

    <properties>
        <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
    </properties>
    
    <!--MapStruct-->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.8.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>org.mapstruct</groupId>
                            <artifactId>mapstruct-processor</artifactId>
                            <version>${org.mapstruct.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
         </plugins>
    </build>

写实体类

该类由 MP 根据 tb_fruit 表结构自动生成的一个实体类,涉及到八个字段

/**
 * <p>
 *  水果实体类
 * </p>
 *
 * @author HUALEI
 * @since 2021-09-04
 */

@TableName("tb_fruit")
@Data
public class Fruit {

      /**
     * 主键id
     */
      @TableId(value = "fruit_id", type = IdType.AUTO)
      private Long fruitId;

      /**
     * 水果名称
     */
      private String fruitName;

      /**
     * 水果销量
     */
      private Integer fruitSale;

      /**
     * 本地图标地址
     */
      private String localIcon;

      /**
     * 备用的网络图标地址
     */
      private String spareIcon;

      /**
     * 逻辑删除 0 未删除,1 被删除
     */
      @TableLogic
      private Byte isDeleted;

      /**
     * 修改更新时间
     */
      @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
      @TableField(fill = FieldFill.INSERT_UPDATE)
      private Date updateTime;

      /**
     * 乐观锁标识,用于控制唯一的修改操作
     */
      @Version
      private Integer version;
}

写转换对象

根据实体类,明确转换的对象应该包含哪些字段属性

这里我拿两个 VO 对象举例,其中一个 FruitVO 对象中属性从 Fruit 中任意选取了六个,属性名保持一致,但另一个 FruitVO2 总共三个字段,都和实体类中属性名不同

@Data
public class FruitVO implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long fruitId;
    private String fruitName;
    private Integer fruitSale;
    private String localIcon;
    private String spareIcon;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    private Date updateTime;
}

@Data
public class FruitVO2 implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    private String name;
    private Integer sale;
}

写映射接口

明确要转换的对象后,编写映射接口,目的是将源对象属性和目标对象属性形成一一对应的映射关系

注意:@Mapper 注解是 org.mapstruct.factory.Mappers 下的,千万不要引入错咯!!

@Mapper
public interface FruitMap {

    FruitMap mapper = Mappers.getMapper(FruitMap.class);

    @Mappings({
            @Mapping(source = "fruitId", target = "fruitId"),
            @Mapping(source = "fruitName", target = "fruitName"),
            @Mapping(source = "fruitSale", target = "fruitSale"),
            @Mapping(source = "localIcon", target = "localIcon"),
            @Mapping(source = "spareIcon", target = "spareIcon"),
            @Mapping(source = "updateTime", target = "updateTime")
    })
    FruitVO pojo2vo(Fruit fruit);
    Fruit vo2pojo(FruitVO fruitVO);

    @Mappings({
            @Mapping(source = "fruitId", target = "id"),
            @Mapping(source = "fruitName", target = "name"),
            @Mapping(source = "fruitSale", target = "sale")
    })
    FruitVO2 pojo2vo2(Fruit fruit);

    List<FruitVO> pojoList2vo(List<Fruit> fruits);

    List<FruitVO2> pojoList2vo2(List<Fruit> fruits);

    @Mappings({
            @Mapping(source = "updateTime", target = "orderId", qualifiedByName = "setOrderIdByUpdateTime"),
            @Mapping(source = "fruitId", target = "shortage.id"),
            @Mapping(source = "fruitSale", target = "shortage.lackNum")
    })
    PurchaseList pojo2PurchaseList(Fruit fruit);

    @Named("setOrderIdByUpdateTime")
    default Long setOrderIdByUpdateTime(Date updateTime) {
        return System.currentTimeMillis() - updateTime.getTime();
    }
}

这段代码开头,创建了一个 FruitMap 类型的实例 mapper ,这是我们接下来调用该接口中方法的 入口

  1. 首先,看第一个接口方法,pojo2vo 是将实体类对象转换成 VO 对象,由于它们之间的属性名一致,是可以不使用 @Mappings 一一与目标对象中的属性对应的,不过写了也没事…… vo2pojo 是将 VO 转换为 PO 已经声明了它们属性之间的映射关系了,所以 MapStruct 底层实现会自动根据声明的规则进行转换
  2. 其次,看 pojo2vo2 方法使用了 @Mappings 注解规定了映射关系,使得在不同属性名的情况下完成对应关系
  3. 最后,pojoList2vopojoList2vo2 是将 PO 集合转换成 VO 集合的,由于对象层面都能相互转换了,集合之间的转换只不过是外面套一层 for 循环罢了

调用接口 | 测试转换

自动注入对象转换接口,调用接口中对应的方法完成转换即可

@SpringBootTest
class MapStructToolTests {

    @Autowired
    private FruitMapper fruitMapper;

    @Test
    // 使用 MapStruct 工具,将实体类转 VO 对象,VO 中的字段和 pojo 字段一致的情况
    void pojo_to_vo_field_same() {
        Fruit fruit = fruitMapper.selectById(18L);
        FruitVO fruitVO = FruitMap.mapper.pojo2vo(fruit);
        System.out.println(fruitVO);
    }

    @Test
    // 使用 MapStruct 工具,将实体类转 VO 对象,VO 中的字段和 pojo 字段不一致的情况
    void pojo_to_vo2_field_different() {
        Fruit fruit = fruitMapper.selectById(15L);
        // 使用了 @Mappings( { @Mapping(...), ...}) 注解
        FruitVO2 fruitVO2 = FruitMap.mapper.pojo2vo2(fruit);
        System.out.println(fruitVO2);
    }

    @Test
    // 不仅能实现实体类到 VO 的转换,还能反着来
    void vo2_to_pojo_field_same() {
        Fruit fruit = fruitMapper.selectById(1L);
        System.out.println(fruit);
        FruitVO fruitVO = FruitMap.mapper.pojo2vo(fruitMapper.selectById(2L));
        System.out.println(fruitVO);
        Fruit fruit1 = FruitMap.mapper.vo2pojo(fruitVO);
        System.out.println(fruit1);
    }
    
    @Test
    // 基于对象转换的集合转换(字段相同)
    void pojo_list_to_vo_field_same() {
        QueryWrapper<Fruit> wrapper = new QueryWrapper<>();
        wrapper.le("fruit_id", 3);
        List<Fruit> fruits = fruitMapper.selectList(wrapper);
        List<FruitVO> fruitVOs = FruitMap.mapper.pojoList2vo(fruits);
        fruitVOs.forEach(System.out::println);
    }

    @Test
    // 基于对象转换的集合转换(字段不相同)
    void pojo_list_to_vo2_field_different() {
        QueryWrapper<Fruit> wrapper = new QueryWrapper<>();
        wrapper.le("fruit_id", 3);
        List<Fruit> fruits = fruitMapper.selectList(wrapper);
        // pojo -> vo 之间的映射只要写一次即可
        List<FruitVO2> fruitVO2s = FruitMap.mapper.pojoList2vo2(fruits);
        for (FruitVO2 fruitVO2 : fruitVO2s) {
            System.out.println(fruitVO2);
        }
    }
}

全部通过测试,bingo ~ 小伙伴,可以尝试尝试哦,用了都说香……

其他用法

属性映射对象

随意乱七八糟,写两个类 o( ̄︶ ̄)o

/**
 * 水果采购单
 * MapStruct 测试类
 */

@Data
public class PurchaseList {

    // 订单编号
    private Long orderId;
    // 水果缺货额
    private Shortage shortage;
}
/**
 * 水果缺货额
 * MapStruct 测试类
 */

@Data
public class Shortage {

    // 缺额水果 id
    private Long id;
    // 缺货量
    private Integer lackNum;
}

PurchaseList 中含有两个属性,其中一个属性是对象,如果要从 Fruit 映射到这个对象,MapStruct 面对这种复杂的映射关系该如何做呢?

FruitMap 中加入如下方法:

注意:在接口中写方法的实现,要使用 default 关键字

@Mappings({
        @Mapping(source = "updateTime", target = "orderId", qualifiedByName = "setOrderIdByUpdateTime"),
        @Mapping(source = "fruitId", target = "shortage.id"),
        @Mapping(source = "fruitSale", target = "shortage.lackNum")
})
PurchaseList pojo2PurchaseList(Fruit fruit);

映射关系为:水果 id 映射到 缺额水果 id水果销量 映射到 缺额数量

掘友:qualifiedByName 这个属性有什么用呢?如果有这个疑问的,请继续往下 ☟

转换中间值处理

倘若,我规定 PurchaseList 中的订单编号属性值是通过当前时间 减去 数据更新时间时间差,那这时该怎么做呢?能不能在转换之前使用一个中间函数处理一下完成赋值呢?答案是肯定的,如下:

@Named("setOrderIdByUpdateTime")
default Long setOrderIdByUpdateTime(Date updateTime) {
    return System.currentTimeMillis() - updateTime.getTime();
}

此时,使用 @Named 注解将 setOrderIdByUpdateTime() 这个中间处理函数命名为 setOrderIdByUpdateTime,然后使用 qualifiedByName 将源对象中 updataTime 属性通过指定的名字找到中间处理函数并且将该属性入参,返回值就是转换的结果,是不是很简单呢?

底层实现

不知名的掘友:我想康康 MapStruct 底层到底干了什么?

通过 yydsIDE 的反编译功能查看编译后的实现类,如下:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-09-09T13:27:31+0800",
    comments = "version: 1.4.2.Final, compiler: javac, environment: Java 1.8.0_221 (Oracle Corporation)"
)
public class FruitMapImpl implements FruitMap {

    @Override
    public FruitVO pojo2vo(Fruit fruit) {
        if ( fruit == null ) {
            return null;
        }

        FruitVO fruitVO = new FruitVO();

        fruitVO.setFruitId( fruit.getFruitId() );
        fruitVO.setFruitName( fruit.getFruitName() );
        fruitVO.setFruitSale( fruit.getFruitSale() );
        fruitVO.setLocalIcon( fruit.getLocalIcon() );
        fruitVO.setSpareIcon( fruit.getSpareIcon() );
        fruitVO.setUpdateTime( fruit.getUpdateTime() );

        return fruitVO;
    }

    @Override
    public Fruit vo2pojo(FruitVO fruitVO) {
        if ( fruitVO == null ) {
            return null;
        }

        Fruit fruit = new Fruit();

        fruit.setFruitId( fruitVO.getFruitId() );
        fruit.setFruitName( fruitVO.getFruitName() );
        fruit.setFruitSale( fruitVO.getFruitSale() );
        fruit.setLocalIcon( fruitVO.getLocalIcon() );
        fruit.setSpareIcon( fruitVO.getSpareIcon() );
        fruit.setUpdateTime( fruitVO.getUpdateTime() );

        return fruit;
    }

    @Override
    public FruitVO2 pojo2vo2(Fruit fruit) {
        if ( fruit == null ) {
            return null;
        }

        FruitVO2 fruitVO2 = new FruitVO2();

        fruitVO2.setId( fruit.getFruitId() );
        fruitVO2.setName( fruit.getFruitName() );
        fruitVO2.setSale( fruit.getFruitSale() );

        return fruitVO2;
    }

    @Override
    public List<FruitVO> pojoList2vo(List<Fruit> fruits) {
        if ( fruits == null ) {
            return null;
        }

        List<FruitVO> list = new ArrayList<FruitVO>( fruits.size() );
        for ( Fruit fruit : fruits ) {
            list.add( pojo2vo( fruit ) );
        }

        return list;
    }

    @Override
    public List<FruitVO2> pojoList2vo2(List<Fruit> fruits) {
        if ( fruits == null ) {
            return null;
        }

        List<FruitVO2> list = new ArrayList<FruitVO2>( fruits.size() );
        for ( Fruit fruit : fruits ) {
            list.add( pojo2vo2( fruit ) );
        }

        return list;
    }

    @Override
    public PurchaseList pojo2PurchaseList(Fruit fruit) {
        if ( fruit == null ) {
            return null;
        }

        PurchaseList purchaseList = new PurchaseList();

        purchaseList.setShortage( fruitToShortage( fruit ) );
        purchaseList.setOrderId( setOrderIdByUpdateTime( fruit.getUpdateTime() ) );

        return purchaseList;
    }

    protected Shortage fruitToShortage(Fruit fruit) {
        if ( fruit == null ) {
            return null;
        }

        Shortage shortage = new Shortage();

        shortage.setId( fruit.getFruitId() );
        shortage.setLackNum( fruit.getFruitSale() );

        return shortage;
    }
}

看完之后,是不是大吃一惊,这不就是 get/set 的调用嘛,也不是什么高大上的东西啊!确实,MapStruct 本质上就是一个代码生成器,能够帮助我们省去很多繁琐、重复的事情,保证快捷方便的同时,还很便于理解、代码编写也容易,和 BeanUtils 相比灵活性更强

综上,你是选择 BeanUtils 呢?还是选择学习 MapStruct ?希望对看到这里的你有一定的帮助和启发 (^▽^)

更多

如果要探索更多且更详细的使用方式,可以参考 MapStruct 官网 哆啦A梦的传送门

结尾

撰文不易,欢迎大家点赞、评论,你的关注、点赞是我坚持的不懈动力,感谢大家能够看到这里!Peace & Love。