1. 概述

本文将使用 MapStruct 库实现:从源对象的特定属性生成目标对象中的列表。虽然 MapStruct 主要依赖 注解 完成对象转换,但当注解无法满足需求时,它也提供了灵活的自定义转换方案。

我们将从注解无法处理的典型场景切入,逐步探索 MapStruct 的替代解决方案,最后通过运行验证代码确认方案有效性。

2. 用例

先分析核心类结构:

对象到列表映射示意图

源对象 Car 类:

public class Car {
    private String make;
    private String model;
    private int year;
    private int seats;

    private String plant1;
    private String plant1Loc;
    private String plant2;
    private String plant2Loc;

    public Car(String make, String model, int year, int seats) {
        this.make = make;
        this.model = model;
        this.year = year;
        this.seats = seats;
    }
    
    //标准Getter和Setter方法...
}

除了常规汽车属性,Car 类还包含两个工厂及其位置的四组属性。构造函数接收四个核心参数。

目标对象 CarDto 类:

public class CarDto {
    private String make;
    private String model;
    private int year;
    private int numberOfSeats;
    private List<ManufacturingPlantDto> manufacturingPlantDtos;

    public CarDto(String make, String model, int year, int numberOfSeats) {
        this.make = make;
        this.model = model;
        this.year = year;
        this.numberOfSeats = numberOfSeats;
    }
    
    //标准Getter和Setter方法...
}

CarDtoCar 结构相似,但关键区别在于包含 List<ManufacturingPlantDto> 属性:

public class ManufacturingPlant {
    private String name;
    private String location;

    public ManufacturingPlant(String name, String location) {
        this.name = name;
        this.location = location;
    }
}

每个 CarDto 对象需包含两个 ManufacturingPlantDto 实例,映射规则:

  • Car#plant1ManufacturingPlantDto#name
  • Car#plant1LocManufacturingPlantDto#location
  • Car#plant2 → 第二个元素的 name
  • Car#plant2Loc → 第二个元素的 location

接下来讨论 MapStruct 的实现方案。

3. 使用表达式映射

常规场景下,@Mapping 注解的 source/target 属性足以处理简单映射。但面对复杂转换时,可通过 expressions 属性嵌入 Java 代码。表达式可直接访问源对象,实现灵活转换逻辑

实现 CarMapper 接口:

@Mapper(imports = { Arrays.class, ManufacturingPlantDto.class })
public interface CarMapper {
    CarMapper INSTANCE = Mappers.getMapper(CarMapper.class);

    @Mapping(target = "numberOfSeats", source = "seats")
    @Mapping(
        target = "manufacturingPlantDtos",
        expression = """
          java(Arrays.asList(
          new ManufacturingPlantDto(car.getPlant1(), car.getPlant1Loc()),
          new ManufacturingPlantDto(car.getPlant2(), car.getPlant2Loc())
            ))
        """
    )
    CarDto carToCarDto(Car car);
}

关键点说明: ✅ @Mapper 注解通过 imports 导入 ArraysManufacturingPlantDto
✅ 表达式使用 Arrays.asList() 创建包含两个元素的列表
✅ 从源对象提取工厂信息,直接构造 ManufacturingPlantDto 实例
⚠️ 若需调用其他自定义方法,需在 @Mapper 中导入相关类

编译后生成的实现类会直接嵌入表达式代码。验证效果:

void whenUseMappingExpression_thenConvertCarToCarDto() {
    Car car = createCarObject();

    CarDto carDto = CarMapper.INSTANCE.carToCarDto(car);

    assertEquals("Morris", carDto.getMake());
    assertEquals("Mini", carDto.getModel());
    assertEquals(1969, carDto.getYear());
    assertEquals(4, carDto.getNumberOfSeats());

    validateTargetList(carDto.getManufacturingPlantDtos());
}

测试数据准备:

Car createCarObject() {
    Car car = new Car("Morris", "Mini", 1969, 4);
    car.setPlant1("Oxford");
    car.setPlant1Loc("United Kingdom");
    car.setPlant2("Swinden");
    car.setPlant2Loc("United Kingdom");
    return car;
}

运行结果:CarDto 对象正确生成,且 manufacturingPlantDtos 列表包含两个预期的工厂信息。

4. 使用装饰器映射

MapStruct 的 装饰器 机制允许编写自定义映射逻辑,特别适合处理复杂属性转换。

定义基础映射接口:

@Mapper
@DecoratedWith(CarMapperDecorator.class)
public interface CustomCarMapper {
    CustomCarMapper INSTANCE = Mappers.getMapper(CustomCarMapper.class);

    @Mapping(source = "seats", target = "numberOfSeats")
    CarDto carToCarDto(Car car);
}

关键设计: ✅ @DecoratedWith 指定装饰器类
✅ 常规属性(如 seatsnumberOfSeats)仍用 @Mapping 处理
✅ 列表属性交给装饰器实现

装饰器实现:

public abstract class CarMapperDecorator implements CustomCarMapper {
    private final Logger logger = LoggerFactory.getLogger(CarMapperDecorator.class);
    private CustomCarMapper delegate;

    public CarMapperDecorator(CustomCarMapper delegate) {
        this.delegate = delegate;
    }

    @Override
    public CarDto carToCarDto(Car car) {
        CarDto carDto = delegate.carToCarDto(car); // 先执行常规映射

        carDto.setManufacturingPlantDtos(getManufacturingPlantDtos(car)); // 自定义处理

        return carDto;
    }

    private List getManufacturingPlantDtos(Car car) {
    // 可在此调用其他服务或复杂转换逻辑
        return Arrays.asList(
          new ManufacturingPlantDto(car.getPlant1(), car.getPlant1Loc()),
          new ManufacturingPlantDto(car.getPlant2(), car.getPlant2Loc())
       );
    }
}

装饰器特点:

  • 继承基础映射接口(需声明为 abstract
  • 通过委托对象 delegate 执行基础映射
  • 在基础映射结果上追加自定义逻辑

验证代码:

void whenUsingDecorator_thenConvertCarToCarDto() {
    Car car = createCarObject();

    CarDto carDto = CustomCarMapper.INSTANCE.carToCarDto(car);

    assertEquals("Morris", carDto.getMake());
    assertEquals("Mini", carDto.getModel());
    assertEquals(1969, carDto.getYear());
    assertEquals(4, carDto.getNumberOfSeats());

    validateTargetList(carDto.getManufacturingPlantDtos());
}

运行结果:装饰器成功处理列表属性,其他属性由基础映射完成。

5. 使用限定符映射

通过 @MappingqualifiedByName 属性可指定自定义映射方法,实现细粒度控制。

实现方案:

@Mapper
public interface QualifiedByNameCarMapper {
    QualifiedByNameMapper INSTANCE = Mappers.getMapper(QualifiedByNameMapper.class);

    @Mapping(source = "seats", target = "numberOfSeats")
    @Mapping(target = "manufacturingPlantDtos", source = "car", qualifiedByName = "mapPlants")
    CarDto carToCarDto(Car car);

    @Named("mapPlants")
    default List<ManufacturingPlantDto> mapPlants(Car car) {
        return List.of(
          new ManufacturingPlantDto(car.getPlant1(), car.getPlant1Loc()),
          new ManufacturingPlantDto(car.getPlant2(), car.getPlant2Loc())
        );
    }
}

核心机制: ✅ @Named 注解标记自定义方法
qualifiedByName 引用方法名
✅ 自定义方法接收源对象作为参数
⚠️ 方法需声明为 defaultstatic

验证代码:

void whenUsingQualifiedByName_thenConvertCarToCarDto() {
    Car car = createCarObject();

    CarDto carDto = QualifiedByNameCarMapper.INSTANCE.carToCarDto(car);

    assertEquals("Morris", carDto.getMake());
    assertEquals("Mini", carDto.getModel());
    assertEquals(1969, carDto.getYear());
    assertEquals(4, carDto.getNumberOfSeats());

    validateTargetList(carDto.getManufacturingPlantDtos());
}

运行结果:限定符方法成功生成列表属性。

6. 结论

本文展示了三种将源对象属性映射到目标列表的方案:

方案 适用场景 优点 缺点
表达式 简单转换逻辑 代码简洁,直接嵌入 复杂逻辑可读性下降
装饰器 需调用外部服务/复杂处理 逻辑分离,可扩展性强 需额外定义装饰器类
限定符 可复用的转换逻辑 方法可复用,结构清晰 需显式声明方法名

核心建议

  • 优先尝试 @Mapping 的基础映射
  • 简单转换用表达式快速实现
  • 复杂逻辑优先考虑限定符方法
  • 需外部依赖时使用装饰器

MapStruct 通过这些机制,在保持注解简洁性的同时,提供了足够的灵活性应对复杂映射场景,显著提升了开发效率。


原始标题:How to Map a Source Object to the Target List Using MapStruct? | Baeldung