MapStruct

mapstruct是我工作中经常使用的一个各种Object转换工具,本来一直是记在脑子里,但好记性不如烂笔头,所以记录在这里供日后查询,用getset 属性少一点的还可以介绍,多了实在是重复劳动了

MapStruct是和lombok一起基于jsr269的注解驱动器,自动生成对象转换的实现类,用于对象转换,比Spring自带的BeanUtils性能要高很多,毕竟发生在编译期,而不是像反射发生在运行期间,还有别的工具比如Dozer(停更),Orika(没用过,不好评价)

先创建一个Demo工程试试水,我个人更偏爱Gradle一些,构建速度快

plugins {
    id 'org.springframework.boot' version '2.7.1'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    implementation('org.mapstruct:mapstruct:1.4.2.Final')
     annotationProcessor "org.projectlombok:lombok-mapstruct-binding:0.2.0"
    annotationProcessor('org.mapstruct:mapstruct-processor:1.4.2.Final')
}

tasks.named('test') {
    useJUnitPlatform()
}

如果使用pom配置的话,参考官网

...
<properties>
    <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
</properties>
...
<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
</dependencies>
...
<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <source>1.8</source> <!-- depending on your project -->
                <target>1.8</target> <!-- depending on your project -->
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <!-- other annotation processors -->
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

如果需要加入Lombok依赖,则需要安装如下配置(lombok版本在1.8.16以上需要这样配置)

<properties>
    <org.mapstruct.version>1.5.2.Final</org.mapstruct.version>
    <org.projectlombok.version>1.18.16</org.projectlombok.version>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>

    <!-- lombok dependency should not end up on classpath -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${org.projectlombok.version}</version>
        <scope>provided</scope>
    </dependency>
</dependencies>

<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>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${org.projectlombok.version}</version>
                    </path>

                    <!-- additional annotation processor required as of Lombok 1.18.16 -->
                    <path>
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>0.2.0</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

我这里提前使用spring相关依赖,后面要用

小试牛刀

新建一个Pojo类

@Data
public class User {

    private Long id;
    private String username;
    private String password;
}

在建一个Vo类

@Data
public class UserVo {

    private Long id;
    private String username;
}

接着使用MapStruct来创建转换接口

@Mapper
public interface UserConverter {

    UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);

    UserVo convert(UserDto userDto);
}

在测试类测试转换效果

    @Test
    void testMapStruct() {
        var userDto = new UserDto();
        userDto.setId(2L);
        userDto.setUsername("Root");
        userDto.setPassword("123456");
        var userVo = UserConverter.INSTANCE.convert(userDto);
        assertThat(userVo.getId()).isEqualTo(userDto.getId());
        assertThat(userVo.getUsername()).isEqualTo(userDto.getUsername());
    }

测试结果
image
我们查看生成的实现类是怎样的

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2022-07-06T22:08:24+0800",
    comments = "version: 1.5.2.Final, compiler: IncrementalProcessingEnvironment from gradle-language-java-7.4.1.jar, environment: Java 17.0.3 (Oracle Corporation)"
)
public class UserConverterImpl implements UserConverter {

    @Override
    public UserVo convert(UserDto userDto) {
        if ( userDto == null ) {
            return null;
        }

        UserVo userVo = new UserVo();

        userVo.setId( userDto.getId() );
        userVo.setUsername( userDto.getUsername() );

        return userVo;
    }
}

最常使用

如果我们使用的Vo与Dto之间属性不一致时,那我们要怎么完成映射关系,
MapStruct为我们提供了这样的一个注解@Mapping
将上方UserVo中的id修改为userId
然后再使用转换接口

@Mapper
public interface UserConverter {

    UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);

    @Mapping(target = "userId", source = "id")
    UserVo convert(UserDto userDto);
}

这里我们使用目标属性为userId,源属性为id的转换进行映射
将测试类进行改写

    @Test
    void testMapStruct() {
        var userDto = new UserDto();
        userDto.setId(2L);
        userDto.setUsername("Root");
        userDto.setPassword("123456");
        var userVo = UserConverter.INSTANCE.convert(userDto);
//        assertThat(userVo.getId()).isEqualTo(userDto.getId());
        assertThat(userVo.getUserId()).isEqualTo(userDto.getId());
        assertThat(userVo.getUsername()).isEqualTo(userDto.getUsername());
    }

image-1657117353565

如果我们需要的属性,由两个类共同转换完成的,这时我们依旧可以使用@Mapping
这里我们在UserVo中添加一个typeName的属性,然后再添加一个新的类Type

@Data
public class TypeDto {

    private Long id;
    private String typeName;
}
@Data
public class UserVo {

    private Long userId;
    private String username;

    private String typeName;
}

将转换接口也进行修改

@Mapper
public interface UserConverter {

    UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);


    @Mapping(target = "typeName", source = "typeDto.typeName")
    @Mapping(target = "userId", source = "userDto.id")
    UserVo convert(UserDto userDto, TypeDto typeDto);
}

我们这时要在source这里加入对应形参的属性,来完成映射

    @Test
    void testMapStruct() {
        var userDto = new UserDto();
        userDto.setId(2L);
        userDto.setUsername("Root");
        userDto.setPassword("123456");
        var typeDto = new TypeDto();
        typeDto.setId(6L);
        typeDto.setTypeName("Admin");
        var userVo = UserConverter.INSTANCE.convert(userDto, typeDto);
//        assertThat(userVo.getId()).isEqualTo(userDto.getId());
        assertThat(userVo.getUserId()).isEqualTo(userDto.getId());
        assertThat(userVo.getUsername()).isEqualTo(userDto.getUsername());
        assertThat(userVo.getTypeName()).isEqualTo(typeDto.getTypeName());
    }

image-1657117790172

如果说我们要映射的属性没有对应的映射而要在后续业务中补充属性时,我们这时可以在 @Mapping(target = “photo”, ignore = true)
加入该条来在映射时忽略对应的属性
同时防止编译时警告

@Data
public class UserVo {

    private Long userId;
    private String username;

    private String typeName;
    private String photo;
}

@Mapper
public interface UserConverter {

    UserConverter INSTANCE = Mappers.getMapper(UserConverter.class);


    @Mapping(target = "photo", ignore = true)
    @Mapping(target = "typeName", source = "typeDto.typeName")
    @Mapping(target = "userId", source = "userDto.id")
    UserVo convert(UserDto userDto, TypeDto typeDto);
}

以上就是MapStruct的常用用法,下面介绍几种不那么常用的

自定义映射

由于目前Java大部分都是JDK8以上,接口已经支持了默认方法,所以我们如果要自定义映射的话我们可以借助默认方法来实现,比如使用Json实现数据的深拷贝
写个Demo

@Data
public class StudentDto {

    private Long id;
    private String name;
    private String className;
}

@Data
public class StudentVo {

    private Long id;
    private String name;
    private String className;
}

@Mapper
public interface StudentConverter {

    default StudentVo convert(StudentDto studentDto) throws JsonProcessingException {
        var objectMapper = new ObjectMapper();
        var jsonString = objectMapper.writeValueAsString(studentDto);
        return objectMapper.readValue(jsonString, new TypeReference<>() {
        });
    }

    StudentConverter INSTANCE = Mappers.getMapper(StudentConverter.class);
}

这里我使用jackson进行实体类件拷贝,这种拷贝是深拷贝
同时编写单元测试代码

   @Test
    void customMapStruct() {
        var studentDto = new StudentDto();
        studentDto.setId(10L);
        studentDto.setName("King");
        studentDto.setClassName("三年二班");
        try {
            var studentVo = StudentConverter.INSTANCE.convert(studentDto);
            assertThat(studentVo.getClassName()).isEqualTo(studentDto.getClassName());
            assertThat(studentVo.getId()).isEqualTo(studentDto.getId());
            assertThat(studentVo.getName()).isEqualTo(studentDto.getName());
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

image-1657121686453

多个映射一个实体类

但如果我们需要将多个实体类转换成一个实体类,这时MapStruct也是可以排上用场的
我们新建一个Address类,考虑到要频繁的书写测试用例,所以这里直接打算引入一个Faker框架用于自动填充数据

    implementation("net.datafaker:datafaker:1.4.0")
@Data
public class AddressDto {
   private int houseNo;
   private String city;
   private String state;
}

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

  @Mapping(target = "name", source = "studentDto.name")
  @Mapping(target = "houseNumber", source = "addressDto.houseNo")
  DeliveryAddressVo convert(StudentDto studentDto, AddressDto addressDto);
}

书写测试用例

  @Test
  void MultipleMapStruct() {
    var faker = new Faker(Locale.CHINA);
    var studentDto = new StudentDto();
    studentDto.setId(faker.number().randomNumber());
    studentDto.setName(faker.name().name());
    studentDto.setClassName(faker.name().name());
    var addressDto = new AddressDto();
    addressDto.setHouseNo(faker.number().positive());
    addressDto.setState(faker.address().state());
    addressDto.setCity(faker.address().cityName());
    var deliveryAddressVo = DeliveryAddressConverter.INSTANCE.convert(studentDto, addressDto);
    assertThat(deliveryAddressVo.getName()).isEqualTo(studentDto.getName()); 		assertThat(deliveryAddressVo.getHouseNumber()).isEqualTo(addressDto.getHouseNo());
  }

测试结果,
image-1657592880926
说明我们使用两个类转换成一个类是成功的

嵌套对象的映射

往往业务环境中我们需要将不同的类以组合 关联 聚合的关系进行组成,这时我们往往就需要多个类进行嵌套,这时我们使用MapStruct也可以实现对应的映射

我们仍然需要新建很多类

@Data
public class TechClass {
private String name;
}

@Data
public class StudentIncludeTechClassDto {
private Long id;
private String name;
private String className;

private TechClass techClass;
}

@Data
public class DeliveryAddressVo {

private String name;
private Integer houseNumber;
private String city;
private String state;
}

@Data
public class StudentIncludeTechClassVo {

private Long id;
private String name;
private String className;
private String techClassName;
}

书写映射接口

@Mapper
public interface StudentIncludeTechClassConverter {

  StudentIncludeTechClassConverter INSTANCE =
      Mappers.getMapper(StudentIncludeTechClassConverter.class);

  @Mapping(target = "techClassName", source = "techClass.name")
  StudentIncludeTechClassVo do2Vo(StudentIncludeTechClassDto dto);

  @Mapping(target = "techClass.name", source = "techClassName")
  StudentIncludeTechClassDto vo2Do(StudentIncludeTechClassVo vo);
}

编写测试用例

  @Test
  void nestedMpaStruct() {
    var faker = new Faker();
    var studentIncludeTechClassDto = new StudentIncludeTechClassDto();
    studentIncludeTechClassDto.setId(faker.number().randomNumber());
    studentIncludeTechClassDto.setName(faker.name().name());
    studentIncludeTechClassDto.setClassName(faker.name().name());
    var techClass = new TechClass();
    techClass.setName(faker.name().name());
    studentIncludeTechClassDto.setTechClass(techClass);
    var studentIncludeTechClassVo =
        StudentIncludeTechClassConverter.INSTANCE.do2Vo(studentIncludeTechClassDto);
    assertThat(studentIncludeTechClassVo.getTechClassName()).isEqualTo(techClass.getName());

    var studentIncludeTechClassVo1 = new StudentIncludeTechClassVo();
    studentIncludeTechClassVo1.setId(faker.number().randomNumber());
    studentIncludeTechClassVo1.setName(faker.name().name());
    studentIncludeTechClassVo1.setClassName(faker.name().name());
    var studentIncludeTechClassDto1 =
        StudentIncludeTechClassConverter.INSTANCE.vo2Do(studentIncludeTechClassVo1);
    assertThat(studentIncludeTechClassDto1.getTechClass().getName())
        .isEqualTo(studentIncludeTechClassVo1.getTechClassName());
  }

image-1657594513645
这里偷懒就使用了一个测试用例了

反向映射

一般双向映射时,条件都是相反的,这时我们可以使用@InheritInverseConfiguration 来复制反向映射配置

我们复用上述的实体类,对映射接口进行配置

@Mapper
public interface StudentIncludeTechClassConverter {

  StudentIncludeTechClassConverter INSTANCE =
      Mappers.getMapper(StudentIncludeTechClassConverter.class);

  @Mapping(target = "techClassName", source = "techClass.name")
  StudentIncludeTechClassVo do2Vo(StudentIncludeTechClassDto dto);

  @InheritInverseConfiguration
  StudentIncludeTechClassDto vo2Do(StudentIncludeTechClassVo vo);
}

测试用例继续使用上方的
image-1657595071593

MapStruct的隐式类型转换

往往我们在业务中,实体类中的类型偶尔是不同的,这使我们在手动书写这些转换时往往需要进行大量的类型转换工作,但有了MapStruct对于类型转换我们可以不用在手动书写

  • 原始类型与包装类
  • 原始类型与字符串
  • 枚举与字符串
  • BigInteger BigDecimal 与字符串
  • LocalDate LocalDateTime LocalTime与字符串(往往需要指定格式)

MapStruct 指定数字格式

当我们使用货币时,往往需求要指定好数字格式
比如:来源:350
需要显示为:350.00
这时我们也可以使用MapStruct的来进行配置
配置实体类

@Data
public class CarDto {
    private Long id;
    private Double price;
}

Q.E.D.