1. 概述
本文将介绍如何在 MapStruct 中使用多个源对象进行对象映射。这是在复杂业务场景中非常实用的功能,尤其适用于需要整合多个领域对象生成 DTO 或视图模型的场景。
2. 单一源对象映射
MapStruct 最常见的用法是从一个源对象映射到目标对象。比如我们有一个 Customer
类:
class Customer {
private String firstName;
private String lastName;
// getters and setters
}
对应地,存在一个数据传输对象 CustomerDto
:
class CustomerDto {
private String forename;
private String surname;
// getters and setters
}
此时可以定义一个简单的映射接口:
@Mapper
public interface CustomerDtoMapper {
@Mapping(source = "firstName", target = "forename")
@Mapping(source = "lastName", target = "surname")
CustomerDto from(Customer customer);
}
这种写法老手都懂,不赘述了——✅标准用法,简单粗暴。
3. 多源对象映射
⚠️实际开发中更常见的是:目标对象的字段来自多个源对象。举个典型例子:电商系统中的配送地址。
假设我们需要构建一个 DeliveryAddress
,它既包含用户信息(姓名),又包含地址信息(街道、邮编等):
class DeliveryAddress {
private String forename;
private String surname;
private String street;
private String postalcode;
private String county;
// getters and setters
}
而用户地址单独抽象为 Address
类:
class Address {
private String street;
private String postalcode;
private String county;
// getters and setters
}
这时就可以通过 MapStruct 的多参数机制,把 Customer
和 Address
合并成 DeliveryAddress
:
@Mapper
interface DeliveryAddressMapper {
@Mapping(source = "customer.firstName", target = "forename")
@Mapping(source = "customer.lastName", target = "surname")
@Mapping(source = "address.street", target = "street")
@Mapping(source = "address.postalcode", target = "postalcode")
@Mapping(source = "address.county", target = "county")
DeliveryAddress from(Customer customer, Address address);
}
✅关键点总结:
- 多个参数时,使用 点号语法(dot notation)指定字段来源,如
"customer.firstName"
- 参数名必须与方法签名中的变量名一致
- 支持任意数量的源对象,不限于两个
来个测试验证一下:
// given
Customer customer = new Customer()
.setFirstName("Max")
.setLastName("Powers");
Address homeAddress = new Address()
.setStreet("123 Some Street")
.setCounty("Nevada")
.setPostalcode("89123");
// when
DeliveryAddress deliveryAddress = deliveryAddressMapper.from(customer, homeAddress);
// then
assertEquals(deliveryAddress.getForename(), customer.getFirstName());
assertEquals(deliveryAddress.getSurname(), customer.getLastName());
assertEquals(deliveryAddress.getStreet(), homeAddress.getStreet());
assertEquals(deliveryAddress.getCounty(), homeAddress.getCounty());
assertEquals(deliveryAddress.getPostalcode(), homeAddress.getPostalcode());
运行通过,说明合并映射成功。
4. 使用 @MappingTarget 更新已有对象
前面的例子都是生成新实例。但有时候我们只想更新已有对象的部分字段,而不是创建新对象——这在 REST API 的 PATCH
操作中很常见。
MapStruct 提供了 @MappingTarget
注解来实现“就地更新”。
例如,我们只想更新配送地址中的地理信息(忽略姓名):
@Mapper
interface DeliveryAddressMapper {
@Mapping(source = "address.postalcode", target = "postalcode")
@Mapping(source = "address.county", target = "county")
@Mapping(source = "address.street", target = "street")
DeliveryAddress updateAddress(@MappingTarget DeliveryAddress deliveryAddress, Address address);
}
注意:
@MappingTarget
标注的参数就是将被修改的目标对象- 方法返回值仍然是该对象(引用不变)
测试一下是否真的“就地”更新:
// given
DeliveryAddress deliveryAddress = new DeliveryAddress()
.setForename("Max")
.setSurname("Powers")
.setStreet("123 Some Street")
.setCounty("Nevada")
.setPostalcode("89123");
Address newAddress = new Address()
.setStreet("456 Some other street")
.setCounty("Arizona")
.setPostalcode("12345");
// when
DeliveryAddress updatedDeliveryAddress = deliveryAddressMapper.updateAddress(deliveryAddress, newAddress);
// then
assertSame(deliveryAddress, updatedDeliveryAddress); // 引用相同,说明是同一个对象
assertEquals("456 Some other street", deliveryAddress.getStreet());
assertEquals("Arizona", deliveryAddress.getCounty());
assertEquals("12345", deliveryAddress.getPostalcode());
✅ 成功更新且未新建对象,适合性能敏感场景。
5. 使用 @Context 传递上下文参数
有时候映射过程需要额外的帮助对象,比如格式化工具、安全上下文、租户信息等。这些不属于源对象,也不能写死在 Mapper 里。
MapStruct 提供了 @Context
注解来传递“辅助参数”。
场景示例:姓名标准化
我们有一个工具类用于清理和标准化姓名:
public class MappingContext {
public String normalizeName(String name) {
return name == null ? null : name.trim().toUpperCase();
}
}
现在希望在映射完成后自动对姓名执行标准化处理。可以结合 @AfterMapping
和 @Context
实现:
@Mapper
public interface CustomerDtoMapper {
@Mapping(source = "firstName", target = "forename")
@Mapping(source = "lastName", target = "surname")
CustomerDto from(Customer customer, @Context MappingContext context);
@AfterMapping
default void normalize(@MappingTarget CustomerDto dto, @Context MappingContext context) {
dto.setForename(context.normalizeName(dto.getForename()));
dto.setSurname(context.normalizeName(dto.getSurname()));
}
}
调用时传入上下文实例:
Customer customer = new Customer();
customer.setFirstName(" max ");
customer.setLastName(" powers ");
MappingContext context = new MappingContext();
CustomerDto dto = customerDtoMapper.from(customer, context);
断言结果:
assertEquals("MAX", dto.getForename());
assertEquals("POWERS", dto.getSurname());
✅ 使用建议:
@Context
适合传递不可变的辅助对象(如 formatter、locale、security context)- 可在
@BeforeMapping
/@AfterMapping
中使用,增强灵活性 - ❌ 不要滥用,避免把业务逻辑塞进 Mapper
6. 总结
MapStruct 对多源对象的支持非常强大且实用,常见应用场景包括:
场景 | 实现方式 |
---|---|
合并多个实体生成 DTO | 多参数 + 点号语法 |
部分更新已有对象 | @MappingTarget |
注入辅助逻辑 | @Context + @AfterMapping |
⚠️踩坑提醒:
- 多参数顺序不影响映射,MapStruct 按类型+名称匹配
@MappingTarget
必须是方法参数之一,且只能出现一次@Context
参数不能作为 source 直接映射,需配合生命周期回调使用
合理使用这些特性,能大幅减少手动 set/get 的样板代码,同时保持类型安全和高性能。