关于前端:丢掉那些BeanUtils工具类吧MapStruct真香

2次阅读

共计 11830 个字符,预计需要花费 30 分钟才能阅读完成。

BeanUtils 工具类

应用 MapStruct 之前咱们是这样来转换对象的:

package com.spring.security.demo.utils.bean;

import org.springframework.beans.BeanUtils;

/**
 * @author wulongbo
 * @version 1.0
 * @date 2020/6/11 9:17
 * @description
 */
public class BabaBeanUtils<Vo, Dto> {

    /**
     * dot 转换为 Do 工具类
     *
     * @param voEntity
     * @param dtoClass
     * @return
     */
    public static <Dto> Dto voToDto(Object voEntity, Class<Dto> dtoClass) {
        // 判断 VoSF 是否为空!
        if (voEntity == null) {return null;}
        // 判断 DtoClass 是否为空
        if (dtoClass == null) {return null;}
        try {Dto newInstance = dtoClass.newInstance();
            BeanUtils.copyProperties(voEntity, newInstance);
            // Dto 转换 Do
            return newInstance;
        } catch (Exception e) {return null;}
    }

    // 前面汇合类型带封装
}

前面读者能够参照 BeanUtils 自行批改 PersonConverter 接口为泛型。

为什么须要 MapStruct?

首先,咱们先说一下 MapStruct 这类框架实用于什么样的场景,为什么市面上会有这么多的相似的框架。

在软件体系架构设计中,分层式构造是最常见,也是最重要的一种构造。很多人都对三层架构、四层架构等并不生疏。

甚至有人说:“ 计算机科学畛域的任何问题都能够通过减少一个间接的中间层来解决,如果不行,那就加两层。”

然而,随着软件架构分层越来越多,那么各个档次之间的数据模型就要面临着互相转换的问题,典型的就是咱们能够在代码中见到各种 O,如 DO、DTO、VO 等。

个别状况下,同样一个数据模型,咱们在不同的档次要应用不同的数据模型。 如在数据存储层,咱们应用 DO 来形象一个业务实体;在业务逻辑层,咱们应用 DTO 来示意数据传输对象;到了展现层,咱们又把对象封装成 VO 来与前端进行交互。

那么,数据的从前端透传到数据长久化层(从长久层透传到前端),就须要进行对象之间的互相转化,即在不同的对象模型之间进行映射。

通常咱们能够应用 get/set 等形式逐个进行字段映射操作,如:

personDTO.setName(personDO.getName());

personDTO.setAge(personDO.getAge());

personDTO.setSex(personDO.getSex());

personDTO.setBirthday(personDO.getBirthday());

然而,编写这样的映射代码是一项简短且容易出错的工作。MapStruct 等相似的框架的指标是通过自动化的形式尽可能多地简化这项工作。

MapStruct 的应用

MapStruct(https://mapstruct.org/)是一种代码生成器,它极大地简化了基于 ” 约定优于配置 ” 办法的 Java bean 类型之间映射的实现。生成的映射代码应用纯办法调用,因而疾速、类型平安且易于了解。

 约定优于配置,也称作按约定编程,是一种软件设计范式,旨在缩小软件开发人员需做决定的数量,取得简略的益处,而又不失灵活性。

假如咱们有两个类须要进行相互转换,别离是 PersonDO 和 PersonDTO,类定义如下:

package com.spring.security.demo.Do;

import lombok.Data;

import java.util.Date;

/**
 * @Author wulongbo
 * @Date 2021/1/6 16:36
 * @Version 1.0
 */
@Data
public class PersonDO {

    private Integer id;

    private String name;

    private int age;

    private Date birthday;

    private String gender;

    private String address;

}
package com.spring.security.demo.dto;

import com.spring.security.demo.base.GenderEnum;
import com.spring.security.demo.entity.HomeAddress;
import lombok.Data;

import java.util.Date;

@Data
public class PersonDTO {

    public String userName;

    private Integer age;

    private Date birthday;

    private GenderEnum gender;

    private HomeAddress address;

}
package com.spring.security.demo.entity;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @Author wulongbo
 * @Date 2021/1/6 17:26
 * @Version 1.0
 */

@Data
@AllArgsConstructor
@NoArgsConstructor
public class HomeAddress {

    private String province;

    private String city;

    private String area;

    private String street;

    private String towns;

    private String community;

    // 门牌号
    private String houseNums;
}

咱们演示下如何应用 MapStruct 进行 bean 映射。
想要应用 MapStruct,首先须要依赖他的相干的 jar 包,应用 maven 依赖形式如下:

<!--MapStruct 依赖 -->
<!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct-jdk8 -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-jdk8</artifactId>
        <version>1.0.0.Final</version>
    </dependency>
<!--https://mvnrepository.com/artifact/org.mapstruct/mapstruct-processor -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct-processor</artifactId>
        <version>1.0.0.Final</version>
    </dependency>

之后,咱们须要定义一个做映射的接口,次要代码如下:

package com.spring.security.demo.service;

import com.alibaba.fastjson.JSON;
import com.spring.security.demo.Do.PersonDO;
import com.spring.security.demo.dto.PersonDTO;
import com.spring.security.demo.entity.HomeAddress;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

import java.util.Map;

/**
 * @Author wulongbo
 * @Date 2021/1/6 16:38
 * @Version 1.0
 */

@Mapper
public interface PersonConverter {PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    @Mappings({@Mapping(source = "name", target = "userName"),
            @Mapping(target = "address", ignore = true)
    })
    @Mapping(target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
    PersonDTO do2dto(PersonDO person);

    @Mappings({@Mapping(expression = "java(map.get(\"province\"))", target = "province"),
            @Mapping(expression = "java(map.get(\"city\"))", target = "city"),
            @Mapping(expression = "java(map.get(\"area\"))", target = "area"),
            @Mapping(expression = "java(map.get(\"street\"))", target = "street"),
            @Mapping(expression = "java(map.get(\"towns\"))", target = "towns"),
            @Mapping(expression = "java(map.get(\"community\"))", target = "community"),
            @Mapping(expression = "java(map.get(\"houseNums\"))", target = "houseNums")})
    HomeAddress map2HomeAddress(Map<String, String> map);


    @Mappings(@Mapping(source = "userName", target = "name"))
    @Mapping(target = "address", expression = "java(homeAddressToString(dto2do.getAddress()))")
    PersonDO dto2do(PersonDTO dto2do);

    default String homeAddressToString(HomeAddress address) {return JSON.toJSONString(address);
    }

}

应用注解 @Mapper 定义一个 Converter 接口,在其中定义一个 do2dto 办法,办法的入参类型是 PersonDO,出参类型是 PersonDTO,这个办法就用于将 PersonDO 转成 PersonDTO。
测试代码如下:

package com.spring.security.demo;

import com.spring.security.demo.Do.PersonDO;
import com.spring.security.demo.base.GenderEnum;
import com.spring.security.demo.dto.PersonDTO;
import com.spring.security.demo.entity.HomeAddress;
import com.spring.security.demo.service.PersonConverter;
import org.springframework.security.core.parameters.P;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author wulongbo
 * @Date 2021/1/6 16:48
 * @Version 1.0
 */
public class do2Dto {public static void main(String[] args) {Map<String, String> map = new HashMap<String, String>();
        map.put("province", "湖南");
        map.put("city", "郴州");
        map.put("area", "临武");
        map.put("street", "湖南");
        map.put("towns", "顺峰");
        map.put("community", "东云");
        map.put("houseNums", "1001");
        HomeAddress homeAddress = PersonConverter.INSTANCE.map2HomeAddress(map);
        System.out.println(homeAddress.toString());

        PersonDO personDO = new PersonDO();

        personDO.setName("wlb");

        personDO.setAge(26);

        personDO.setBirthday(new Date());

        personDO.setId(1);

        personDO.setGender(GenderEnum.MALE.name());

        PersonDTO personDTO = PersonConverter.INSTANCE.do2dto(personDO);

        personDTO.setAddress(homeAddress);
        System.out.println(personDTO);

        PersonDO personDO1 = PersonConverter.INSTANCE.dto2do(personDTO);
        System.out.println(personDO1);
    }
}

输入后果:

HomeAddress(province= 湖南, city= 郴州, area= 临武, street= 湖南, towns= 顺峰, community= 东云, houseNums=1001)
PersonDTO(userName=wlb, age=26, birthday=Thu Jan 07 11:33:26 CST 2021, gender=MALE, address=HomeAddress(province= 湖南, city= 郴州, area= 临武, street= 湖南, towns= 顺峰, community= 东云, houseNums=1001))
PersonDO(id=null, name=wlb, age=26, birthday=Thu Jan 07 11:33:26 CST 2021, gender=MALE, address={"area":"临武","city":"郴州","community":"东云","houseNums":"1001","province":"湖南","street":"湖南","towns":"顺峰"})

能够看到,咱们应用 MapStruct 完满的将 PersonDO 转成了 PersonDTO。
下面的代码能够看出,MapStruct 的用法比较简单,次要依赖 @Mapper 注解。

然而咱们晓得,大多数状况下,咱们须要相互转换的两个类之间的属性名称、类型等并不完全一致,还有些状况咱们并不想间接做映射,那么该如何解决呢?
其实 MapStruct 在这方面也是做的很好的。

MapStruct 解决字段映射

首先,能够明确的通知大家,如果要转换的两个类中源对象属性与指标对象属性的类型和名字统一的时候,会主动映射对应属性。

那么,如果遇到非凡状况如何解决呢?

名字不统一如何映射

如下面的例子中,在 PersonDO 中用 name 示意用户名称,而在 PersonDTO 中应用 userName 示意用户名,那么如何进行参数映射呢。
这时候就要应用 @Mapping 注解了,只须要在办法签名上,应用该注解,并指明须要转换的源对象的名字和指标对象的名字就能够了,如将 name 的值映射给 userName,能够应用如下形式:

@Mapping(source = "name", target = "userName")

能够主动映射的类型

除了名字不统一以外,还有一种非凡状况,那就是类型不统一,如下面的例子中,在 PersonDO 中用 String 类型示意用户性别,而在 PersonDTO 中应用一个 Genter 的枚举示意用户性别。

这时候类型不统一,就须要波及到相互转换的问题

其实,MapStruct 会对局部类型主动做映射,不须要咱们做额定配置,如例子中咱们将 String 类型主动转成了枚举类型。

个别状况下,对于以下状况能够做主动类型转换:

  • 根本类型及其他们对应的包装类型。
  • 根本类型的包装类型和 String 类型之间
  • String 类型和枚举类型之间

自定义常量

如果咱们在转换映射过程中,想要给一些属性定义一个固定的值,这个时候能够应用 constant

@Mapping(source = "name", constant = "wlb")

类型不统一的如何映射

还是下面的例子,如果咱们须要在 Person 这个对象中减少家庭住址这个属性,那么咱们个别在 PersonoDTO 中会独自定义一个 HomeAddress 类来示意家庭住址,而在 Person 类中,咱们个别应用 String 类型示意家庭住址。

这就须要在 HomeAddress 和 String 之间应用 JSON 进行互相转化,这种状况下,MapStruct 也是能够反对的。

咱们只须要在 PersonConverter 中在定义一个办法(因为 PersonConverter 是一个接口,所以在 JDK 1.8 当前的版本中能够定义一个 default 办法),这个办法的作用就是将 HomeAddress 转换成 String 类型。

default 办法:Java 8 引入的新的语言个性,用关键字 default 来标注,被 default 所标注的办法,须要提供实现,而子类能够抉择实现或者不实现该办法 

而后在 dto2do 办法上,通过以下注解形式即可实现类型的转换:

@Mapping(target = "address",expression = "java(homeAddressToString(dto2do.getAddress()))")

下面这种是自定义的类型转换,还有一些类型的转换是 MapStruct 自身就反对的,如 String 和 Date 之间的转换:

@Mapping(target = "birthday",dateFormat = "yyyy-MM-dd HH:mm:ss")

上,简略介绍了一些罕用的字段映射的办法,也是我本人在工作中常常遇到的几个场景,更多的状况大家能够查看官网的示例(https://github.com/mapstruct/…)。

MapStruct 的性能

后面说了这么多 MapStruct 的用法,能够看出 MapStruct 的应用还是比较简单的,并且字段映射下面的性能很弱小,那么他的性能到底怎么样呢?

参考《为什么阿里巴巴禁止应用 Apache Beanutils 进行属性的 copy?》中的示例,咱们对 MapStruct 进行性能测试。

别离执行 1000、10000、100000、1000000 次映射的耗时别离为:0ms、1ms、3ms、6ms。

能够看到,MapStruct 的耗时相比拟于其余几款工具来说是十分短的

那么,为什么 MapStruct 的性能能够这么好呢?

其实,MapStruct 和其余几类框架最大的区别就是: 与其余映射框架相比,MapStruct 在编译时生成 bean 映射,这确保了高性能,能够提前将问题反馈进去,也使得开发人员能够彻底的谬误查看。

在引入 MapStruct 的依赖的时候,特地在 maven-compiler-plugin 中减少了 mapstruct-processor 的反对吗?

并且咱们在代码中应用了很多 MapStruct 提供的注解,这使得在编译期,MapStruct 就能够间接生成 bean 映射的代码,相当于代替咱们写了很多 setter 和 getter。
如咱们在代码中定义了以下一个 Mapper:

package com.spring.security.demo.service;

import com.alibaba.fastjson.JSON;
import com.spring.security.demo.Do.PersonDO;
import com.spring.security.demo.dto.PersonDTO;
import com.spring.security.demo.entity.HomeAddress;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.Mappings;
import org.mapstruct.factory.Mappers;

import java.util.Map;

/**
 * @Author wulongbo
 * @Date 2021/1/6 16:38
 * @Version 1.0
 */

@Mapper
public interface PersonConverter {PersonConverter INSTANCE = Mappers.getMapper(PersonConverter.class);

    @Mappings({@Mapping(source = "name", target = "userName"),
            @Mapping(target = "address", ignore = true)
    })
    @Mapping(target = "birthday", dateFormat = "yyyy-MM-dd HH:mm:ss")
    PersonDTO do2dto(PersonDO person);

    @Mappings({@Mapping(expression = "java(map.get(\"province\"))", target = "province"),
            @Mapping(expression = "java(map.get(\"city\"))", target = "city"),
            @Mapping(expression = "java(map.get(\"area\"))", target = "area"),
            @Mapping(expression = "java(map.get(\"street\"))", target = "street"),
            @Mapping(expression = "java(map.get(\"towns\"))", target = "towns"),
            @Mapping(expression = "java(map.get(\"community\"))", target = "community"),
            @Mapping(expression = "java(map.get(\"houseNums\"))", target = "houseNums")})
    HomeAddress map2HomeAddress(Map<String, String> map);


    @Mappings(@Mapping(source = "userName", target = "name"))
    @Mapping(target = "address", expression = "java(homeAddressToString(dto2do.getAddress()))")
    PersonDO dto2do(PersonDTO dto2do);

    default String homeAddressToString(HomeAddress address) {return JSON.toJSONString(address);
    }

}

通过代码编译后,会主动生成一个 PersonConverterImpl:

package com.spring.security.demo.service;

import com.spring.security.demo.Do.PersonDO;
import com.spring.security.demo.base.GenderEnum;
import com.spring.security.demo.dto.PersonDTO;
import com.spring.security.demo.entity.HomeAddress;
import java.util.Map;
import javax.annotation.Generated;

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2021-01-07T11:33:24+0800",
    comments = "version: 1.0.0.Final, compiler: javac, environment: Java 1.8.0_251 (Oracle Corporation)"
)
public class PersonConverterImpl implements PersonConverter {

    @Override
    public PersonDTO do2dto(PersonDO person) {if ( person == null) {return null;}

        PersonDTO personDTO = new PersonDTO();

        personDTO.setUserName(person.getName() );
        personDTO.setAge(person.getAge() );
        personDTO.setBirthday(person.getBirthday() );
        if (person.getGender() != null ) {personDTO.setGender( Enum.valueOf( GenderEnum.class, person.getGender() ) );
        }

        return personDTO;
    }

    @Override
    public HomeAddress map2HomeAddress(Map<String, String> map) {if ( map == null) {return null;}

        HomeAddress homeAddress = new HomeAddress();

        homeAddress.setArea(map.get("area") );
        homeAddress.setProvince(map.get("province") );
        homeAddress.setCity(map.get("city") );
        homeAddress.setStreet(map.get("street") );
        homeAddress.setTowns(map.get("towns") );
        homeAddress.setHouseNums(map.get("houseNums") );
        homeAddress.setCommunity(map.get("community") );

        return homeAddress;
    }

    @Override
    public PersonDO dto2do(PersonDTO dto2do) {if ( dto2do == null) {return null;}

        PersonDO personDO = new PersonDO();

        personDO.setName(dto2do.getUserName() );
        if (dto2do.getAge() != null ) {personDO.setAge( dto2do.getAge() );
        }
        personDO.setBirthday(dto2do.getBirthday() );
        if (dto2do.getGender() != null ) {personDO.setGender( dto2do.getGender().name());
        }

        personDO.setAddress(homeAddressToString(dto2do.getAddress()) );

        return personDO;
    }
}

在运行期,对于 bean 进行映射的时候,就会间接调用 PersonConverterImpl 的 dto2do 办法,这样就没有什么非凡的事件要做了,只是在内存中进行 set 和 get 就能够了。
所以,因为在编译期做了很多事件,所以 MapStruct 在运行期的性能会很好,并且还有一个益处,那就是能够把问题的裸露提前到编译期。
使得如果代码中字段映射有问题,那么利用就会无奈编译,强制开发者要解决这个问题才行。

总结

本文介绍了一款 Java 中的字段映射工具类,MapStruct,他的用法比较简单,并且性能十分欠缺,能够应酬各种状况的字段映射。
并且因为他是编译期就会生成真正的映射代码,使得运行期的性能失去了大大的晋升。
强烈推荐,真的很香!!!

正文完
 0