1. 介绍

本文将介绍如何使用 MapStruct 实现枚举类型之间的映射,包括:

  • 枚举到其他枚举的映射
  • 枚举与基本数据类型(如 intString)的双向转换

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 处理枚举映射的核心技巧:

  1. 基础映射:通过 @ValueMapping 实现枚举间转换
  2. 类型转换:支持枚举与 String/数值类型的双向映射
  3. 名称转换:通过 @EnumMapping 处理命名规范差异
  4. 异常处理:使用 ANY_REMAINING/ANY_UNMAPPED/THROW_EXCEPTION 等策略健壮处理未知值

最佳实践: ✅ 始终显式处理所有枚举值
✅ 为映射方法编写单元测试
✅ 根据业务场景选择合适的未知值处理策略
✅ 优先使用 MapStruct 注解而非手写转换逻辑

完整示例代码可在 GitHub 获取。


原始标题:Mapping Enum With MapStruct