1. 介绍
本文将介绍如何使用 MapStruct 实现枚举类型之间的映射,包括:
- 枚举到其他枚举的映射
- 枚举与基本数据类型(如
int
、String
)的双向转换
2. Maven 依赖
在 pom.xml
中添加以下依赖:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.0.Beta1</version>
</dependency>
最新稳定版可在 Maven 中央仓库 获取。
3. 枚举到枚举的映射
3.1 典型应用场景
✅ REST API 转换:将外部接口的状态码映射为应用内部枚举
✅ 微服务数据交互:在不同服务间转换相似枚举类型
✅ 第三方库集成:将外部枚举转换为应用内部枚举
3.2 使用 MapStruct 实现映射
核心注解 @ValueMapping
用于配置枚举值映射规则:
- 支持同名映射(默认行为)
- 支持异名映射(如
"Go"
→"Move"
) - 支持多源值映射到同一目标值
示例:交通信号灯映射
源枚举:
public enum TrafficSignal {
Off, Stop, Go
}
目标枚举:
public enum RoadSign {
Off, Halt, Move
}
映射接口:
@Mapper
public interface TrafficSignalMapper {
TrafficSignalMapper INSTANCE = Mappers.getMapper(TrafficSignalMapper.class);
@ValueMapping(target = "Off", source = "Off")
@ValueMapping(target = "Go", source = "Move")
@ValueMapping(target = "Stop", source = "Halt")
TrafficSignal toTrafficSignal(RoadSign source);
}
⚠️ 关键点:必须显式映射所有枚举值,否则可能引发意外行为。
测试验证
@Test
void whenRoadSignIsMapped_thenGetTrafficSignal() {
RoadSign source = RoadSign.Move;
TrafficSignal target = TrafficSignalMapper.INSTANCE.toTrafficSignal(source);
assertEquals(TrafficSignal.Go, target);
}
建议:对所有映射方法编写单元测试,确保行为符合预期。
4. String 到枚举的映射
4.1 典型应用场景
✅ 用户输入处理:将字符串指令映射为枚举(如 "add"
→ Operation.ADD
)
✅ 配置解析:将配置字符串转为枚举(如 "EXEC"
→ Mode.EXEC
)
✅ 外部 API 集成:将状态字符串转为枚举(如 "active"
→ Status.ACTIVE
)
4.2 实现映射
使用 @ValueMapping
进行字符串到枚举的映射:
@ValueMapping(target = "Off", source = "Off")
@ValueMapping(target = "Go", source = "Move")
@ValueMapping(target = "Stop", source = "Halt")
TrafficSignal stringToTrafficSignal(String source);
测试验证
@Test
void whenStringIsMapped_thenGetTrafficSignal() {
String source = RoadSign.Move.name();
TrafficSignal target = TrafficSignalMapper.INSTANCE.stringToTrafficSignal(source);
assertEquals(TrafficSignal.Go, target);
}
5. 自定义名称转换
当枚举值仅因命名规范不同时(如大小写、前缀/后缀差异),可通过以下策略处理:
5.1 添加后缀
目标枚举:
public enum TrafficSignalSuffixed { Off_Value, Stop_Value, Go_Value }
映射配置:
@EnumMapping(nameTransformationStrategy = MappingConstants.SUFFIX_TRANSFORMATION, configuration = "_Value")
TrafficSignalSuffixed applySuffix(TrafficSignal source);
测试验证
@ParameterizedTest
@CsvSource({"Off,Off_Value", "Go,Go_Value"})
void whenTrafficSignalIsMappedWithSuffix_thenGetTrafficSignalSuffixed(TrafficSignal source, TrafficSignalSuffixed expected) {
TrafficSignalSuffixed result = TrafficSignalMapper.INSTANCE.applySuffix(source);
assertEquals(expected, result);
}
5.2 添加前缀
目标枚举:
public enum TrafficSignalPrefixed { Value_Off, Value_Stop, Value_Go }
映射配置:
@EnumMapping(nameTransformationStrategy = MappingConstants.PREFIX_TRANSFORMATION, configuration = "Value_")
TrafficSignalPrefixed applyPrefix(TrafficSignal source);
5.3 移除后缀
映射配置:
@EnumMapping(nameTransformationStrategy = MappingConstants.STRIP_SUFFIX_TRANSFORMATION, configuration = "_Value")
TrafficSignal stripSuffix(TrafficSignalSuffixed source);
5.4 移除前缀
映射配置:
@EnumMapping(nameTransformationStrategy = MappingConstants.STRIP_PREFIX_TRANSFORMATION, configuration = "Value_")
TrafficSignal stripPrefix(TrafficSignalPrefixed source);
5.5 转小写
目标枚举:
public enum TrafficSignalLowercase { off, stop, go }
映射配置:
@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "lower")
TrafficSignalLowercase applyLowercase(TrafficSignal source);
5.6 转大写
目标枚举:
public enum TrafficSignalUppercase { OFF, STOP, GO }
映射配置:
@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "upper")
TrafficSignalUppercase applyUppercase(TrafficSignal source);
5.7 首字母大写
映射配置:
@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "captial")
TrafficSignal lowercaseToCapital(TrafficSignalLowercase source);
6. 其他枚举映射场景
6.1 枚举到 String 的映射
映射配置:
@ValueMapping(target = "Off", source = "Off")
@ValueMapping(target = "Go", source = "Go")
@ValueMapping(target = "Stop", source = "Stop")
String trafficSignalToString(TrafficSignal source);
测试验证
@Test
void whenTrafficSignalIsMapped_thenGetString() {
TrafficSignal source = TrafficSignal.Go;
String target = TrafficSignalMapper.INSTANCE.trafficSignalToString(source);
assertEquals("Go", target);
}
6.2 枚举到数值类型的映射
包装类:
public class TrafficSignalNumber {
private Integer number;
// getter/setter 省略
}
映射配置:
@Mapping(target = "number", source = ".")
TrafficSignalNumber trafficSignalToTrafficSignalNumber(TrafficSignal source);
default Integer convertTrafficSignalToInteger(TrafficSignal source) {
switch (source) {
case Off: return 0;
case Stop: return 1;
case Go: return 2;
default: return null;
}
}
测试验证
@ParameterizedTest
@CsvSource({"Off,0", "Stop,1"})
void whenTrafficSignalIsMapped_thenGetInt(TrafficSignal source, int expected) {
Integer intResult = TrafficSignalMapper.INSTANCE.convertTrafficSignalToInteger(source);
TrafficSignalNumber objResult = TrafficSignalMapper.INSTANCE.trafficSignalToTrafficSignalNumber(source);
assertEquals(expected, intResult.intValue());
assertEquals(expected, objResult.getNumber().intValue());
}
7. 处理未知枚举值
7.1 MapStruct 的默认行为
⚠️ 当源枚举值无对应目标值时,MapStruct 会:
- 编译时报错(未处理所有枚举值)
- 运行时抛出异常(未处理未知输入)
7.2 映射剩余值(ANY_REMAINING)
适用场景:将所有未显式映射的值转为默认值
示例:
public enum SimpleTrafficSignal { Off, On }
映射配置:
@ValueMapping(target = "On", source = "Go")
@ValueMapping(target = "Off", source = MappingConstants.ANY_REMAINING)
SimpleTrafficSignal toSimpleTrafficSignalWithRemaining(TrafficSignal source);
测试验证
@ParameterizedTest
@CsvSource({"Off,Off", "Go,On", "Stop,Off"})
void whenTrafficSignalIsMappedWithRemaining_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
SimpleTrafficSignal target = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithRemaining(source);
assertEquals(expected, target);
}
7.3 映射未定义值(ANY_UNMAPPED)
适用场景:处理所有未显式映射的值(忽略名称匹配)
映射配置:
@ValueMapping(target = "On", source = "Go")
@ValueMapping(target = "Off", source = MappingConstants.ANY_UNMAPPED)
SimpleTrafficSignal toSimpleTrafficSignalWithUnmapped(TrafficSignal source);
7.4 处理 NULL 值
映射配置:
@ValueMapping(target = "Off", source = MappingConstants.NULL)
@ValueMapping(target = "On", source = "Go")
@ValueMapping(target = MappingConstants.NULL, source = MappingConstants.ANY_UNMAPPED)
SimpleTrafficSignal toSimpleTrafficSignalWithNullHandling(TrafficSignal source);
测试验证
@ParameterizedTest
@CsvSource({",Off", "Go,On", "Stop,"})
void whenTrafficSignalIsMappedWithNull_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
SimpleTrafficSignal target = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithNullHandling(source);
assertEquals(expected, target);
}
7.5 抛出异常
映射配置:
@ValueMapping(target = "On", source = "Go")
@ValueMapping(target = MappingConstants.THROW_EXCEPTION, source = MappingConstants.ANY_UNMAPPED)
@ValueMapping(target = MappingConstants.THROW_EXCEPTION, source = MappingConstants.NULL)
SimpleTrafficSignal toSimpleTrafficSignalWithExceptionHandling(TrafficSignal source);
测试验证
@ParameterizedTest
@CsvSource({",", "Go,On", "Stop,"})
void whenTrafficSignalIsMappedWithException_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
if (source == TrafficSignal.Go) {
SimpleTrafficSignal target = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithExceptionHandling(source);
assertEquals(expected, target);
} else {
Exception exception = assertThrows(IllegalArgumentException.class, () -> {
TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithExceptionHandling(source);
});
assertEquals("Unexpected enum constant: " + source, exception.getMessage());
}
}
8. 总结
本文系统介绍了使用 MapStruct 处理枚举映射的核心技巧:
- 基础映射:通过
@ValueMapping
实现枚举间转换 - 类型转换:支持枚举与
String
/数值类型的双向映射 - 名称转换:通过
@EnumMapping
处理命名规范差异 - 异常处理:使用
ANY_REMAINING
/ANY_UNMAPPED
/THROW_EXCEPTION
等策略健壮处理未知值
最佳实践:
✅ 始终显式处理所有枚举值
✅ 为映射方法编写单元测试
✅ 根据业务场景选择合适的未知值处理策略
✅ 优先使用 MapStruct 注解而非手写转换逻辑
完整示例代码可在 GitHub 获取。