MapStruct是一个效率工具,可以在处理Java Bean映射时,帮助我们尽量减少样板代码,只需要定义接口,它会自动生成映射逻辑。本文中,我们一起看下如何使用MapStruct进行条件映射。
一、准备
在对象之间映射数据时,我们经常需要根据某些条件映射属性,MapStruct提供了一些配置选项来实现这一点。
我们定义一个目标对象License,该对象需要根据一些条件映射属性:
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的映射规则:
- id:如果LicenseDto有id,则赋值;
- startDate:如果LicenseDto没有startDate,设置为当前日期;
- endDate:如果LicenseDto没有endDate,设置为从当前日期起一年后;
- active:如果endDate在未来,我们将其设置为true;
- renewalRequired:如果endDate在接下来的两周内,我们将其设置为true;
- licenseType:如果输入的licenseType不为空且值是预期值INDIVIDUAL或FAMILY之一,则转为枚举并赋值。
上述规则其实很简单,如果是手写代码也能实现,不过既然有工具,还是要高效使用工具。
二、使用MapStruct进行条件映射
我们一起看下,如何通过MapStruct实现上述逻辑。
(一)使用表达式
MapStruct提供了在映射表达式中,使用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表达式中,也能正常运行。
(二)使用条件表达式
MapStruct还提供了条件表达式,允许根据字符串中的条件表达式映射属性。我们一起利用此功能映射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布尔表达式。
在编译时,MapStruct可以生成代码来设置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块中包含条件。当条件匹配时,将使用源中的相应值填充所需的属性。
(三)使用映射前/后操作
在某些情况下,如果我们希望在映射之前或之后通过自定义修改对象,可以使用MapStruct的@BeforeMapping和@AfterMapping注解。
我们可以使用此功能有条件地映射endDate:
@Mapping(target = "endDate", ignore = true)
License toLicense(LicenseDto licenseDto);
我们可以定义AfterMapping注解来有条件地映射endDate。通过这种方式,我们可以根据特定条件控制映射:
@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和目标License对象都作为参数传递给afterMapping方法。确保了MapStruct生成在返回License对象之前作为映射的最后一步调用此方法。
MapStruct生成的代码是:
@Override
public License toLicense(LicenseDto licenseDto) {
if ( licenseDto == null ) {
return null;
}
License license = new License();
// 其余生成的映射...
afterMapping( licenseDto, license );
return license;
}
同样的,我们也可以使用BeforeMapping注解来达到相同的结果。
(四)使用@Condition
在映射时,我们可以使用@Condition为属性添加自定义存在性检查。
默认情况下,MapStruct会对每个属性执行存在性检查,但如果有@Condition注解的方法,则优先使用该方法。
让我们使用此功能映射licenseType。
源对象LicenseDto的licenseType是String类型,在映射期间,如果它不为null且解析为预期的枚举INDIVIDUAL或FAMILY之一,我们需要将其映射到目标。我们先定义条件函数:
@Condition
default boolean mapsToExpectedLicenseType(String licenseType) {
try {
if (licenseType == null) {
return false;
}
License.LicenseType.valueOf(licenseType);
return true;
} catch (IllegalArgumentException e) {
return false;
}
}
方法mapsToExpectedLicenseType的参数类型是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或者炫技的嫌疑。我们要的是少写无营养代码,不是引入太多不可控因素,给自己埋雷。