MapStruct教程-四種條件映射實現(xiàn)
MapStruct是一個效率工具,可以在處理Java Bean映射時,幫助我們盡量減少樣板代碼,只需要定義接口,它會自動生成映射邏輯。本文中,我們一起看下如何使用MapStruct進(jìn)行條件映射。
一、準(zhǔn)備
在對象之間映射數(shù)據(jù)時,我們經(jīng)常需要根據(jù)某些條件映射屬性,MapStruct提供了一些配置選項來實現(xiàn)這一點。
我們定義一個目標(biāo)對象License,該對象需要根據(jù)一些條件映射屬性:
public class License {
private UUID id;
private OffsetDateTime startDate;
private OffsetDateTime endDate;
private boolean active;
private boolean renewalRequired;
private LicenseType licenseType;
public enum LicenseType {
INDIVIDUAL, FAMILY
}
}
在定義一個源對象LicenseDto,包含可選的startDate、endDate和licenseType:
public class LicenseDto {
private UUID id;
private LocalDateTime startDate;
private LocalDateTime endDate;
private String licenseType;
}
以下是從LicenseDto到License的映射規(guī)則:
- id:如果LicenseDto有id,則賦值;
- startDate:如果LicenseDto沒有startDate,設(shè)置為當(dāng)前日期;
- endDate:如果LicenseDto沒有endDate,設(shè)置為從當(dāng)前日期起一年后;
- active:如果endDate在未來,我們將其設(shè)置為true;
- renewalRequired:如果endDate在接下來的兩周內(nèi),我們將其設(shè)置為true;
- licenseType:如果輸入的licenseType不為空且值是預(yù)期值INDIVIDUAL或FAMILY之一,則轉(zhuǎn)為枚舉并賦值。
上述規(guī)則其實很簡單,如果是手寫代碼也能實現(xiàn),不過既然有工具,還是要高效使用工具。
二、使用MapStruct進(jìn)行條件映射
我們一起看下,如何通過MapStruct實現(xiàn)上述邏輯。
(一)使用表達(dá)式
MapStruct提供了在映射表達(dá)式中,使用Java代碼的功能。讓我們利用此功能映射startDate:
@Mapper
public interface LicenseMapper {
@Mapping(target = "startDate", expression = "java(mapStartDate(licenseDto))")
License toLicense(LicenseDto licenseDto);
default OffsetDateTime mapStartDate(LicenseDto licenseDto) {
return licenseDto.getStartDate() != null
? licenseDto.getStartDate().atOffset(ZoneOffset.UTC) : OffsetDateTime.now();
}
}
我們可以在@Mapping定義expression指定該字段賦值的Java代碼。我們看下MapStruct生成的代碼:
@Override
public License toLicense(LicenseDto licenseDto) {
if ( licenseDto == null ) {
return null;
}
License license = new License();
license.setStartDate( mapStartDate(licenseDto) );
// 其余生成的映射...
return license;
}
我們也可以這樣寫:
@Mapping(target = "startDate", expression = "java(licenseDto.getStartDate() != null ? licenseDto.getStartDate().atOffset(java.time.ZoneOffset.UTC) : java.time.OffsetDateTime.now())")
License toLicense(LicenseDto licenseDto);
直接把三元運算符的邏輯放到expression表達(dá)式中,也能正常運行。
(二)使用條件表達(dá)式
MapStruct還提供了條件表達(dá)式,允許根據(jù)字符串中的條件表達(dá)式映射屬性。我們一起利用此功能映射License中的renewalRequired字段:
@Mapping(target = "renewalRequired", conditionExpression = "java(isEndDateInTwoWeeks(licenseDto))", source = ".")
License toLicense(LicenseDto licenseDto);
default boolean isEndDateInTwoWeeks(LicenseDto licenseDto) {
return licenseDto.getEndDate() != null
&& Duration.between(licenseDto.getEndDate(), LocalDateTime.now()).toDays() <= 14;
}
我們可以在java()方法中傳遞任何有效的Java布爾表達(dá)式。
在編譯時,MapStruct可以生成代碼來設(shè)置renewalRequired:
@Override
public License toLicense(LicenseDto licenseDto) {
if ( licenseDto == null ) {
return null;
}
License license = new License();
if ( isEndDateInTwoWeeks(licenseDto) ) {
license.setRenewalRequired( isEndDateInTwoWeeks( licenseDto ) );
}
// 其余生成的映射...
return license;
}
可以看到,生成的代碼在if塊中包含條件。當(dāng)條件匹配時,將使用源中的相應(yīng)值填充所需的屬性。
(三)使用映射前/后操作
在某些情況下,如果我們希望在映射之前或之后通過自定義修改對象,可以使用MapStruct的@BeforeMapping和@AfterMapping注解。
我們可以使用此功能有條件地映射endDate:
@Mapping(target = "endDate", ignore = true)
License toLicense(LicenseDto licenseDto);
我們可以定義AfterMapping注解來有條件地映射endDate。通過這種方式,我們可以根據(jù)特定條件控制映射:
@AfterMapping
default void afterMapping(LicenseDto licenseDto, @MappingTarget License license) {
OffsetDateTime endDate = licenseDto.getEndDate() != null ? licenseDto.getEndDate().atOffset(ZoneOffset.UTC)
: OffsetDateTime.now().plusYears(1);
license.setEndDate(endDate);
}
我們需要將輸入的LicenseDto和目標(biāo)License對象都作為參數(shù)傳遞給afterMapping方法。確保了MapStruct生成在返回License對象之前作為映射的最后一步調(diào)用此方法。
MapStruct生成的代碼是:
@Override
public License toLicense(LicenseDto licenseDto) {
if ( licenseDto == null ) {
return null;
}
License license = new License();
// 其余生成的映射...
afterMapping( licenseDto, license );
return license;
}
同樣的,我們也可以使用BeforeMapping注解來達(dá)到相同的結(jié)果。
(四)使用@Condition
在映射時,我們可以使用@Condition為屬性添加自定義存在性檢查。
默認(rèn)情況下,MapStruct會對每個屬性執(zhí)行存在性檢查,但如果有@Condition注解的方法,則優(yōu)先使用該方法。
讓我們使用此功能映射licenseType。
源對象LicenseDto的licenseType是String類型,在映射期間,如果它不為null且解析為預(yù)期的枚舉INDIVIDUAL或FAMILY之一,我們需要將其映射到目標(biāo)。我們先定義條件函數(shù):
@Condition
default boolean mapsToExpectedLicenseType(String licenseType) {
try {
if (licenseType == null) {
return false;
}
License.LicenseType.valueOf(licenseType);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
方法mapsToExpectedLicenseType的參數(shù)類型是String,與LicenseDto中的licenseType匹配,MapStruct在映射licenseType時生成使用此方法mapsToExpectedLicenseType()的代碼:
@Override
public License toLicense(LicenseDto licenseDto) {
if ( licenseDto == null ) {
return null;
}
License license = new License();
if ( mapsToExpectedLicenseType( licenseDto.getLicenseType() ) ) {
license.setLicenseType( Enum.valueOf( License.LicenseType.class, licenseDto.getLicenseType() ) );
}
// 其余生成的映射...
return license;
}
我們的例子中傳入的是屬性,還可以直接傳入源對象。還有其他的比如@TargetPropertyName、@SourcePropertyName、@Context等高級用法。
在筆者實踐時,這些配置雖然能夠解決問題,但是有過渡依賴MapStruct或者炫技的嫌疑。我們要的是少寫無營養(yǎng)代碼,不是引入太多不可控因素,給自己埋雷。