Featured image of post Java工程师 MapStruct -> Pojo对象之间的转换工具

Java工程师 MapStruct -> Pojo对象之间的转换工具

🌏Java工程师 MapStruct对象映射框架 🎯 这系列文章用于记录 对MapStruct(Pojo对象之间的转换工具) 的学习和实践

🎄概述

随着业务开发变得复杂 Pojo类之间转化的繁琐操作占用了程序员大量的时间 而这些代码没有直接的业务价值 因此人们开发出很多用于Pojo类之间转换的框架用于简化开发 从而让程序员能够把更多的时间投入到实际业务的开发上面

MapStruct出生之前,人们一直用BeanUtils来简化类之间的转换

MapStruct的性能远远高于BeanUtils,下面是 https://juejin.cn/post/7140149801991012365 文章作者的测试结果 根据测试结果,可以看出随着属性个数的增加,BeanUtils的耗时也在增加,并且BeanUtils的耗时跟属性个数成正比,而MapStruct的耗时却一直是1秒,所以从对比数据可以看出MapStruct是非常优秀的,其性能远远超过BeanUtils 现在 程序员们首选MapStruct来简化类之间的转换操作

🎄 MapStruct 入门实操 + SpringBoot

🍭创建项目搭建环境

(1)创建项目

(2)引入需要的依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
		 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
		 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.bigbigmeng</groupId>
	<artifactId>mapstrcut-start-20231206</artifactId>
	<version>1.0-SNAPSHOT</version>

	<properties>
		<maven.compiler.source>8</maven.compiler.source>
		<maven.compiler.target>8</maven.compiler.target>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<mapstruct.version>1.3.1.Final</mapstruct.version>
	</properties>

	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-dependencies</artifactId>
				<version>2.2.2.RELEASE</version>
				<scope>import</scope>
				<type>pom</type>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<dependencies>
		<!--springboot工程-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<version>1.18.10</version>
		</dependency>
		
		<!--mapstruct依赖-->
		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct</artifactId>
			<version>${mapstruct.version}</version>
		</dependency>
		<dependency>
			<groupId>org.mapstruct</groupId>
			<artifactId>mapstruct-processor</artifactId>
			<version>${mapstruct.version}</version>
		</dependency>
	</dependencies>
</project>

🍭创建测试用的Pojo类

这里将使用Car小轿车、Driver司机、Part车上的零件 等类来演示MapStruct的强大功能

📑 PartDto类

/**
@author Liu Xianmeng
@createTime 2023/12/6 17:34
@instruction 汽车零件DTO类
*/

@SuppressWarnings({"all"})
@Data
public class PartDTO {
    /**
     * 汽车零件的id
     */
    private Long partDtoId;
    /**
     * 零件的名字 比如:多功能方向盘
     */
    private String partDtoName;
    /**
     * 零件的生产日期
     */
    private Date partDtoProductionDate;
}

📑 PartVo类

package com.bigbigmeng.dto;

/**
@author Liu Xianmeng
@createTime 2023/12/6 17:36
@instruction 汽车零件VO类
*/

@SuppressWarnings({"all"})
@Data
public class PartVo {
    /**
     * 汽车零件的id
     */
    private Long partVoId;
    /**
     * 零件的名字 比如:多功能方向盘
     */
    private String partVoName;
    /**
     * 零件的生产日期
     */
    private Date partVoProductionDate;
}

创建完两个类之后 项目文件的结构如下

🍭使用MapStruct创建转换工具类

接下来创建一个PartConvert类 用来转化不同的Part对象 比如上面的PartDTO和PartVO对象的互相转化

📑 PartConverter类

PartConverter类可以实现PartDTO和PartVO对象的互相转化 这里就是🎯实际运用MapStruct的地方了

【使用MapStruct的步骤如下】

​ (1) 引入依赖(之前的操作已经引入) ​ (2) 新建一个抽象类或者接口并标注 @Mapper ​ (3) 写一个转换方法 方法名规范命名以方便自己使用 ​ (4) 创建PartConverter对象命名为PART_CONVERTER(也可以使用其他命名)供其他(测试)类使用

package com.bigbigmeng.converter;

import com.bigbigmeng.dto.PartDTO;
import com.bigbigmeng.vo.PartVo;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.factory.Mappers;

/**
@author Liu Xianmeng
@createTime 2023/12/6 17:43
@instruction 汽车零件转化器类 -> 作为java对象转换的工具类进行使用

    【使用MapStruct的步骤如下】

        (1) 引入依赖(之前的操作已经引入)
        (2) 新建一个抽象类或者接口并标注 @Mapper
        (3) 写一个转换方法 方法名规范命名以方便自己使用
        (4) 创建PartConverter对象命名为PART_CONVERTER(也可以使用其他命名)供其他(测试)类使用

*/
@Mapper(componentModel = "spring")
@SuppressWarnings({"all"})
public abstract class PartConverter {

    // 创建一个PartConverter转换器对象 供其他类使用
    public static PartConverter PART_CONVERTER = Mappers.getMapper(PartConverter.class);

    /**
     * 用于将PartVO对象转换为PartDTO对象
     *
     * @param partVo
     * @return
     */
    @Mapping(source = "partVoId", target = "partDtoId")
    @Mapping(source = "partVoName", target = "partDtoName")
    public abstract PartDTO partVo2PartDto(PartVo partVo);
}

到这里 一个使用MapStruct相关注解写的java对象转换器类就写好了 后面可以直接使用 使用之前还需要创建一个主启动类 MapStructApplication

📑 MapStructApplication类

/**
@author Liu Xianmeng
@createTime 2023/12/6 18:05
@instruction
*/

@SuppressWarnings({"all"})
@SpringBootApplication
public class MapStructApplication {
    public static void main(String[] args) {
        SpringApplication.run(MapStructApplication.class);
    }
}

目前的项目结构如下

⚡测试使用PartConverter

接下来就来实际使用刚才所创建的PartConverter

生成测试类 PartConverterTest

package com.bigbigmeng.converter;

@SpringBootTest(classes = MapStructApplication.class)
class PartConverterTest {

    @Resource
    PartConverter partConverter;

    @Test
    void partVo2PartDto() {
        // 创建一个PartVo对象
        PartVo partVo = new PartVo();
        partVo.setPartVoId(100L);
        partVo.setPartVoName("小黄鸭");
        partVo.setPartVoProductionDate(new Date());
        // 获取转换结果
        PartDTO partDto = partConverter.partVo2PartDto(partVo);
        // 断言转换后的partDto对象的属性值是否符合预期
        Assertions.assertEquals(100L, partDto.getPartDtoId());
        Assertions.assertEquals("小黄鸭", partDto.getPartDtoName());
        // 打印转换后的结果
        System.out.println("partVo  = " + partVo);
        System.out.println("partDto = " + partDto);
    }
}

对partVo2PartDto方法进行测试 ->

🌱入门实操总结

再来回顾PartConverter类 -> 简单的配置就能实现java类之间的互相转换 ✅一次配置 永久使用 省去1了业务开发中大量无意义的java类的转换工作 关于MapStruct的其他用法和嵌套对象转换 后面紧接着进行实操

@Mapper(componentModel = "spring")
public abstract class PartConverter {

    // 创建一个PartConverter转换器对象 供其他类使用
    public static PartConverter PART_CONVERTER = Mappers.getMapper(PartConverter.class);

    /**
     * 用于将PartVO对象转换为PartDTO对象
     */
    @Mapping(source = "partVoId", target = "partDtoId")
    @Mapping(source = "partVoName", target = "partDtoName")
    public abstract PartDTO partVo2PartDto(PartVo partVo);
}

🎄 MapStruct映射规则

🍭同类型且同名的属性自动映射

如果PartDTO和PartVO都有一个类型为long的testField属性,即使不使用@Mapping指定PartDTO的testField属性也能映射到PartVO的testField属性 接下来进行验证

首先 给两个类🎯添加testField属性

public class PartDTO {
    /**
     * 添加测试属性 testField
     */
    private Long 🎯testField;
    ...
}
public class PartVO {
    /**
     * 添加测试属性 testField
     */
    private Long 🎯testField;
    ...
}

然后在测试方法中给partVo对象的testField属性赋值

@Test
void partVo2PartDto() {
    // 创建一个PartVo对象
    PartVo partVo = new PartVo();
    ...
    partVo.setTestField(123L);
    // 获取转换结果
    PartDTO partDto = partConverter.partVo2PartDto(partVo);
    // 断言转换后的partDto对象的属性值是否符合预期
    ...
}

测试执行

🍭MapStruct会自动进行类型转换

(1)MapStruct支持8种基本类型和他们对应的包装类型之间的自动转换

(2)MapStruct支持8种基本类型(包括他们的包装类型)和String类型之间的自动转换

(3)MapStruct支持日期类型和String之间的自动转换

🍭@Mappings和@Mapping

@Mappings

@Mappings相当于可以把多个@Mapping进行包含 效果是一样的

@Mappings(
    value = {
        @Mapping(source = "partVoId", target = "partDtoId"),
        @Mapping(source = "partVoName", target = "partDtoName"),
        @Mapping(source = "partVoProductionDate", target = "partDtoProductionDate"),
        @Mapping(source = "length", target = "length", numberFormat = "#.00")
    }
)

🍭数字格式化:numberFormat = “#.00”

给pojo类添加length属性

public class PartDTO {
    /**
     * 零件的长度 Double类型属性 验证数字格式化 
     */
    private double length;
    ...
}
public class PartVO {
    /**
     * 零件的长度 Double类型属性 验证数字格式化
     */
    private String length;
    ...
}

给映射工具类的添加映射方法🎯partDto2PartVo

@Mapper(componentModel = "spring")
@SuppressWarnings({"all"})
public abstract class PartConverter {
    @Mapping(source = "length",target = "length",numberFormat = "#.00")
    public abstract PartVo 🎯partDto2PartVo(PartDTO partDTO);
}

添加测试方法

@Test
void partDto2PartVo() {
    PartVo rst = PartConverter.PART_CONVERTER.partDto2PartVo(getAPartDto());
    System.out.println(rst);
}

PartDTO getAPartDto() {
    PartDTO partDTO = new PartDTO();
    partDTO.setLength(8888.88888);
    return partDTO;
}

执行测试

查看生成的源码的处理

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-12-08T10:31:48+0800",
    comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_341 (Oracle Corporation)"
)
@Component
public class PartConverterImpl extends PartConverter {
    ...
    @Override
    public PartVo partDto2PartVo(PartDTO partDTO) {
        if ( partDTO == null ) {
            return null;
        }

        PartVo partVo = new PartVo();
        /**
         * 🎯对dto传入的double进行格式化处理 处理后赋值给vo对象
         *
         * 🎯🎯🎯这里一定要❗❗❗注意 dto的length属性是double类型 而vo的length属性是String类型
         */
        partVo.setLength( new DecimalFormat( "#.00" ).format( partDTO.getLength() ) );
        partVo.setTestField( partDTO.getTestField() );

        return partVo;
    }
}

🍭source和target多余的属性对方没有是不会报错的

🎄 MapStruct嵌套转换

🍭概述和准备

当转换的对象为一个复合对象的时候(一个java对象包含另一个java对象作为属性) 只要做了响应的配置 MapStruct就会自动进行嵌套转换 例如下面的例子:一个汽车对象包含1个车座对象 创建CarDTO类和CarVO

/**
@author Liu Xianmeng
@createTime 2023/12/8 13:33
@instruction 小轿车DTO
*/

@SuppressWarnings({"all"})
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CarDTO {
    // 车座
    private PartDTO carPartDtoSeat;
    // 车的品牌
    private String carDtoBrand;
}

/**
@author Liu Xianmeng
@createTime 2023/12/8 13:37
@instruction 汽车VO
*/

@SuppressWarnings({"all"})
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class CarVO {
    // 车座
    private PartDTO carPartVoSeat;
    // 车的品牌
    private String carVoBrand;
}

然后创建新的转换类CarConverter 并写一个新的转换方法carDto2carVo、做mapping注解配置

/**
@author Liu Xianmeng
@createTime 2023/12/8 13:48
@instruction
*/

@SuppressWarnings({"all"})
@Mapper(componentModel = "spring")
public abstract class CarConverter {

    // 创建一个CarConverter转换器对象 供其他类使用
    public static CarConverter CAR_CONVERTER = Mappers.getMapper(CarConverter.class);

    /**
     * carDto -> carVo
     */
    @Mapping(source = "carDtoBrand", target = "carVoBrand")
    @Mapping(source = "carPartDtoSeat", target = "carPartVoSeat")
    public abstract CarVO carDto2carVo(CarDTO carDto);
}

在测试之前 还需要对🎯partDto2PartVo方法做完整的配置

/**
@author Liu Xianmeng
@createTime 2023/12/6 17:43
@instruction 汽车零件转化器类 -> 作为java对象转换的工具类进行使用
*/
@Mapper(componentModel = "spring")
@SuppressWarnings({"all"})
public abstract class PartConverter {
    ...
    /**
     * PartDTO -> PartVO
     */
    @Mapping(source = "partDtoId", target = "partVoId")
    @Mapping(source = "partDtoName", target = "partVoName")
    @Mapping(source = "length",target = "length",numberFormat = "#.00")
    🎯public abstract PartVO partDto2PartVo(PartDTO partDTO);
}

⚡执行测试

编写测试类

package com.bigbigmeng.converter;
...

@SpringBootTest(classes = MapStructApplication.class)
class CarConverterTest {

    @Resource
    CarConverter carConverter;

    @Test
    void carDto2carVo() {
        CarDTO carDto = new CarDTO();
        carDto.setCarDtoBrand("兰博基尼");
        carDto.setCarPartDtoSeat(getACarSeatPartDto());
        CarVO carVO = carConverter.carDto2carVo(carDto);
        System.out.println(carVO);
    }

    /**
     * 获取一个车座对象
     * @return
     */
    PartDTO getACarSeatPartDto() {
        PartDTO partDTO = new PartDTO();
        partDTO.setLength(8888.88888);
        partDTO.setPartDtoId(123L);
        partDTO.setPartDtoName("车座");
        return partDTO;
    }
}

执行测试方法

🎄 @AfterMapping && @MappingTarget

🍭概述和准备

有时候DTO和VO之间的的属性没有直接的映射关系 例如下面 给CarVO添加一个hasSeatPart属性 为布尔类型 表示这辆车有没有车座

/**
@author Liu Xianmeng
@createTime 2023/12/8 13:37
@instruction 汽车VO
*/
public class CarVO {
    // 车座
    private PartDTO carPartVoSeat;
    // 车的品牌
    private String carVoBrand;
    // 车是否有车座
    private boolean hasSeatPart;
}

这个时候 如果我们想要根据DTO中是否存在车座组件来映射CarVO中的hasSeatPart属性 就需要完成一些自定义属性的转换 @AfterMapping && @MappingTarget 可以帮我们做到

/**
@author Liu Xianmeng
@createTime 2023/12/8 13:48
@instruction
*/

@SuppressWarnings({"all"})
@Mapper(componentModel = "spring")
public abstract class CarConverter {
    ...
    /**
     * 表示让mapstruct在调用完自动转换方法之后 再紧接着调用本方法 完成一些自定义属性的转换
     * 例如: 根据DTO中是否存在车座组件来映射CarVO中的hasSeatPart属性
     *
     * @MappingTarget -> 表示传来的catVo对象是已经赋过值的
     * @param carDto
     * @param carVo
     */
    @AfterMapping
    public void carDto2carVoAfter(CarDTO carDto, @MappingTarget CarVO carVo) {
        // 获取dto中的车座组件属性
        PartDTO carPartDtoSeat = carDto.getCarPartDtoSeat();
        // 如果车座组件不为空 则将carVo的hasSeatPart属性赋值为true
        if(carPartDtoSeat != null && !carPartDtoSeat.getPartDtoName().equals("")) {
            carVo.setHasSeatPart(true);
        } else {
            carVo.setHasSeatPart(false);
        }
    }
}

⚡执行测试

🎄 MapStruct批量装换

🍭概述和准备

对🎯批量pojo对象进行转换

/**
@author Liu Xianmeng
@createTime 2023/12/6 17:43
@instruction 汽车零件转化器类 -> 作为java对象转换的工具类进行使用
*/
@Mapper(componentModel = "spring")
@SuppressWarnings({"all"})
public abstract class PartConverter {
	...
	/**
     * 批量转换
     */
    public abstract List<PartVO> 🎯partDtos2partVos(List<PartDTO> partDTOS);
}

⚡执行测试

编写测试方法

@SpringBootTest(classes = MapStructApplication.class)
class PartConverterTest {
    @Resource
    PartConverter partConverter;
    
    @Test
    void partDtos2partVos() {
        // 获取1个车座对象 和1个车门对象
        List<PartDTO> partDtosList = new ArrayList<>();
        PartDTO partDTO_001 = getACarSeatPartDto();
        partDTO_001.setPartDtoName("车座001");
        partDtosList.add(partDTO_001);
        PartDTO partDTO_002 = getACarDoorPartDto();
        partDTO_002.setPartDtoName("车门002");
        partDtosList.add(partDTO_002);

        // 将两个对象转换为PartVO对象
        List<PartVO> partVOS = partConverter.partDtos2partVos(partDtosList);
        System.out.println(partVOS);
    }
}

执行测试

🎄 @BeanMapping注解

🍭概述和准备

@BeanMapping注解可以指定不执行默认的映射规则 如:@BeanMapping(ignoreByDefault = true) 指定之后 只有被@Mapping注解配置的属性会被映射 其他属性均为空 下面举例 :修改partDto2PartVo方法,加上@BeanMapping(ignoreByDefault = true)修饰 并且只指定车组件的名字属性的映射

/**
 * PartDTO -> PartVO
 */
@BeanMapping(ignoreByDefault = true) // 取消默认的映射
@Mapping(source = "partDtoName", target = "partVoName") // 只指定车组件的名字属性的映射
public abstract PartVO partDto2PartVo(PartDTO partDTO);

运行刚才的批量转换partDtos2partVos方法 查看运行结果

Licensed under CC BY-NC-SA 4.0
最后更新于 2023年12月8日