当前位置:Java -> 使用MapStruct生成对象映射

使用MapStruct生成对象映射

你是否需要编写大量映射代码来在不同对象模型之间进行映射?MapStruct通过生成映射代码简化了这个任务。在这篇博客中,您将了解MapStruct的一些基本特性。祝您阅读愉快!

介绍

在多层应用程序中,人们经常需要编写样板代码以将不同的对象模型进行映射。这可能是一项繁琐且易出错的任务。 MapStruct 通过为您生成映射代码来简化此任务。它在编译时生成代码,并旨在生成的代码看起来就像是您自己编写的一样。

本博客将仅对MapStruct如何帮助您进行基本概述,但这将足以让您对它能解决哪些问题有良好的印象。

如果您使用IntelliJ作为IDE,还可以安装 MapStruct支持插件,它将帮助您使用MapStruct。

本博客中使用的资源可在 GitHub 上找到。

先决条件

本博客的先决条件包括:

  • 基本的Java知识,本博客使用Java 21
  • 基本的Spring Boot知识

基本应用程序

本博客中使用的应用程序是一个基本的Spring Boot项目。通过一个Rest API,一个客户可以被创建和检索。为了使API规范和源代码保持一致,您将使用 openapi-generator-maven-plugin。首先,您编写 OpenAPI规范,然后插件将根据规范为您生成源代码。OpenAPI规范由两个端点组成,一个用于创建客户(POST),另一个用于检索客户(GET)。客户包括其名称和一些地址数据。

Customer:
  type: object
  properties:
    firstName:
      type: string
      description: First name of the customer
      minLength: 1
      maxLength: 20
    lastName:
      type: string
      description: Last name of the customer
      minLength: 1
      maxLength: 20
    street:
      type: string
      description: Street of the customer
      minLength: 1
      maxLength: 20
    number:
      type: string
      description: House number of the customer
      minLength: 1
      maxLength: 5
    postalCode:
      type: string
      description: Postal code of the customer
      minLength: 1
      maxLength: 5
    city:
      type: string
      description: City of the customer
      minLength: 1
      maxLength: 20


CustomerController 实现了生成的Controller接口。OpenAPI Maven插件使用自己的模型。为了将数据传输到 CustomerService,创建了DTOs。这些是Java记录。 CustomerDto 如下:

public record CustomerDto(Long id, String firstName, String lastName, AddressDto address) {
}


AddressDto 如下:

public record AddressDto(String street, String houseNumber, String zipcode, String city) {
}


领域本身在服务中使用,是一个基本的Java POJO。 Customer 领域如下:

public class Customer {
    private Long customerId;
    private String firstName;
    private String lastName;
 
    private Address address;
 
    // Getters and setters left out for brevity
}


Address 领域如下:

public class Address {
 
    private String street;
    private int houseNumber;
    private String zipcode;
    private String city;
 
    // Getters and setters left out for brevity
}


为了将所有内容连接在一起,您需要为以下内容编写映射器代码:

  • 在API模型和DTO之间进行映射
  • 在DTO和领域之间进行映射

DTO和领域之间的映射

添加依赖

要使用MapStruct,只需要添加MapStruct Maven依赖项,并向Maven编译器插件添加一些配置即可。

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>${org.mapstruct.version}</version>
</dependency>
...
<build>
    <plugins>
        ...
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <version>3.8.1</version>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>org.mapstruct</groupId>
                        <artifactId>mapstruct-processor</artifactId>
                        <version>${org.mapstruct.version}</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
        ...
    </plugins>
</build>


创建映射器

CustomerDtoAddressDtoCustomerAddress 领域之间并没有很大的区别。

  • CustomerDto 有一个 id,而 Customer 有一个 customerId
  • AddressDto 有一个字符串类型的 houseNumber,而 Address 有一个整数类型的 houseNumber

为了使用MapStruct创建这个映射器,您创建一个名为 CustomerMapper 的接口,用 @Mapper 进行注解,并指定组件模型为值 spring。这样做将确保所生成的映射器是一个单例范围的Spring bean,可以通过 @Autowired 获取。

由于两个模型非常相似,MapStruct将能够自动生成大部分代码。因为客户ID在两个模型中有不同的名称,您需要帮助MapStruct一下。使用 @Mapping 注释,您指定源和目标映射。对于类型转换,您无需执行任何操作,MapStruct可以根据 隐式类型转换 解决这个问题。

相应的映射器代码如下:

@Mapper(componentModel = "spring")
public interface CustomerMapper {
 
    @Mapping(source = "customerId", target = "id")
    CustomerDto transformToCustomerDto(Customer customer);
 
    @Mapping(source = "id", target = "customerId")
    Customer transformToCustomer(CustomerDto customerDto);
 
}


生成代码:

$ mvn clean compile


target/generated-sources/annotations 目录中,您可以找到生成的 CustomerMapperImpl 类。

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2024-04-21T13:38:51+0200",
    comments = "version: 1.5.5.Final, compiler: javac, environment: Java 21 (Eclipse Adoptium)"
)
@Component
public class CustomerMapperImpl implements CustomerMapper {
 
    @Override
    public CustomerDto transformToCustomerDto(Customer customer) {
        if ( customer == null ) {
            return null;
        }
 
        Long id = null;
        String firstName = null;
        String lastName = null;
        AddressDto address = null;
 
        id = customer.getCustomerId();
        firstName = customer.getFirstName();
        lastName = customer.getLastName();
        address = addressToAddressDto( customer.getAddress() );
 
        CustomerDto customerDto = new CustomerDto( id, firstName, lastName, address );
 
        return customerDto;
    }
 
    @Override
    public Customer transformToCustomer(CustomerDto customerDto) {
        if ( customerDto == null ) {
            return null;
        }
 
        Customer customer = new Customer();
 
        customer.setCustomerId( customerDto.id() );
        customer.setFirstName( customerDto.firstName() );
        customer.setLastName( customerDto.lastName() );
        customer.setAddress( addressDtoToAddress( customerDto.address() ) );
 
        return customer;
    }
 
    protected AddressDto addressToAddressDto(Address address) {
        if ( address == null ) {
            return null;
        }
 
        String street = null;
        String houseNumber = null;
        String zipcode = null;
        String city = null;
 
        street = address.getStreet();
        houseNumber = String.valueOf( address.getHouseNumber() );
        zipcode = address.getZipcode();
        city = address.getCity();
 
        AddressDto addressDto = new AddressDto( street, houseNumber, zipcode, city );
 
        return addressDto;
    }
 
    protected Address addressDtoToAddress(AddressDto addressDto) {
        if ( addressDto == null ) {
            return null;
        }
 
        Address address = new Address();
 
        address.setStreet( addressDto.street() );
        if ( addressDto.houseNumber() != null ) {
            address.setHouseNumber( Integer.parseInt( addressDto.houseNumber() ) );
        }
        address.setZipcode( addressDto.zipcode() );
        address.setCity( addressDto.city() );
 
        return address;
    }
}


如您所见,代码非常易读,考虑了 CustomerAddress 的映射。

创建服务

服务将创建一个名为Customer的领域对象,以CustomerDto作为输入。 customerMapper被注入到服务中,并用于在这两个模型之间进行转换。反过来,当检索到一个客户时,映射器将领域对象Customer转换为CustomerDto。在服务中,客户被持久化在一个基本的列表中,以保持事情简单。

@Service
public class CustomerService {
 
    private final CustomerMapper customerMapper;
 
    private final HashMap<Long, Customer> customers = new HashMap<>();
    private Long index = 0L;
 
    CustomerService(CustomerMapper customerMapper) {
        this.customerMapper = customerMapper;
    }
 
    public CustomerDto createCustomer(CustomerDto customerDto) {
        Customer customer = customerMapper.transformToCustomer(customerDto);
        customer.setCustomerId(index);
        customers.put(index, customer);
        index++;
        return customerMapper.transformToCustomerDto(customer);
    }
 
    public CustomerDto getCustomer(Long customerId) {
        if (customers.containsKey(customerId)) {
            return customerMapper.transformToCustomerDto(customers.get(customerId));
        } else {
            return null;
        }
    }
}


测试映射器

可以通过使用生成的CustomerMapperImpl类轻松测试映射器,并验证映射是否成功执行。

class CustomerMapperTest {
 
    @Test
    void givenCustomer_whenMaps_thenCustomerDto() {
        CustomerMapperImpl customerMapper = new CustomerMapperImpl();
        Customer customer = new Customer();
        customer.setCustomerId(2L);
        customer.setFirstName("John");
        customer.setLastName("Doe");
        Address address = new Address();
        address.setStreet("street");
        address.setHouseNumber(42);
        address.setZipcode("zipcode");
        address.setCity("city");
        customer.setAddress(address);
 
        CustomerDto customerDto = customerMapper.transformToCustomerDto(customer);
        assertThat( customerDto ).isNotNull();
        assertThat(customerDto.id()).isEqualTo(customer.getCustomerId());
        assertThat(customerDto.firstName()).isEqualTo(customer.getFirstName());
        assertThat(customerDto.lastName()).isEqualTo(customer.getLastName());
 
        AddressDto addressDto = customerDto.address();
        assertThat(addressDto.street()).isEqualTo(address.getStreet());
        assertThat(addressDto.houseNumber()).isEqualTo(String.valueOf(address.getHouseNumber()));
        assertThat(addressDto.zipcode()).isEqualTo(address.getZipcode());
        assertThat(addressDto.city()).isEqualTo(address.getCity());
    }
 
    @Test
    void givenCustomerDto_whenMaps_thenCustomer() {
        CustomerMapperImpl customerMapper = new CustomerMapperImpl();
        AddressDto addressDto = new AddressDto("street", "42", "zipcode", "city");
        CustomerDto customerDto = new CustomerDto(2L, "John", "Doe", addressDto);
 
        Customer customer = customerMapper.transformToCustomer(customerDto);
        assertThat( customer ).isNotNull();
        assertThat(customer.getCustomerId()).isEqualTo(customerDto.id());
        assertThat(customer.getFirstName()).isEqualTo(customerDto.firstName());
        assertThat(customer.getLastName()).isEqualTo(customerDto.lastName());
 
        Address address = customer.getAddress();
        assertThat(address.getStreet()).isEqualTo(addressDto.street());
        assertThat(address.getHouseNumber()).isEqualTo(Integer.valueOf(addressDto.houseNumber()));
        assertThat(address.getZipcode()).isEqualTo(addressDto.zipcode());
        assertThat(address.getCity()).isEqualTo(addressDto.city());
 
    }
 
}


API和DTO之间的映射

创建映射器

API模型看起来与CustomerDto有所不同,因为它没有Address对象,而CustomerDto中的numberpostalCode有不同的名称。

public class Customer {
 
  private String firstName;
 
  private String lastName;
 
  private String street;
 
  private String number;
 
  private String postalCode;
 
  private String city;
  // Getters and setters left out for brevity
}


为了创建一个映射器,您需要添加更多的@Mapping注解,就像您之前为顾客ID所做的那样。

@Mapper(componentModel = "spring")
public interface CustomerPortMapper {
 
    @Mapping(source = "street", target = "address.street")
    @Mapping(source = "number", target = "address.houseNumber")
    @Mapping(source = "postalCode", target = "address.zipcode")
    @Mapping(source = "city", target = "address.city")
    CustomerDto transformToCustomerDto(Customer customerApi);
 
    @Mapping(source = "id", target = "customerId")
    @Mapping(source = "address.street", target = "street")
    @Mapping(source = "address.houseNumber", target = "number")
    @Mapping(source = "address.zipcode", target = "postalCode")
    @Mapping(source = "address.city", target = "city")
    CustomerFullData transformToCustomerApi(CustomerDto customerDto);
 
}


同样,调用Maven编译目标后,生成的CustomerPortMapperImpl类可以在target/generated-sources/annotations目录中找到。

创建控制器

该映射器被注入到控制器中,并且相应的映射器可以很容易地被使用。

@RestController
class CustomerController implements CustomerApi {
 
    private final CustomerPortMapper customerPortMapper;
    private final CustomerService customerService;
 
    CustomerController(CustomerPortMapper customerPortMapper, CustomerService customerService) {
        this.customerPortMapper = customerPortMapper;
        this.customerService = customerService;
    }
 
    @Override
    public ResponseEntity<CustomerFullData> createCustomer(Customer customerApi) {
 
        CustomerDto customerDtoIn = customerPortMapper.transformToCustomerDto(customerApi);
        CustomerDto customerDtoOut = customerService.createCustomer(customerDtoIn);
 
        return ResponseEntity.ok(customerPortMapper.transformToCustomerApi(customerDtoOut));
    }
 
    @Override
    public ResponseEntity<CustomerFullData> getCustomer(Long customerId) {
        CustomerDto customerDtoOut = customerService.getCustomer(customerId);
        return ResponseEntity.ok(customerPortMapper.transformToCustomerApi(customerDtoOut));
    }
 
}


测试映射器

通过类似于针对服务的单元测试的方式创建了一个单元测试,可以在这里查看。

为了测试完整的应用程序,还创建了一个用于创建客户的集成测试。

@SpringBootTest
@AutoConfigureMockMvc
class CustomerControllerIT {
 
    @Autowired
    private MockMvc mockMvc;
 
    @Test
    void whenCreateCustomer_thenReturnOk() throws Exception {
        String body = """
                {
                  "firstName": "John",
                  "lastName": "Doe",
                  "street": "street",
                  "number": "42",
                  "postalCode": "1234",
                  "city": "city"
                }
                """;
 
 
        mockMvc.perform(post("/customer")
                        .contentType("application/json")
                        .content(body))
                .andExpect(status().isOk())
                .andExpect(jsonPath("firstName", equalTo("John")))
                .andExpect(jsonPath("lastName", equalTo("Doe")))
                .andExpect(jsonPath("customerId", equalTo(0)))
                .andExpect(jsonPath("street", equalTo("street")))
                .andExpect(jsonPath("number", equalTo("42")))
                .andExpect(jsonPath("postalCode", equalTo("1234")))
                .andExpect(jsonPath("city", equalTo("city")));
 
    }
 
}


结论

MapStruct是一个易于使用的用于模型之间映射的库。如果基本的映射不够,甚至可以创建自定义的映射逻辑(这在本博客中没有演示)。建议阅读官方文档,以获得所有可用功能的全面列表。

推荐阅读: 6.线程死锁是如何产生的,如何避免

本文链接: 使用MapStruct生成对象映射