1. 概述

本文将探讨如何自定义MapStruct映射器,将空字符串转换为null值。我们将研究三种实现方案,每种方案提供不同级别的控制能力和灵活性。

2. 示例对象

在开始之前,我们需要创建两个用于演示和测试的映射对象。为保持简洁,我们使用Student作为源对象:

class Student {
    String firstName;
    String lastName;
}

目标对象则使用Teacher

class Teacher {
    String firstName;
    String lastName;
}

这两个类结构非常基础,属性名称完全相同,因此默认情况下映射器无需额外注解即可工作。

3. 全局字符串映射器

我们先看一个覆盖所有场景的解决方案。如果应用需要统一将空字符串转为null,这个方案特别实用。

基础映射器结构在所有示例中保持一致:需要@Mapper注解的接口、接口实例和映射方法:

@Mapper
interface EmptyStringToNullGlobal {
    EmptyStringToNullGlobal INSTANCE = Mappers.getMapper(EmptyStringToNullGlobal.class);

    Teacher toTeacher(Student student);
}

这已经能实现基础映射。但要处理空字符串转换,需在接口中添加额外方法:

String mapEmptyString(String string) {
    return string != null && !string.isEmpty() ? string : null;
}

该方法指示MapStruct:当遇到null或空字符串时返回null,否则保留原值。

MapStruct会对所有字符串自动应用此方法,即使后续添加新映射方法也会生效。如果始终需要此行为,该方案简单、自动复用且代码量最小。 同时它也是通用方案,能实现比当前需求更复杂的转换逻辑。

通过测试验证效果:

@Test
void givenAMapperWithGlobalNullHandling_whenConvertingEmptyString_thenOutputNull() {
    EmptyStringToNullGlobal globalMapper = EmptyStringToNullGlobal.INSTANCE;

    Student student = new Student("Steve", "");

    Teacher teacher = globalMapper.toTeacher(student);
    assertEquals("Steve", teacher.firstName);
    assertNull(teacher.lastName);
}

测试中获取映射器实例后,创建了一个名为Steve但姓氏为空字符串的Student。映射后生成的Teacher对象姓氏为null,证明映射器按预期工作。

4. 使用@Condition注解

4.1. 基础@Condition用法

第二种方案使用@Condition注解。该注解允许创建条件方法,由MapStruct调用以判断是否应映射属性。由于MapStruct默认将未映射字段设为null,我们可利用此特性实现目标。 映射器接口结构相同,但用新方法替换之前的字符串映射器:

@Mapper
interface EmptyStringToNullCondition {
    EmptyStringToNullCondition INSTANCE = Mappers.getMapper(EmptyStringToNullCondition.class);

    Teacher toTeacher(Student student);

    @Condition
    default boolean isNotEmpty(String value) {
        return value != null && !value.isEmpty();
    }
}

isNotEmpty()方法会在每次处理字符串时被调用。返回false时跳过映射(目标字段为null),返回true时执行映射。

测试验证效果:

@Test
void givenAMapperWithConditionAnnotationNullHandling_whenConvertingEmptyString_thenOutputNull() {
    EmptyStringToNullCondition conditionMapper = EmptyStringToNullCondition.INSTANCE;

    Student student = new Student("Steve", "");

    Teacher teacher = conditionMapper.toTeacher(student);
    assertEquals("Steve", teacher.firstName);
    assertNull(teacher.lastName);
}

测试与前例几乎相同,仅替换映射器并验证相同结果。

4.2. 检查目标与源属性名

@Condition方案比全局映射器更灵活。可在isNotEmpty()方法签名中添加两个注解参数:

@Condition
boolean isNotEmpty(String value, @TargetPropertyName String targetPropertyName, @SourcePropertyName String sourcePropertyName) {
    if (sourcePropertyName.equals("lastName")) {
        return value != null && !value.isEmpty();
    }
    return true;
}

这些参数提供源字段和目标字段的名称。可据此对特定字符串特殊处理,甚至完全跳过某些字段的检查。示例中仅对源属性lastName应用检查。当大部分场景需要转换但存在少数例外时,此方案非常实用。

若例外情况过多或需检查大量字段,代码会变得混乱,应考虑其他方案。该方案仅能控制目标字段为null或保留源值,无法修改值本身。

5. 使用表达式

最后看最精确的方案。可在映射方法中使用表达式,每次仅影响单个映射。当此行为不常使用时,该方案最理想。只需在@Mapping注解中添加表达式:

@Mapper
public interface EmptyStringToNullExpression {
    EmptyStringToNullExpression INSTANCE = Mappers.getMapper(EmptyStringToNullExpression.class);

    @Mapping(target = "lastName", expression = "java(student.lastName.isEmpty() ? null : student.lastName)")
    Teacher toTeacher(Student student);
}

此方案提供精确控制。我们指定目标字段并提供Java代码指示映射逻辑:当lastName为空时返回null,否则保留原值。

该方案的优势与局限都在于其精确性。 firstName字段完全不受影响,其他映射器中的字符串更不会涉及。因此当应用中极少需要此类转换时,这是理想选择。

最后测试表达式方案:

@Test
void givenAMapperUsingExpressionBasedNullHandling_whenConvertingEmptyString_thenOutputNull() {
    EmptyStringToNullExpression expressionMapper = EmptyStringToNullExpression.INSTANCE;

    Student student = new Student("Steve", "");

    Teacher teacher = expressionMapper.toTeacher(student);
    assertEquals("Steve", teacher.firstName);
    assertNull(teacher.lastName);
}

再次验证相同逻辑,仅使用表达式映射器。lastName如期转为null,firstName保持不变。

6. 总结

本文介绍了三种使用MapStruct将字符串转为null的方案:

全局映射器:始终需要此行为时的简单选择
@Condition注解:提供更多控制能力,适合有例外场景
表达式:精确控制单个字段,适合极少使用的场景

根据实际需求选择合适方案,避免过度设计。完整示例代码可在GitHub仓库获取。