JavaRush /Java 博客 /Random-ZH /什么是 Mapstruct 以及如何正确配置它以在 SpringBoot 应用程序中进行单元测试
Roman Beekeeper
第 35 级

什么是 Mapstruct 以及如何正确配置它以在 SpringBoot 应用程序中进行单元测试

已在 Random-ZH 群组中发布

背景

大家好,亲爱的朋友们和读者们!在我们写这篇文章之前,先介绍一下背景……我最近在使用Mapstruct库时遇到了一个问题,我在我的电报频道中对此进行了简要描述。帖子的问题在评论中得到了解决;我之前项目的同事帮助解决了这个问题。 什么是 Mapstruct 以及如何正确配置它以在 SpringBoot 应用程序中进行单元测试。 第 1 - 1 部分之后,我决定写一篇关于这个主题的文章,但我们当然不会采取狭隘的观点,而是首先尝试加快速度,理解 Mapstruct 是什么以及为什么需要它,并使用一个真实的例子,我们将分析一下之前出现的情况以及如何解决。因此,我强烈建议在阅读本文的同时进行所有计算,以便在实践中体验一切。 在我们开始之前,请订阅我的电报频道,我在那里收集我的活动,写下关于 Java 和 IT 开发的总体想法。 订阅了?伟大的!好吧,现在我们走吧!

地图结构,常见问题解答?

用于快速类型安全 bean 映射的代码生成器。 我们的首要任务是弄清楚 Mapstruct 是什么以及为什么我们需要它。一般来说,你可以在官方网站上了解它。该网站的主页上有三个问题的答案:这是什么?为了什么?如何?我们也会尝试这样做:

这是什么?

Mapstruct 是一个库,可帮助使用基于通过接口描述的配置生成的代码将某些实体的对象映射(映射,一般来说,这就是人们常说的:映射、映射等)到其他实体的对象。

为了什么?

在大多数情况下,我们开发多层应用程序(一层用于处理数据库,一层业务逻辑,一层用于应用程序与外界的交互),并且每一层都有自己的对象来存储和处理数据。并且这些数据需要通过从一个实体传输到另一个实体来从一层传输到另一层。对于那些没有使用过这种方法的人来说,这可能看起来有点复杂。例如,我们有一个学生数据库实体。当这个实体的数据进入业务逻辑(服务)层时,我们需要将数据从Student类传输到StudentModel类。接下来,在完成所有业务逻辑的操作之后,需要将数据发布到外部。为此,我们有 StudentDto 类。当然,我们需要将数据从StudentModel类传递到StudentDto。每次要转移的方法都要手工编写,这是一项劳动密集型工作。另外,这是代码库中需要维护的额外代码。你可能会犯错误。Mapstruct 在编译阶段生成此类方法并将它们存储在 generated-source 中。

如何?

使用注释。我们只需要创建一个具有主 Mapper 注释的注释,该注释告诉库该接口中的方法可用于从一个对象转换为另一个对象。正如我之前关于学生所说的,在我们的例子中,这将是 StudentMapper 接口,它将有多种将数据从一层传输到另一层的方法:
public class Student {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}

public class StudentDTO {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}

public class StudentModel {
   private Long id;
   private String firstName;
   private String lastName;
   private Integer age;
}
对于这些类,我们创建一个映射器(下文中这就是我们所说的接口,它描述了我们要传输的内容以及传输的位置):
@Mapper
public interface StudentMapper {
   StudentModel toModel(StudentDTO dto);
   Student toEntity(StudentModel model);
   StudentModel toModel(Student entity);
   StudentDTO toDto(StudentModel model);
}
这种方法的美妙之处在于,如果不同类中字段的名称和类型相同(如我们的例子),那么 Mapstruct 的设置足以在编译阶段基于 StudentMapper 接口生成必要的实现,这会翻译。那么事情已经变得很清楚了,对吧?让我们更进一步,用一个真实的例子来分析Spring Boot应用程序中的工作。

Spring Boot 和 Mapstruct 工作的示例

我们需要的第一件事是创建一个 Spring Boot 项目并向其中添加 Mapstruct。就此事而言,我在 GitHub 上有一个组织,其中包含存储库模板,Spring Boot 的启动就是其中之一。基于它,我们创建一个新项目: 什么是 Mapstruct 以及如何正确配置它以在 SpringBoot 应用程序中进行单元测试。 第 1 - 2 部分接下来,我们得到该项目是的,朋友们,如果您觉得该项目有用,请给它一颗星,这样我就知道我没有白做。在这个项目中,我们将揭示我在工作中收到并在我的 Telegram 频道上的帖子 中描述的情况。我将向那些不了解情况的人简要概述一下情况:当我们为映射器(即我们之前讨论的那些接口实现)编写测试时,我们希望测试尽快通过。使用映射器的最简单选项是在运行测试时使用 SpringBootTest 注释,它将获取 Spring Boot 应用程序的整个 ApplicationContext 并将测试所需的映射器注入到测试中。但这个选项非常耗费资源并且需要更多的时间,所以它不适合我们。我们需要编写一个单元测试,简单地创建所需的映射器并检查其方法是否完全按照我们的预期工作。为什么需要测试才能运行得更快?如果测试花费很长时间,就会减慢整个开发过程。在新代码的测试通过之前,该代码不能被认为是正确的,并且不会被用于测试,这意味着它不会被投入生产,这意味着开发人员还没有完成工作。看起来,为什么要为一个运行毫无疑问的库编写测试呢?然而我们需要编写一个测试,因为我们正在测试我们描述映射器的正确程度以及它是否符合我们的预期。首先,为了让我们的工作更轻松,让我们通过向 pom.xml 添加另一个依赖项来将 Lombok 添加到我们的项目中:
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok</artifactId>
   <scope>provided</scope>
</dependency>
在我们的项目中,我们需要从模型类(用于处理业务逻辑)转移到 DTO 类,用于与外界通信。在我们的简化版本中,我们假设字段不会改变,并且我们的映射器将很简单。但是,如果有愿望,可以写一篇更详细的文章,介绍如何使用 Mapstruct、如何配置它以及如何利用它的优势。但是,由于这篇文章会很长。假设我们有一个学生,有他参加的讲座和讲师的列表。让我们创建一个模型包。在此基础上,我们将创建一个简单的模型:
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

import java.util.List;

@Data
public class StudentDTO {

   private Long id;

   private String name;

   private List<LectureDTO> lectures;

   private List<LecturerDTO> lecturers;
}
他的讲座
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LectureDTO {

   private Long id;

   private String name;
}
和讲师
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LecturerDTO {

   private Long id;

   private String name;
}
并在模型包旁边 创建一个dto包:
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

import java.util.List;

@Data
public class StudentDTO {

   private Long id;

   private String name;

   private List<LectureDTO> lectures;

   private List<LecturerDTO> lecturers;
}
讲座
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LectureDTO {

   private Long id;

   private String name;
}
和讲师
package com.github.romankh3.templaterepository.springboot.dto;

import lombok.Data;

@Data
public class LecturerDTO {

   private Long id;

   private String name;
}
现在让我们创建一个映射器,将讲座模型集合转换为 DTO 讲座集合。首先要做的是将 Mapstruct 添加到项目中。为此,我们将使用他们的官方网站,那里有所有描述。也就是说,我们需要向内存中添加一个依赖项和一个插件(如果您对内存是什么有疑问,请看这里,Article1Article2):
<dependency>
   <groupId>org.mapstruct</groupId>
   <artifactId>mapstruct</artifactId>
   <version>1.4.2.Final</version>
</dependency>
并在内存中的 <build/> 块中。我们还没有:
<build>
   <plugins>
       <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <version>3.5.1</version>
           <configuration>
               <source>1.8</source>
               <target>1.8</target>
               <annotationProcessorPaths>
                   <path>
                       <groupId>org.mapstruct</groupId>
                       <artifactId>mapstruct-processor</artifactId>
                       <version>1.4.2.Final</version>
                   </path>
               </annotationProcessorPaths>
           </configuration>
       </plugin>
   </plugins>
</build>
接下来,我们在dtomodel旁边创建一个映射器包。根据我们之前展示的类,您将需要创建另外五个映射器:
  • Mapper LectureModel <-> LectureDTO
  • 映射器列表<LectureModel> <-> 列表<LectureDTO>
  • 映射器讲师模型 <-> 讲师DTO
  • 映射器列表<LecturerModel> <-> 列表<LecturerDTO>
  • 映射器 StudentModel <-> StudentDTO
去:

讲座映射器

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.dto.LecturerDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface LectureMapper {
   LectureDTO toDTO(LectureModel model);

   LectureModel toModel(LecturerDTO dto);
}

讲座列表映射器

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring", uses = LectureMapper.class)
public interface LectureListMapper {
   List<LectureModel> toModelList(List<LectureDTO> dtos);
   List<LectureDTO> toDTOList(List<LectureModel> models);
}

讲师制图师

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring")
public interface LectureMapper {
   LectureDTO toDTO(LectureModel model);

   LectureModel toModel(LectureDTO dto);
}

讲师列表映射器

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LecturerDTO;
import com.github.romankh3.templaterepository.springboot.model.LecturerModel;
import org.mapstruct.Mapper;

import java.util.List;

@Mapper(componentModel = "spring", uses = LecturerMapper.class)
public interface LecturerListMapper {
   List<LecturerModel> toModelList(List<LecturerDTO> dloList);
   List<LecturerDTO> toDTOList(List<LecturerModel> modelList);
}

学生绘图员

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.StudentDTO;
import com.github.romankh3.templaterepository.springboot.model.StudentModel;
import org.mapstruct.Mapper;

@Mapper(componentModel = "spring", uses = {LectureListMapper.class, LecturerListMapper.class})
public interface StudentMapper {
   StudentDTO toDTO(StudentModel model);
   StudentModel toModel(StudentDTO dto);
}
应该单独指出的是,在映射器中我们引用了其他映射器。这是通过Mapper 注释中的 use字段完成的,如 StudentMapper 中所做的那样:
@Mapper(componentModel = "spring", uses = {LectureListMapper.class, LecturerListMapper.class})
这里我们使用两个映射器来正确映射讲座列表和讲师列表。现在我们需要编译我们的代码并查看其中有什么以及如何进行。这可以使用mvn clean 编译命令来完成。但是,事实证明,在创建映射器的 Mapstruct 实现时,映射器实现并没有覆盖字段。为什么?事实证明,无法从 Lombok 获取数据注释。必须做点什么......因此,我们在文章中增加了一个新的部分。

连接 Lombok 和 Mapstruct

经过几分钟的搜索,发现我们需要通过某种方式连接Lombok和Mapstruct。Mapstruct 文档中有关于此的信息。检查了 Mapstruct 开发人员提出的示例后,让我们更新 pom.xml:让我们添加单独的版本:
​​<properties>
   <org.mapstruct.version>1.4.2.Final</org.mapstruct.version>
   <lombok-mapstruct-binding.version>0.2.0</lombok-mapstruct-binding.version>
</properties>
让我们添加缺少的依赖项:
<dependency>
   <groupId>org.projectlombok</groupId>
   <artifactId>lombok-mapstruct-binding</artifactId>
   <version>${lombok-mapstruct-binding.version}</version>
</dependency>
让我们更新我们的编译器插件,以便它可以连接 Lombok 和 Mapstruct:
<plugin>
   <groupId>org.apache.maven.plugins</groupId>
   <artifactId>maven-compiler-plugin</artifactId>
   <version>3.5.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>${lombok.version}</version>
           </path>
           <path>
               <groupId>org.projectlombok</groupId>
               <artifactId>lombok-mapstruct-binding</artifactId>
               <version>${lombok-mapstruct-binding.version}</version>
           </path>
       </annotationProcessorPaths>
   </configuration>
</plugin>
在此之后一切都会解决。让我们再次编译我们的项目。但是在哪里可以找到 Mapstruct 生成的类呢?它们位于生成源中: ${projectDir}/target/ generated-sources/annotations/ 什么是 Mapstruct 以及如何正确配置它以在 SpringBoot 应用程序中进行单元测试。 第 1 - 3 部分现在我们已经准备好认识到我对 Mapstruct 帖子的失望了,让我们尝试为映射器创建测试。

我们为我们的映射器编写测试

我将创建一个快速而简单的测试,在我们创建集成测试并且不用担心其完成时间的情况下测试其中一个映射器:

讲座映射器测试

package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class LectureMapperTest {

   @Autowired
   private LectureMapper mapperUnderTest;

   @Test
   void shouldProperlyMapModelToDto() {
       //given
       LectureModel model = new LectureModel();
       model.setId(11L);
       model.setName("lecture name");

       //when
       LectureDTO dto = mapperUnderTest.toDTO(model);

       //then
       Assertions.assertNotNull(dto);
       Assertions.assertEquals(model.getId(), dto.getId());
       Assertions.assertEquals(model.getName(), dto.getName());
   }

   @Test
   void shouldProperlyMapDtoToModel() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(11L);
       dto.setName("lecture name");

       //when
       LectureModel model = mapperUnderTest.toModel(dto);

       //then
       Assertions.assertNotNull(model);
       Assertions.assertEquals(dto.getId(), model.getId());
       Assertions.assertEquals(dto.getName(), model.getName());
   }
}
在这里,我们使用 SpringBootTest 注释启动整个 applicationContext,并使用 Autowired 注释从中提取测试所需的类。从编写测试的速度和易用性的角度来看,这非常好。测试顺利通过,一切正常。但我们会采取另一种方式,为映射器编写单元测试,例如 LectureListMapper...
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

class LectureListMapperTest {

   private final LectureListMapper lectureListMapper = new LectureListMapperImpl();

   @Test
   void shouldProperlyMapListDtosToListModels() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(12L);
       dto.setName("I'm BATMAN!");

       List<LectureDTO> dtos = Collections.singletonList(dto);

       //when
       List<LectureModel> models = lectureListMapper.toModelList(dtos);

       //then
       Assertions.assertNotNull(models);
       Assertions.assertEquals(1, models.size());
       Assertions.assertEquals(dto.getId(), models.get(0).getId());
       Assertions.assertEquals(dto.getName(), models.get(0).getName());
   }
}
由于 Mapstruct 生成的实现与我们的项目位于同一类中,因此我们可以轻松地在测试中使用它们。一切看起来都很棒 - 没有注释,我们以最简单的方式创建我们需要的类,仅此而已。但是当我们运行测试时,我们会发现它会崩溃,并且控制台中会出现 NullPointerException...这是因为 LectureListMapper 映射器的实现如下所示:
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2021-12-09T21:46:12+0300",
   comments = "version: 1.4.2.Final, compiler: javac, environment: Java 15.0.2 (N/A)"
)
@Component
public class LectureListMapperImpl implements LectureListMapper {

   @Autowired
   private LectureMapper lectureMapper;

   @Override
   public List<LectureModel> toModelList(List<LectureDTO> dtos) {
       if ( dtos == null ) {
           return null;
       }

       List<LectureModel> list = new ArrayList<LectureModel>( dtos.size() );
       for ( LectureDTO lectureDTO : dtos ) {
           list.add( lectureMapper.toModel( lectureDTO ) );
       }

       return list;
   }

   @Override
   public List<LectureDTO> toDTOList(List<LectureModel> models) {
       if ( models == null ) {
           return null;
       }

       List<LectureDTO> list = new ArrayList<LectureDTO>( models.size() );
       for ( LectureModel lectureModel : models ) {
           list.add( lectureMapper.toDTO( lectureModel ) );
       }

       return list;
   }
}
如果我们查看 NPE(NullPointerException 的缩写),我们会从LectureMapper变量中获取它,结果发现该变量尚未初始化。但在我们的实现中,我们没有可以用来初始化变量的构造函数。这正是 Mapstruct 以这种方式实现映射器的原因!在 Spring 中,您可以通过多种方式将 bean 添加到类中,您可以通过字段以及 Autowired 注释来注入它们,如上所述,也可以通过构造函数注入它们。当我需要优化测试执行时间时,我发现自己在工作中遇到了这样的问题。我认为对此无能为力,并在我的 Telegram 频道上倾诉了我的痛苦。然后他们在评论中帮我说可以定制注入策略。Mapper接口有一个injectionStrategy字段,它只接受InjectionStrategy名称,它有两个值:FIELDCONSTRUCTOR。现在,了解了这一点,让我们将此设置添加到我们的映射器中;我将使用LectureListMapper作为示例来展示它:
@Mapper(componentModel = "spring", uses = LectureMapper.class, injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface LectureListMapper {
   List<LectureModel> toModelList(List<LectureDTO> dtos);
   List<LectureDTO> toDTOList(List<LectureModel> models);
}
我用粗体突出显示了我添加的部分。让我们为所有其他选项添加此选项并重新编译项目,以便使用新行生成映射器。完成此操作后,让我们看看LectureListMapper的映射器实现如何更改(以粗体突出显示我们需要的部分):
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Generated(
   value = "org.mapstruct.ap.MappingProcessor",
   date = "2021-12-09T22:25:37+0300",
   comments = "version: 1.4.2.Final, compiler: javac, environment: Java 15.0.2 (N/A)"
)
@Component
public class LectureListMapperImpl implements LectureListMapper {

   private final LectureMapper lectureMapper;

   @Autowired
   public LectureListMapperImpl(LectureMapper lectureMapper) {

       this.lectureMapper = lectureMapper;
   }

   @Override
   public List<LectureModel> toModelList(List<LectureDTO> dtos) {
       if ( dtos == null ) {
           return null;
       }

       List<LectureModel> list = new ArrayList<LectureModel>( dtos.size() );
       for ( LectureDTO lectureDTO : dtos ) {
           list.add( lectureMapper.toModel( lectureDTO ) );
       }

       return list;
   }

   @Override
   public List<LectureDTO> toDTOList(List<LectureModel> models) {
       if ( models == null ) {
           return null;
       }

       List<LectureDTO> list = new ArrayList<LectureDTO>( models.size() );
       for ( LectureModel lectureModel : models ) {
           list.add( lectureMapper.toDTO( lectureModel ) );
       }

       return list;
   }
}
现在Mapstruct已经通过构造函数实现了mapper注入。这正是我们想要实现的目标。现在我们的测试将停止编译,让我们更新它并获取:
package com.github.romankh3.templaterepository.springboot.mapper;

import com.github.romankh3.templaterepository.springboot.dto.LectureDTO;
import com.github.romankh3.templaterepository.springboot.model.LectureModel;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Collections;
import java.util.List;

class LectureListMapperTest {

   private final LectureListMapper lectureListMapper = new LectureListMapperImpl(new LectureMapperImpl());

   @Test
   void shouldProperlyMapListDtosToListModels() {
       //given
       LectureDTO dto = new LectureDTO();
       dto.setId(12L);
       dto.setName("I'm BATMAN!");

       List<LectureDTO> dtos = Collections.singletonList(dto);

       //when
       List<LectureModel> models = lectureListMapper.toModelList(dtos);

       //then
       Assertions.assertNotNull(models);
       Assertions.assertEquals(1, models.size());
       Assertions.assertEquals(dto.getId(), models.get(0).getId());
       Assertions.assertEquals(dto.getName(), models.get(0).getName());
   }
}
现在,如果我们运行测试,一切都会按预期工作,因为在 LectureListMapperImpl 中我们传递了它需要的 LectureMapper...胜利!这对你来说并不困难,但我很高兴: 朋友们,一切如常,订阅我的GitHub 帐户,订阅我的Telegram 帐户。在那里我发布了我的活动结果,确实有用的东西)我特别邀请您加入telegram频道的讨论组。碰巧的是,如果有人有技术问题,他们可以在那里得到答案。这种格式对每个人来说都很有趣,您可以阅读谁知道什么并获得经验。

结论

通过这篇文章,我们认识了Mapstruct这样一个必要且经常使用的产品。我们弄清楚了它是什么、为什么以及如何。通过一个真实的例子,我们感受到了可以做什么以及如何改变它。我们还研究了如何通过构造函数设置 bean 注入,以便可以正确测试映射器。Mapstruct 的同事允许其产品的用户准确选择如何注入映射器,对此我们无疑要感谢他们。但是,尽管 Spring 建议通过构造函数注入 bean,但 Mapstruct 的人员默认通过字段设置注入。这是为什么?没有答案。我怀疑可能有我们不知道的原因,这就是他们这样做的原因。为了向他们了解情况,我在他们的官方产品存储库上 创建了一个 GitHub 问题。
评论
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION