当前位置:Java -> 使用MapStruct生成对象映射
你是否需要编写大量映射代码来在不同对象模型之间进行映射?MapStruct通过生成映射代码简化了这个任务。在这篇博客中,您将了解MapStruct的一些基本特性。祝您阅读愉快!
在多层应用程序中,人们经常需要编写样板代码以将不同的对象模型进行映射。这可能是一项繁琐且易出错的任务。 MapStruct 通过为您生成映射代码来简化此任务。它在编译时生成代码,并旨在生成的代码看起来就像是您自己编写的一样。
本博客将仅对MapStruct如何帮助您进行基本概述,但这将足以让您对它能解决哪些问题有良好的印象。
如果您使用IntelliJ作为IDE,还可以安装 MapStruct支持插件,它将帮助您使用MapStruct。
本博客中使用的资源可在 GitHub 上找到。
本博客的先决条件包括:
本博客中使用的应用程序是一个基本的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
}
为了将所有内容连接在一起,您需要为以下内容编写映射器代码:
要使用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>
CustomerDto
、AddressDto
与 Customer
、Address
领域之间并没有很大的区别。
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;
}
}
如您所见,代码非常易读,考虑了 Customer
和 Address
的映射。
服务将创建一个名为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模型看起来与CustomerDto
有所不同,因为它没有Address
对象,而CustomerDto
中的number
和postalCode
有不同的名称。
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生成对象映射