How to Use Conditional Mapping With MapStruct – 如何使用 MapStruct 进行条件映射

最后修改: 2023年 9月 7日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.导言

MapStruct is a code generation tool that simplifies mapping between Java bean types. In this article, we’ll explore how to use conditional mapping with MapStruct and look at different configurations to achieve it.

MapStruct是一种代码生成工具,可简化 Java Bean 类型之间的映射。在本文中,我们将探讨如何使用 MapStruct 进行条件映射,并了解实现条件映射的不同配置。

2. Conditional Mapping With MapStruct

2.使用 MapStruct 进行条件映射

When mapping data between objects, we often find the need to map an attribute based on certain conditions, MapStruct offers a few configuration options to achieve this.

在对象间映射数据时,我们经常会发现需要根据某些条件映射属性,MapStruct 提供了一些配置选项来实现这一目的。

Let’s examine an instance of a target License object that requires mapping attributes based on a few conditions:

让我们来看看目标 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
}

The input LicenseDto contains an optional startDate, endDate and licenseType:

输入 LicenseDto 包含一个可选的 startDate, endDate licenseType

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

Here are the mapping rules from LicenseDto to License:

下面是 LicenseDtoLicense 的映射规则:

  • id – if the input LicenseDto has id
  • startDate – if the input LicenseDto doesn’t have startDate in the request, let’s set startDate as the current date
  • endDate – if the input LicenseDto doesn’t have endDate in the request, let’s set endDate as one year from the current date
  • active – if the endDate is in the future, we’ll set this to true
  • renewalRequired – if the endDate is in the next two weeks, we’ll set this to true
  • licenseType – if the input licenseType is available and is one of the expected values INDIVIDUAL or FAMILY.

Let’s explore some of the ways we can achieve this by using the configurations provided by MapStruct.

让我们通过使用 MapStruct 提供的配置来探索实现这一目标的一些方法。

2.1. Using Expressions

2.1.使用表达式

MapStruct provides the capability to use any valid Java expression inside a mapping expression to generate the mapping. Let’s utilize this feature to map startDate:

MapStruct 提供了在映射 表达式中使用任何有效 Java 表达式来生成映射的功能。让我们利用这一功能来映射 startDate

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

We can now define the method mapStartDate:

现在我们可以定义 mapStartDate 方法:

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

On compilation, MapStruct generates the code:

编译时,MapStruct 会生成代码:

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

Alternatively, without using this method, the ternary operation inside the method can be directly passed into the expression as it’s a valid Java expression.

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

2.2. Using Conditional Expressions

2.2.使用条件表达式

Similar to an expression, a condition expression is a feature in MapStruct that allows the mapping of an attribute based on a conditional expression inside a string as Java code. The generated code contains the condition inside an if block, Therefore, let’s utilize this feature to map renewalRequired in the License:

与表达式类似,条件表达式是 MapStruct 中的一项功能,它允许根据字符串中的条件表达式将属性映射为 Java 代码。生成的代码在 if 块中包含条件,因此,让我们利用此功能将 renewalRequired 映射到 License 中:

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

We can pass in any valid Java boolean expression inside the java() method. Let’s define the isEndDateInTwoWeeks method inside the mapper interface:

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

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

On compile, MapStruct generates the code to set renewalRequired if this condition is satisfied:

编译时,如果满足该条件,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;
}

It’s also possible to set the value of an attribute from the source when a condition matches. In such instances, the mapper will populate the desired attribute with the corresponding value from the source.

当条件匹配时,也可以设置源属性的值。在这种情况下,映射器会用源文件中的相应值填充所需的属性。

2.3. Using Before/After Mapping

2.3.使用前后映射

In certain situations, if we want to modify the object before or after mapping through customization, we can make use of the @BeforeMapping and @AfterMapping annotations from MapStruct. Let’s use this feature to map endDate:

在某些情况下,如果我们想通过自定义在映射之前或之后修改对象,我们可以使用 MapStruct 中的 @BeforeMapping@AfterMapping 注解。让我们使用此功能来映射 endDate

@Mapping(target = "endDate", ignore = true)
License toLicense(LicenseDto licenseDto);

We can define the AfterMapping annotation to map endDate conditionally. In this way, we can control the mapping based on specific conditions:

我们可以定义 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);
}

We need to pass both the input LicenseDto and the target License object as parameters to this afterMapping method. Consequently, this ensures that MapStruct generates the code that invokes this method as a final step of mapping, before returning the License object:

我们需要将输入 LicenseDto 和目标 License 对象作为参数传递给此 afterMapping 方法。因此,这将确保 MapStruct 在返回 License 对象之前,生成调用此方法的代码,作为映射的最后一步:

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

    // Rest of generated mapping...

    afterMapping( licenseDto, license );

    return license;
}

Alternatively, we can use the BeforeMapping annotation instead to achieve the same result.

或者,我们也可以使用 BeforeMapping 注解来实现相同的效果。

2.4. Using @Condition

2.4.使用 @Condition

When mapping, we can add a custom presence check to an attribute using @Condition. By default, MapStruct performs a presence check for every attribute, but gives preference to a method annotated with @Condition if available.

在映射时,我们可以使用 @Condition 为属性添加自定义存在检查。默认情况下,MapStruct 会对每个属性执行存在性检查,但如果可用,则会优先使用注释了 @Condition 的方法。

Let’s use this feature to map licenseType. The input LicenseDto receives licenseType as a String, and during mapping, we need to map it to the target if it’s not null and resolves to one of the expected enums INDIVIDUAL or FAMILY:

让我们使用这一功能来映射 licenseType 。输入 LicenseDtolicenseType 作为 String 接收,在映射过程中,如果它不是空值并解析为预期的枚举 INDIVIDUALFAMILY 之一,我们就需要将它映射到目标:

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

MapStruct generates the code to use this method mapsToExpectedLicenseType() when mapping licenseType because the signature String matches with the licenseType in LicenseDto:

在映射 licenseType 时,MapStruct 会生成使用此方法 mapsToExpectedLicenseType() 的代码,因为签名 StringLicenseDto: 中的 licenseType 匹配。

@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. Conclusion

3.结论

In this article, we explored different ways to map attributes between Java bean types conditionally using MapStruct.

在本文中,我们探讨了使用 MapStruct 在 Java Bean 类型之间有条件地映射属性的不同方法。

As always, the source code for the examples is available over on GitHub.

与往常一样,这些示例的源代码可在 GitHub 上获取。