MapStruct教程-四种条件映射实现

开发 前端
我们的例子中传入的是属性,还可以直接传入源对象。还有其他的比如@TargetPropertyName、@SourcePropertyName、@Context等高级用法。

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或者炫技的嫌疑。我们要的是少写无营养代码,不是引入太多不可控因素,给自己埋雷。

责任编辑:武晓燕 来源: 看山的小屋
相关推荐

2021-12-22 09:34:01

Golagn配置方式

2011-03-16 09:05:53

NATiptables

2024-11-27 11:12:32

MapStructArticlePerson

2010-08-05 09:33:08

Flex页面跳转

2015-04-13 11:39:26

VDI灾难恢复

2015-04-02 16:54:52

灾难恢复VDI灾难恢复

2022-01-12 11:02:01

云计算安全技术

2023-07-11 10:24:00

分布式限流算法

2021-10-24 08:37:18

网络监控网络架构网络

2015-03-19 15:13:20

PHP基本排序算法代码实现

2010-07-16 13:50:53

Perl哈希表

2011-07-06 18:07:16

ASP

2020-01-21 19:15:23

漏洞安全IT

2010-09-02 10:55:57

CSS

2024-11-07 11:17:50

2021-06-17 14:46:50

框架jQuery开发

2010-08-05 13:44:12

Flex布局

2011-11-24 16:34:39

Java

2013-10-17 09:25:52

2023-11-27 13:42:00

消息队列RocketMQ
点赞
收藏

51CTO技术栈公众号