跳至主要內容

Mapstruct神器

LeoSpringbootSpringboot约 2512 字大约 8 分钟

2024年了你还在用BeanUtil.copy,看看这款神器

1.前言

在项目中,如果我们要遵循分层领域模型规约: 话,肯定避免不了在DTO、VO、BO、AO、VO、Query等实体的转换,我们通常有几种做法:

  • 手动一个个字段的赋值
  • 通过反序列化的手段,必须先转成JSON字符串,再转回来
  • 使用Spring的BeanUtils,提供的克隆方法。

想必上面这几种方式大家都使用过。但是BeanUtils底层是通过反射实现的,复杂场景支持不足,控制 copy 粒度太粗,不易重构。

而前两种方式的话, 重复性工作多,手写代码容易遗漏掉有些字段。

今天我们来介绍关于MapStruct,MapStruct是一个代码生成器,它通过编译时自动生成映射代码,从而避免了手动编写重复且容易出错的映射逻辑。这种方式不仅提高了代码的效率和性能,而且保证了类型的安全性。

2.什么是DTO、DO、BO、AO、VO、Query

在此之前,我们先来认识认识什么是DTO、VO、BO、AO、VO、Query专业名词。

我们项目中会定义各种Object,如下图所示,我把它们分成了三大类

image-20240306200530976
image-20240306200530976

2.1 DTO

Data Transfer Object的简称: 数据传输对象,Service 或 Manager 向外传输的对象。

2.2 DO

Data Object的简称: 此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。

2.3 BO

Business Object的简称: 业务对象,由 Service 层输出的封装业务逻辑的对象。

2.4 AO

ApplicationObject的简称: 应用对象,在Web层与Service层之间抽象的复用对象模型, 极为贴近展示层,复用度不高。

2.5 VO

View Object的简称: 数据传输对象,Service向外传输的对象。

2.6 Query

数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止 使用 Map 类来传输。

最难理解的是BO,大致这么理解:

BO这个对象可以包括一个或多个其它的对象。

比如一个简历,有教育经历、工作经历、社会关系等等。

我们可以把教育经历对应一个PO,工作经历对应一个PO,社会关系对应一个PO。

建立一个对应简历的BO对象处理简历,每个BO包含这些PO。这样处理业务逻辑时,我们就可以针对BO去处理。

2.7 示例代码

Controllerpublic List getUsers(UserQuery userQuery);

此层常见的转换为:DTOVO

Service// 普通的service层接口

 List getUsers(UserQuery userQuery);

然后在Service内部使用UserBO封装中间所需的逻辑对象

// 来自前端的请求

 List getUsers(UserAO userAo);

此层常见的转换为:DOBOBODTO

DAOList getUsers(UserQuery userQuery);

3.mapstruct的作用

  1. 自动生成映射代码: 通过注解配置,MapStruct 可以在编译时自动生成符合映射规则的 JavaBean 映射代码,减少了手动编写代码的工作量。
  2. 类型转换支持: MapStruct 提供了丰富的类型转换支持,可以处理不同类型之间的映射转换,包括基本类型、集合类型等。
  3. 易于使用: MapStruct 的注解简单明了,易于理解和配置,同时提供了灵活的定制选项,适应各种复杂映射需求。
  4. 高性能: 生成的映射代码是高度优化的,性能较高,适用于对性能要求较高的应用场景。

4.mapstruct哪点比BeanUtils好?

  • 支持复杂属性赋值
  • 效率高,在编译时直接给你生成代码,相当与帮你手动去一个个赋值
  • 支持不同字段间的赋值,通过注解实现

假如没有使用 MapStruct 的话,当我们需要把 DO 对象转成一个 VO 对象时,我们需要这样做。

 public static UserInfoVO originalCopyItem(UserDTO userDTO){
     UserInfoVO userInfoVO = new UserInfoVO();
     userInfoVO.setUserName(userDTO.getName());
     userInfoVO.setAge(userDTO.getAge());
     userInfoVO.setBirthday(userDTO.getBirthday());
     userInfoVO.setIdCard(userDTO.getIdCard());
     userInfoVO.setGender(userDTO.getGender());
     userInfoVO.setIsMarried(userDTO.getIsMarried());
     userInfoVO.setPhoneNumber(userDTO.getPhoneNumber());
     userInfoVO.setAddress(userDTO.getAddress());
     return userInfoVO;
 }

传统的方法一般是采用硬编码,将每个对象的值都逐一设值。当然为了偷懒也会有采用一些BeanUtil简约代码的方式:

    public UserInfoVO utilCopyItem(UserDTO userDTO){
         UserInfoVO userInfoVO = new UserInfoVO();
         //采用反射、内省机制实现拷贝
         BeanUtils.copyProperties(userDTO, userInfoVO);
         return userInfoVO;
     }

但是,像BeanUtils这类通过反射、内省等实现的框架,在速度上会带来比较严重的影响。尤其是对于一些大字段、大对象而言,这个速度的缺陷就会越明显。针对速度这块我还专门进行了测试,对普通的setter方法、BeanUtils的拷贝以及本次需要介绍的mapperStruct进行了一次对比。得到的耗时结果如下所示:

运行次数setter方法耗时BeanUtils拷贝耗时MapperStruct拷贝耗时
12921528(1)3973292(1.36)2989942(1.023)
102362724(1)66402953(28.10)3348099(1.417)
1002500452(1)71741323(28.69)2120820(0.848)
10003187151(1)157925125(49.55)5456290(1.711)
100005722147(1)300814054(52.57)5229080(0.913)
10000019324227(1)244625923(12.65)12932441(0.669)

以上单位均为毫微秒。括号内的为当前组件同Setter比较的比值。可以看到BeanUtils的拷贝耗时基本为setter方法的十倍、二十倍以上。而MapperStruct方法拷贝的耗时,则与setter方法相近。由此可见,简单的BeanUtils确实会给服务的性能带来很大的压力。而MapperStruct拷贝则可以很好的解决这个问题。

5.SpringBoot整合mapstruct

5.1 引入pom依赖

<dependencies>
       <dependency>
           <groupId>org.mapstruct</groupId>
           <artifactId>mapstruct</artifactId>
       </dependency>
       <dependency>
           <groupId>org.mapstruct</groupId>
           <artifactId>mapstruct-processor</artifactId>
       </dependency>
       <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>
       </dependency>
    <!-- 不是必要的,哥们只是比较懒      -->
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
       </dependency>
   </dependencies>

它们的作用如下:

  1. org.mapstruct:mapstruct:
  • MapStruct 的核心库。它提供了 MapStruct 所需的主要注解和工具方法,例如 @Mapper, @Mapping 等注解以及 Mappers.getMapper() 方法。
  • 在运行时,这个库是必需的,生成的映射代码会依赖它。
  1. org.mapstruct:mapstruct-processor:
  • MapStruct 的注解处理器。它在编译时生成具体的映射实现代码。
  • compile 作用域,意味着它只在编译时被使用。
  • 当你编译一个使用了 MapStruct 注解的项目时,注解处理器会检测你的代码,然后为你的 @Mapper 注解的接口或抽象类生成实现。

5.2 创建 DO、VO

创建实体类Student跟StudentVo

@Data
@AllArgsConstructor
public class Student {

    private String name;

    private int age;
}
@Data
public class StudentVO {

    private String name;

    private Integer age;
}

5.3 创建转换器接口

定义了一个接口 StudentMapper,该接口的主要作用是将 Student对象转换为 StudentVO对象。

@Mapper
public interface StudentMapper {

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


    StudentVO tostudentVO(Student student);

    @Mappings({
            @Mapping(source = "age", target = "ageVo"),
            @Mapping(source = "name", target = "nameVo")
    })
}
  1. @Mapper

    这是 MapStruct 的核心注解之一。它标记了这个接口为一个映射器,并告诉 MapStruct 的注解处理器在编译时为此接口生成实现。

  2. INSTANCE常量

    Mappers.getMapper 是 MapStruct 提供的一个工具方法,用于在不使用 Spring 或其他依赖注入框架的情况下获取映射器的实例。

5.4 进行测试

@SpringBootTest(classes = {MapStructApplication.class})
public class StudentTest {


    /**
     *  用于测试:01
     */
    @Test
    public void test01() {
        Student student = new Student("张三", 18);
        StudentVO studentVO = StudentMapper.INSTANCE.tostudentVO(student);
        System.out.println("studentVO = " + studentVO);
    }

}

我们可以看到,已经成功将student实体类赋值给StudentVO。

image-20240306205022847
image-20240306205022847

6.mapstruct其他操作

6.1 不同字段映射

我们的实际开发中,原实体类跟我们需要转换的VO的字段名可能不太一样,那这个时候我们该怎么办,mpapstruct是否可以帮我们进行转换呢,我们接着往下聊。

我们创建第二个VO,名字叫做StudentVo2

@Data
public class StudentVO2 {

    private String nameVo;

    private Integer ageVo;
}

编写mapper转换器,使用@Mappings来映射多个字段不一致情况,如果只有一个字段,使用@mapping即可。

@Mappings({
            @Mapping(source = "age", target = "ageVo"),
            @Mapping(source = "name", target = "nameVo")
    })
    StudentVO2 tostudentVO2(Student student);

6.2 @Mappings注解

org.mapstruct 包下的 @Mappings 和 @DecoratedWith 注解是 MapStruct 中用于定义映射规则和装饰器的注解。
@Mappings 注解用于定义多个 @Mapping 注解,表示多个字段之间的映射关系。@Mapping 注解用于指定源对象和目标对象之间的字段映射关系,例如:

@Mappings({
            @Mapping(source = "age", target = "ageVo"),
            @Mapping(source = "name", target = "nameVo")
    })
    StudentVO2 tostudentVO2(Student student);

上述代码中,使用 @Mappings 注解定义了两个 @Mapping 注解,表示 age映射到 ageVo,name映射到 nameVo。

6.3 List转换

 List<StudentVO> studentListVo2Dto(List<StudentVO> vo);

6.4 Spring依赖注入

在前面的案例中,我们一直在使用 Mappers.getMapper 来获取映射器 INSTANCE

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

如果是在 Spring 环境下,还可以在 @Mapper 注解中添加 componentModel = "spring" 参数来告诉 MapStruct 在生成映射实现类的时候,提供 Spring 依赖注入。

这样我们在使用映射器的时候,可以直接通过 @Autowired 注解来注入 ColumnStructMapper 对象,然后就可以直接这样使用。

@Autowired
private StudentMapper studentMapper;

StudentVO tostudentVO(Student student);

这样我们以后就需要映射器接口中添加 INSTANCE 了。

7.MapStruct的IDEA插件

我们可以通过在Intellij IDEA 中安装MapStruct插件。

直接点击install即可。

image-20240306212122365
image-20240306212122365

安装完成后,可以直接在 @Mapper 接口和它的实现类之间快速导航。

image-20240306212712842
image-20240306212712842

点击之后,就可以到对应的实体类中了。

image-20240306213302904
image-20240306213302904

8.总结

以上便是本文的全部内容,本人才疏学浅,文章有什么错误的地方,欢迎大佬们批评指正!我是Leo,一个在互联网行业的小白,立志成为更好的自己。

如果你想了解更多关于Leo,可以关注公众号-程序员Leo,后面文章会首先同步至公众号。

公众号封面
公众号封面