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 的多参数机制,把 CustomerAddress 合并成 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 的样板代码,同时保持类型安全和高性能。


原始标题:Using Multiple Source Objects with MapStruct | Baeldung

« 上一篇: Java 中的线程模型
» 下一篇: Java 中的方法详解