浅谈MapStruct应用

436 阅读7分钟

一、MapStruct基本概念

1、MapStruct是什么

MapStruct 是一个代码生成器,它和 Spring Boot、Maven 一样也是基于约定优于配置的理念,极大地简化了Java bean之间数据映射的实现。

2、MapStruct解决了什么问题

根据《阿里 Java 开发手册》分层领域模型规约,不能一个对象走天下,需要定义成POJO/DO/BO/DTO/VO/Query 等数据对象;

我们在开发中,还没出现MapStruct框架之前,对各个分层领域模型对象进行转换时,往往是采用以下几种方式进行相互转换:

1、利用手写对象字段属性的get 和 set 方法

2、利用手写对象的构造器

3、利用Builder模式,手动赋值

4、利用BeanUtils工具类

前三种方式,当对象字段属性特别多时,在书写代码时很容易出现经常丢参数,或者搞错参数值,在实际应用中,重复且乏味,最后一种方式虽然极大的解放了我们重复写代码操作,但是存在性能问题,因为BeanUtils本质是利用反射原理。

那有没有一种更优雅的方式来代替我们重复的领域模型对象相互转换的方案勒?

没错,就是MapStruct。

3、MapStruct原理

在编译期间生成了一个该接口的实现类,其实现类就是调用了该对象的 get/set 方法。

4、MapStruct地址

1、mapstruct.org/documentati…

2、github.com/mapstruct/m…

3、mapstruct.org/

二、应用

1、MapStruct和Lombok结合出现版本冲突

原因:

由于MapStruct和lombok都会在编译期生成代码,如果在还没生成get和set方法之前,先生成了MapStruct实现类,其实现类也就调用不了对象的get与set方法,故而不能完成赋值。

解决方式,在pom文件中添加以下插件:

<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.projectlombok</groupId>
                        <artifactId>lombok</artifactId>
                        <version>${lombok.version}</version>
                    </path>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                    <path>
                        <!-- 如果是0.1.0 有可能出现生成了maptruct的实现类,但该类只创建了对象,没有进行赋值 -->
                        <groupId>org.projectlombok</groupId>
                        <artifactId>lombok-mapstruct-binding</artifactId>
                        <version>0.2.0</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

2、maven依赖与数据准备

2.1 maven依赖

<properties>
    <maven.compiler.source>8</maven.compiler.source>
    <maven.compiler.target>8</maven.compiler.target>
    <java.version>1.8</java.version>
    <org.mapstruct.version>1.5.3.Final</org.mapstruct.version>
    <lombok.version>1.18.22</lombok.version>
</properties>
<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <version>${lombok.version}</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.mapstruct/mapstruct -->
    <dependency>
        <groupId>org.mapstruct</groupId>
        <artifactId>mapstruct</artifactId>
        <version>${org.mapstruct.version}</version>
    </dependency>
   
    <!-- https://mvnrepository.com/artifact/junit/junit -->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <scope>test</scope>
    </dependency>
    <!-- https://mvnrepository.com/artifact/cn.hutool/hutool-all -->
    <dependency>
        <groupId>cn.hutool</groupId>
        <artifactId>hutool-all</artifactId>
        <version>5.8.5</version>
    </dependency>
</dependencies>

2.2 数据准备

以编辑用户信息为例

2.2.1 DTO

@ToString
@Getter
@Setter
public class EditUserDTO implements Serializable {

    private static final long serialVersionUID = 1L;
    /**
     * 主键id
     */
    private Integer id;
    /**
     * 用户名称
     */
    private String userName;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别
     */
    private Integer sex;
    /**
     * 生日
     */
    private Date birthday;

    /**
     * 头像图片地址
     */
    private String headImageUrl;
    /**
     * 个人简介
     */
    private String introduction;
    /**
     * 个人爱好编号集合
     */
    private List<Integer> hobbyIds;
    /**
     * 毕业院校
     */
    private String graduateSchoolName;
}

2.2.2 BO

UserBO:

@ToString
@Getter
@Setter
public class UserBO {
    /**
     * 主键id
     */
    private Integer id;
    /**
     * 用户名称
     */
    private String userName;
    /**
     * 年龄
     */
    private Integer age;
    /**
     * 性别
     */
    private Integer sex;
    /**
     * 生日
     */
    private Date birthday;
}

UserExtBO:


@ToString
@Getter
@Setter
public class UserExtBO {
    /**
     * 用户id
     */
    private String userId;

    /**
     * 头像图片地址
     */
    private String headImageUrl;
    /**
     * 个人简介
     */
    private String introduction;
    /**
     * 个人爱好编号集合,以逗号拼接
     */
    private String hobbyIds;
    /**
     * 毕业院校
     */
    private String graduateSchoolName;
}

3、使用mapstruct赋值

3.1 基于Mappers#getMapper使用mapstruct

书写Mapper接口:

@Mapper
public interface UserConvert {

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

    /**
     * UserDTO -> UserBO
     *
     * @param dto EditUserDTO
     * @return 用户基础信息
     */
    @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
    UserBO dtoToBO(EditUserDTO dto);

    /**
     * EditUserDTO -> UserExtBO
     *
     * @param dto EditUserDTO
     * @return 用户拓展信息
     */
    @Mapping(source = "id", target = "userId")
    @Mapping(target = "hobbyIds", expression = "java(cn.hutool.core.collection.CollUtil.join(dto.getHobbyIds(),cn.hutool.core.text.StrPool.COMMA))")
    UserExtBO dtoToExtBO(EditUserDTO dto);
}

单元测试:

public class MapperTest {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Test
    public void mapperTest() {
        EditUserDTO dto = new EditUserDTO();
        dto.setId(1);
        dto.setBirthday(new Date());
        dto.setAge(18);
        dto.setGraduateSchoolName("Social University");
        dto.setIntroduction("From China");
        dto.setHobbyIds(ListUtil.toList(1, 2, 3, 4, 5, 6, 7, 8));
        dto.setHeadImageUrl("https://www.tsinghua.edu.cn/images/20210605.jpg");
        dto.setSex(0);
        dto.setUserName("Alice");
        UserBO userBO = UserConvert.INSTANCE.dtoToBO(dto);
        UserExtBO userExtBO = UserConvert.INSTANCE.dtoToExtBO(dto);
        logger.info("dto is {}", dto);
        logger.info("================================================");
        logger.info("userBO is {}", userBO);
        logger.info("================================================");
        logger.info("userExtBO is {}", userExtBO);
    }
}

输出信息:

[2023-08-04 23:09:59.609]  INFO  com.zhi.ma.mapper.MapperTest 32 - dto is EditUserDTO(id=1, userName=Alice, age=18, sex=0, birthday=Fri Aug 04 23:09:59 CST 2023, headImageUrl=https://www.tsinghua.edu.cn/images/20210605.jpg, introduction=From China, hobbyIds=[1, 2, 3, 4, 5, 6, 7, 8], graduateSchoolName=Social University)
[2023-08-04 23:09:59.617]  INFO  com.zhi.ma.mapper.MapperTest 33 - ================================================
[2023-08-04 23:09:59.617]  INFO  com.zhi.ma.mapper.MapperTest 34 - userBO is UserBO(id=1, userName=Alice, age=18, sex=0, birthday=Fri Aug 04 23:09:59 CST 2023)
[2023-08-04 23:09:59.617]  INFO  com.zhi.ma.mapper.MapperTest 35 - ================================================
[2023-08-04 23:09:59.618]  INFO  com.zhi.ma.mapper.MapperTest 36 - userExtBO is UserExtBO(userId=1, headImageUrl=https://www.tsinghua.edu.cn/images/20210605.jpg, introduction=From China, hobbyIds=1,2,3,4,5,6,7,8, graduateSchoolName=Social University)

3.2 基于spring使用mapstruct

参数 componentModel 默认值是 default,也就是手动创建实例,也可以通过 Spring 注入。

使用spring注入,只需要在@Mapper 注解加入了 componentModel = "spring" 值。

mapstruct接口类:

public abstract class ComponentModelConstant {

    public static final String SPRING = "spring";
}

@Mapper(componentModel = ComponentModelConstant.SPRING)
public interface SpringUserConvert {
    /**
     * UserDTO -> UserBO
     *
     * @param dto EditUserDTO
     * @return 用户基础信息
     */
    @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
    UserBO dtoToBO(EditUserDTO dto);

    /**
     * EditUserDTO -> UserExtBO
     *
     * @param dto EditUserDTO
     * @return 用户拓展信息
     */
    @Mapping(source = "id", target = "userId")
    @Mapping(target = "hobbyIds", expression = "java(cn.hutool.core.collection.CollUtil.join(dto.getHobbyIds(),cn.hutool.core.text.StrPool.COMMA))")
    UserExtBO dtoToExtBO(EditUserDTO dto);
}

单元测试类:

@RunWith(SpringRunner.class)
@SpringBootTest(classes = ElasticsearchApp.class)
public class SpringMapperTest {

    @Resource
    private SpringUserConvert springUserConvert;

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Test
    public void mapperTest() {
        EditUserDTO dto = new EditUserDTO();
        dto.setId(1);
        dto.setBirthday(new Date());
        dto.setAge(18);
        dto.setGraduateSchoolName("Social University");
        dto.setIntroduction("From China");
        dto.setHobbyIds(ListUtil.toList(1, 2, 3, 4, 5, 6, 7, 8));
        dto.setHeadImageUrl("https://www.tsinghua.edu.cn/images/20210605.jpg");
        dto.setSex(0);
        dto.setUserName("Alice");
        UserBO userBO = springUserConvert.dtoToBO(dto);
        UserExtBO userExtBO = springUserConvert.dtoToExtBO(dto);
        logger.info("SpringUserConvert dto is {}", dto);
        logger.info("================================================");
        logger.info("SpringUserConvert userBO is {}", userBO);
        logger.info("================================================");
        logger.info("SpringUserConvert userExtBO is {}", userExtBO);
    }
}

日志输出信息:

[2023-08-04 23:25:04.061]  INFO  com.zhi.ma.mapper.SpringMapperTest 42 - SpringUserConvert dto is EditUserDTO(id=1, userName=Alice, age=18, sex=0, birthday=Fri Aug 04 23:25:04 CST 2023, headImageUrl=https://www.tsinghua.edu.cn/images/20210605.jpg, introduction=From China, hobbyIds=[1, 2, 3, 4, 5, 6, 7, 8], graduateSchoolName=Social University)
[2023-08-04 23:25:04.061]  INFO  com.zhi.ma.mapper.SpringMapperTest 43 - ================================================
[2023-08-04 23:25:04.061]  INFO  com.zhi.ma.mapper.SpringMapperTest 44 - SpringUserConvert userBO is UserBO(id=1, userName=Alice, age=18, sex=0, birthday=Fri Aug 04 23:25:04 CST 2023)
[2023-08-04 23:25:04.061]  INFO  com.zhi.ma.mapper.SpringMapperTest 45 - ================================================
[2023-08-04 23:25:04.062]  INFO  com.zhi.ma.mapper.SpringMapperTest 46 - SpringUserConvert userExtBO is UserExtBO(userId=1, headImageUrl=https://www.tsinghua.edu.cn/images/20210605.jpg, introduction=From China, hobbyIds=1,2,3,4,5,6,7,8, graduateSchoolName=Social University)

4、应用说明

1 添加一个 interface 接口,使用MapStruct的@Mapper注解修饰,这里取名 XxxConvert,是为了不和MyBatis的Mapper混淆;

2 使用 Mappers 添加一个 INSTANCE 实例,也可以使用 Spring 注入;

3 添加两个映射方法,返回单个对象、对象列表;

4 使用 @Mappings + @Mapping 组合映射,如果两个字段名相同可以不用写,可以指定映射的日期格式、数字格式、表达式等,ignore 表示忽略该字段映射;

5 List 方法的映射会调用单个方法映射,不用单独映射,看其编译之后的源码就可得知

拓展点:[Java 8,Java 8+] 以上版本不需要 @Mappings 注解,直接使用 @Mapping 注解就行了.

想必各位看官老爷,已经大致了解了Mapstruct基本用法与原理啦,那么思考一个问题,Mapstruct相对于BeanUtils的优势在哪?

5 mapstruct玩法讲解

1 字段名不一致时,如何实现映射

@Mapping(source = "id", target = "userId")

2 自定义常量

@Mapping( target = "age", constant = "999")

注意:使用 constant,加上该属性,通过mapstruct编译实现类 后期赋值均不能覆盖常量值.

3 字段类型不一致映射

例如:上述例子中List -> String

@Mapping(target = "hobbyIds", expression = "java(cn.hutool.core.collection.CollUtil.join(dto.getHobbyIds(),cn.hutool.core.text.StrPool.COMMA))")

添加自己业务合适默认方法,如果不加,类型无法转换

方式一: 指定java表达式

方式二:添加转化类,不建议用此方式

@Mapper(componentModel = ComponentModelConstant.SPRING,uses = {HobbyConvert.class})
public interface SpringUserConvert {
    /**
     * UserDTO -> UserBO
     *
     * @param dto EditUserDTO
     * @return 用户基础信息
     */
    @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
    UserBO dtoToBO(EditUserDTO dto);

    /**
     * EditUserDTO -> UserExtBO
     *
     * @param dto EditUserDTO
     * @return 用户拓展信息
     */
    @Mapping(source = "id", target = "userId")
    UserExtBO dtoToExtBO(EditUserDTO dto);
}
@Component
public class HobbyConvert {

    public List<Integer> toList(String hobbyIds){
        if (StrUtil.isBlank(hobbyIds)){
            return new ArrayList<>();
        }
       return StrUtil.split(hobbyIds, StrPool.COMMA)
                .stream()
                .filter(NumberUtil::isInteger)
                .map(Integer::parseInt)
                .collect(Collectors.toList());
    }

    public String toStr(List<Integer> hobbyIds){
        return CollUtil.join(hobbyIds,StrPool.COMMA);
    }
}

4 字段设置默认值

当字段 没有主动赋值时,会自己设置默认值。但是当字段主动赋值时,主动值会覆盖默认值。

@Mapping( target = "userName", defaultValue = "Evan")

5 多转一

@Mapper(componentModel = ComponentModelConstant.SPRING,uses = {HobbyConvert.class})
public interface SpringUserConvert {
    /**
     * UserDTO -> UserBO
     *
     * @param dto EditUserDTO
     * @return 用户基础信息
     */
    @Mapping(source = "birthday", target = "birthday", dateFormat = "yyyy-MM-dd")
    UserBO dtoToBO(EditUserDTO dto);

    /**
     * EditUserDTO -> UserExtBO
     *
     * @param dto EditUserDTO
     * @return 用户拓展信息
     */
    @Mapping(source = "id", target = "userId")
    UserExtBO dtoToExtBO(EditUserDTO dto);

    /**
     * BO -> DTO
     *
     * @param userBO    UserBO
     * @param userExtBO UserExtBO
     * @return EditUserDTO
     */
    EditUserDTO boToDTO(UserBO userBO, UserExtBO userExtBO);
}

转换原则:

当多个对象中, 有其中一个为 null, 则会直接返回 null 如一对一转换一样, 属性通过名字来自动匹配。因此,名称和类型相同的不需要进行特殊处理 当多个原对象中,有相同名字的属性时,需要通过 @Mapping 注解来具体的指定, 以免出现歧义(不指 定会报错)。

6 更新 Bean 对象

有时候,我们不是想返回一个新的 Bean 对象,而是希望更新传入对象的一些属性。这个在实际的时 候也会经常使用到。

SpringUserConvert 新增

/**
 * 更新DTO
 * @param userBO UserBO
 * @param dto EditUserDTO
 */
void updateDTO(UserBO userBO, @MappingTarget EditUserDTO dto);

7 忽略对象中某个字段

@Mapping(ignore = true, target = "userId")

8 接口同时存在BO,DTO相互转换

@Getter
@Setter
@ToString
public class UserDTO implements Serializable {

    private static final long serialVersionUID = 1L;
    /**
     * 用户名称
     */
    private String name;
    /**
     * 年龄
     */
    private Integer ageNum;
    /**
     * 性别
     */
    private Integer sexFlag;
    /**
     * 生日
     */
    private Date birthday;
}

SpringUserConvert 新增


@Mappings({
        @Mapping(source = "userName", target = "name"),
        @Mapping(source = "age", target = "ageNum"),
        @Mapping(source = "sex", target = "sexFlag")
}
)
UserDTO boToUserDTO(UserBO userBO);

@InheritInverseConfiguration(name = "boToUserDTO")
UserBO dtoToUserBO(UserDTO dto);

@InheritInverseConfiguration解释:表示方法继承相应的反向方法的反向配置

三、引用与总结

引用文章地址:www.likecs.com/show-368361…

应用常见坑点:juejin.cn/post/720927…