如何使用 MapStruct 將來源物件對應到目標清單?
1. 概述
在本教程中,我們將使用 MapStruct 庫從來源物件的特定屬性填入目標物件中的List
。雖然 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;
}
//Standard Getter and Setter methods...
}
除了汽車的通常屬性外,在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;
}
//Standard Getter and Setter methods...
}
CarDto
具有常見的屬性、getter 和 setter 方法以及類似Car
類別的建構子。但是,它有一個額外的List
屬性,包含ManufacturingPlantDto
類型的元素:
public class ManufacturingPlant {
private String name;
private String location;
public ManufacturingPlant(String name, String location) {
this.name = name;
this.location = location;
}
}
它有兩個字串屬性: name
和location
。此外,建構函式接受幾個參數來設定這兩個屬性的值。
從Car
物件建立的每個CarDto
物件必須在清單屬性manufacturingPlantDtos
中具有兩個ManufacturingPlantDto
物件。 Car#plant1
和Car#plant2
映射到每個CarDto#manufacturingPlants
列表元素中的ManufacturingPlantDto#name
。類似地, Car#plant1Loc
和Car#plant2Loc
會對應到每個CarDto#manufacturingPlantDtos
元素清單中的ManufacturingPlantDto#location
屬性。
接下來,我們將討論使用 MapStruct 函式庫解決這個用例。
3. 使用表達式映射
通常我們使用@Mapping
註解中的target
和source
屬性,將來源物件屬性對應到目標物件屬性。但是,MapStruct借助expressions
屬性提供了額外的靈活性來處理更複雜的映射。 expression
屬性有助於嵌入自訂Java程式碼來實現複雜的映射邏輯。此外,Java 程式碼可以存取來源物件。
讓我們用carToCarDto()
映射器方法實作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
註解並在方法層級使用@Mapping
註解的基本映射器。 @Mapper
註解有imports
屬性,有助於導入Array
和ManufacturingPlantDto
類別。在介面中,我們必須透過carToCarDto()
方法匯入@Mapping
註解的expression
屬性中嵌入的 Java 程式碼中使用的類別。
此外,Java 程式碼使用Arrays#asList()
方法建立一個具有兩個ManufacturingPlantDto
類別類型元素的List
物件。此外,此表達式從來源Car
物件中提取製造工廠及其位置,並將它們作為參數傳遞給ManufacturingPlantDto
的建構子。此外,我們可以透過從表達式中呼叫其他自訂方法來處理更複雜的場景。為此,我們必須在@Mapper
註解中導入相關的類別。
編譯後,產生的CarMapperImpl
類別使用表達式程式碼實作自訂映射。
現在,讓我們看看產生的CarMapper
介面的實作是否從Car
物件建立CarDto
:
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());
}
在程式的開始處, createCarObject()
方法建立了Car
類別的測試實例:
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;
}
然後,我們將這個Car
實例作為參數傳遞給CarMapper#carToCarDto()
方法。最後,當我們執行這個程式時,產生的CarDto
物件完全按照預期創建,其中CarDto#manufacturingPlantDtos List
屬性中有兩個ManufacturingPlantDto
元素。
4. 使用裝飾器的地圖
MapStruct 庫的裝飾器功能還可以幫助編寫自訂邏輯來映射 Java bean 屬性。我們將進一步了解它。
讓我們考慮一個用於將Car
物件轉換為CarDto
物件的映射器類別:
@Mapper
@DecoratedWith(CarMapperDecorator.class)
public interface CustomCarMapper {
CustomCarMapper INSTANCE = Mappers.getMapper(CustomCarMapper.class);
@Mapping(source = "seats", target = "numberOfSeats")
CarDto carToCarDto(Car car);
}
在CustomCarMapper
介面中,對於Car#seats
和CarDto#numberOfSeats
屬性之間的直接映射, @Mapping
註解非常有效。但是,我們將使用像CarMapperDecorator
類別這樣的裝飾器來實作自訂邏輯,將Car
屬性對應到CarDto#ManufacturingPlantDtos
List
屬性的元素。我們必須使用映射器介面上的@DecoratedWith
註解來配置裝飾器。
接下來,讓我們來看看CarMapperDecorator
類別:
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) {
// some custom logic or transformation which may require calls to other services
return Arrays.asList(
new ManufacturingPlantDto(car.getPlant1(), car.getPlant1Loc()),
new ManufacturingPlantDto(car.getPlant2(), car.getPlant2Loc())
);
}
}
CarMapperDecorator
類別是CustomCarMapper
介面的抽象實作。它覆寫了CustomCarMapper#carToCarDto()
方法,透過@Mapping
註解執行標準映射後呼叫getManufacturingPlantDtos()
方法。方法getManufacturingPlantDtos()
主要處理轉換邏輯。
編譯後產生的特定映射器實作在運行時處理映射。
接下來,讓我們運行該程式:
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());
}
該程式成功地將Car
屬性對應到CarDto
屬性。此外, CarMapperDecorator
類別處理自訂邏輯以建立CarDto#manufacturingPlantDtos
List
屬性。
5. 使用限定符進行映射
我們可以使用@Mapping
註解的qualifiedByName
屬性來指定自訂映射器方法。例如,要從 Car 物件派生CarDto#manufacturingPlantDtos
屬性,我們可以指向方法mapPlants():
@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())
);
}
}
在QualifiedByNameCarMapper
介面中, mapPlants()
方法實作了針對特定目標CarDto#manufacturingPlantDtos
屬性的映射邏輯。
進一步編譯後,產生QualifiedByNameCarMapper's
實作類別。最後,當我們執行程式時, CarDto#manufacturingPlants
屬性會從Car
物件屬性正確建立:
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
註解在source
和target
屬性的幫助下可以處理直接對應。幸運的是,該程式庫還借助表達式、裝飾器和限定符提供了靈活性,以應用複雜的轉換邏輯。因此,它們對提高其可用性和在應用程式開發中的進一步應用做出了巨大貢獻。
與往常一樣,本文中使用的程式碼可在 GitHub 上找到。