1. 概述

MapStruct 是一个简化 Java 对象类型之间映射的代码生成工具。 在本文中,我们将探讨如何使用 MapStruct 实现条件映射,并查看不同的配置选项。

2. 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
    }
    // getters and setters
}

输入 LicenseDto 包含可选的 startDate, endDatelicenseType

public class LicenseDto {
    private UUID id;
    private LocalDateTime startDate;
    private LocalDateTime endDate;
    private String licenseType;
    // getters and setters
}

以下是 LicenseDto 映射到 License 的规则:

  • id - 如果输入 LicenseDtoid
  • startDate - 如果输入 LicenseDto 请求中没有 startDate,则设置为当前日期
  • endDate - 如果输入 LicenseDto 请求中没有 endDate,则设置为当前日期后的一年
  • active - 如果 endDate 在未来,设置为 true
  • renewalRequired - 如果 endDate 在接下来的两周内,设置为 true
  • licenseType - 如果输入 licenseType 存在且是预期值之一(INDIVIDUAL 或 FAMILY)。

接下来,我们将探讨如何利用 MapStruct 提供的配置来实现这些规则。

2.1. 使用表达式

MapStruct 允许在映射的 表达式 中使用任何有效的 Java 表达式,以生成映射代码。 我们可以利用这个特性来映射 startDate

@Mapping(target = "startDate", expression = "java(mapStartDate(licenseDto))")
License toLicense(LicenseDto licenseDto);

现在我们可以定义方法 mapStartDate

default OffsetDateTime mapStartDate(LicenseDto licenseDto) {
    return licenseDto.getStartDate() != null ? 
      licenseDto.getStartDate().atOffset(ZoneOffset.UTC) : OffsetDateTime.now();
}

编译时,MapStruct 会生成如下代码:

@Override
public License toLicense(LicenseDto licenseDto) {
    License license = new License();
    license.setStartDate( mapStartDate(licenseDto) );
    
    // Rest of generated mapping...
    return license;
}

如果不使用此方法,方法内的三元操作可以直接作为 expression 传递,因为它是一个有效的 Java 表达式。

2.2. 使用条件表达式

类似于表达式,MapStruct 的 条件表达式 是一个功能,允许根据字符串形式的 Java 代码中的条件表达式来映射属性。生成的代码包含条件在 if 块中。让我们利用这个特性来映射 renewalRequiredLicense

@Mapping(target = "renewalRequired", conditionExpression = "java(isEndDateInTwoWeeks(licenseDto))", source = ".")
License toLicense(LicenseDto licenseDto);

我们可以在 java() 方法中传递任何有效的布尔表达式。在映射器接口中定义 isEndDateInTwoWeeks 方法:

default boolean isEndDateInTwoWeeks(LicenseDto licenseDto) {
    return licenseDto.getEndDate() != null 
       && Duration.between(licenseDto.getEndDate(), LocalDateTime.now()).toDays() <= 14;
}

编译时,MapStruct 会在满足条件时生成设置 renewalRequired 的代码:

@Override
public License toLicense(LicenseDto licenseDto) {
    License license = new License();

    if ( isEndDateInTwoWeeks(licenseDto) ) {
        license.setRenewalRequired( isEndDateInTwoWeeks( licenseDto ) );
    }
    
    // Rest of generated mapping...

    return license;
}

还可以在条件匹配时从源对象设置属性的值。在这种情况下,映射器将根据源对象的相应值填充所需的属性。

2.3. 使用 Before/After 映射

在某些情况下,如果我们希望通过自定义修改对象在映射前后,可以使用 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);
}

在这个 afterMapping 方法中,我们需要传递输入 LicenseDto 和目标 License 对象作为参数。这样,MapStruct 会在返回 License 对象之前生成调用此方法的代码,作为映射过程的最终步骤:

@Override
public License toLicense(LicenseDto licenseDto) {
    License license = new License();

    // Rest of generated mapping...

    afterMapping( licenseDto, license );

    return license;
}

也可以使用 BeforeMapping 注解来达到相同的效果。

2.4. 使用 @Condition

在映射时,我们可以通过添加自定义存在检查到属性上使用 @Condition 注解。默认情况下,MapStruct 为每个属性进行存在检查,但如果可用,会优先使用带有 @Condition 注解的方法。

让我们利用这个特性来映射 licenseType。输入 LicenseDto 接收 licenseType 作为 String,在映射过程中,我们需要在 licenseType 不为 null 并且是预期枚举值(INDIVIDUAL 或 FAMILY)时进行映射:

@Condition
default boolean mapsToExpectedLicenseType(String licenseType) {
    try {
        return licenseType != null && License.LicenseType.valueOf(licenseType) != null;
    } catch (IllegalArgumentException e) {
        return false;
    }
}

由于 String 类型与 LicenseDto 中的 licenseType 匹配,MapStruct 会在映射 licenseType 时生成使用 mapsToExpectedLicenseType() 方法的代码:

@Override
public License toLicense(LicenseDto licenseDto) {
    if ( LicenseMapper.mapsToExpectedLicenseType( licenseDto.getLicenseType() ) ) {
        license.setLicenseType( Enum.valueOf( License.LicenseType.class, licenseDto.getLicenseType() ) );
    }

    // Rest of generated mapping...
    return license;
}

3. 总结

在这篇文章中,我们探讨了使用 MapStruct 条件性地在 Java 对象类型之间映射属性的不同方法。

如常,示例代码可在 GitHub 上找到。