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仓库获取。