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方法...
}
CarDto
与 Car
结构相似,但关键区别在于包含 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#plant1
→ManufacturingPlantDto#name
Car#plant1Loc
→ManufacturingPlantDto#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
导入 Arrays
和 ManufacturingPlantDto
✅ 表达式使用 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
指定装饰器类
✅ 常规属性(如 seats
→ numberOfSeats
)仍用 @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. 使用限定符映射
通过 @Mapping
的 qualifiedByName
属性可指定自定义映射方法,实现细粒度控制。
实现方案:
@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
引用方法名
✅ 自定义方法接收源对象作为参数
⚠️ 方法需声明为 default
或 static
验证代码:
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 通过这些机制,在保持注解简洁性的同时,提供了足够的灵活性应对复杂映射场景,显著提升了开发效率。