使用 MapStruct 映射枚举

在 REST API 响应映射中,MapStruct 将外部 API 状态代码转换为应用程序的内部状态枚举。
对于微服务中的数据转换,MapStruct 通过映射相似的枚举来促进服务之间的平滑数据交换。
与第三方库的集成通常涉及处理第三方枚举。 MapStruct 通过将它们转换为我们应用程序的枚举来简化这一过程。

将以下依赖项添加到 Maven pom.xml中:

<dependency>
    <groupId>org.mapstruct</groupId>
    <artifactId>mapstruct</artifactId>
    <version>1.6.0.Beta1</version> 
</dependency>

使用 MapStruct 实现映射
要配置源常量值到目标常量值的映射,我们使用@ValueMapping MapStruct 注释。它根据名称进行映射。但是,我们也可以将源枚举中的常量映射到目标枚举类型中具有不同名称的常量。例如,我们可以将源枚举“ Go ”映射到目标枚举“ Move ”。

还可以将源枚举中的多个常量映射到目标类型中的相同常量。

TrafficSignal枚举代表交通信号。我们与之交互的外部服务使用RoadSign枚举。映射器会将枚举相互转换。

让我们定义交通信号枚举:

public enum TrafficSignal {
    Off, Stop, Go
}

让我们定义路标枚举:

public enum RoadSign {
    Off, Halt, Move
}

让我们实现@Mapper:

@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);
}

@Mapper定义了一个名为TrafficSignalMapper的 MapStruct 映射器,用于将枚举转换为TrafficSignal。它的方法代表一个映射操作。

接口中的 @ValueMapping 注释指定枚举值之间的显式映射。例如,  @ValueMapping(target = “Go”, source = “Move”)将Move枚举映射到TrafficSignal中的Go枚举,等等。

我们需要确保将所有枚举值从源映射到目标以实现完整覆盖并防止意外行为。

这是对其的测试:

@Test
void whenRoadSignIsMapped_thenGetTrafficSignal() {
    RoadSign source = RoadSign.Move;
    TrafficSignal target = TrafficSignalMapper.INSTANCE.toTrafficSignal(source);
    assertEquals(TrafficSignal.Go, target);
}

它验证RoadSign的映射。移至交通信号。去。

我们必须通过单元测试彻底测试映射方法,以确保行为准确并检测潜在问题。

 将字符串映射到枚举
让我们将文本文字值转换为枚举值。

1.了解用例
我们的应用程序将用户输入收集为字符串。我们将这些字符串映射到枚举值来表示不同的命令或选项。例如,我们将“add”映射到Operation.ADD,将“subtract”映射到Operation.SUBTRACT,等等。

我们在应用程序配置中将设置指定为字符串。我们将这些字符串映射到枚举值以确保类型安全的配置。例如,我们将“EXEC”映射到Mode.EXEC,“TEST”映射到Mode.TEST,等等。

我们将外部 API 字符串映射到应用程序中的枚举值。例如,我们将“active”映射到Status.ACTIVE,将“inactive”映射到Status.INACTIVE,等等。

2.使用 MapStruct 实现映射
让我们使用@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);
}

它验证“移动”映射到TrafficSignal.Go。

处理自定义名称转换
枚举名称可能仅因命名约定而有所不同。它可能遵循不同的大小写、前缀或后缀约定。例如,信号可以是Go、go、GO、Go_Value、Value_Go。

1.将后缀应用于源枚举
我们对源枚举应用后缀以获取目标枚举。例如,Go变为Go_Value:

public enum TrafficSignalSuffixed { Off_Value, Stop_Value, Go_Value }

让我们定义映射:

@EnumMapping(nameTransformationStrategy = MappingConstants.SUFFIX_TRANSFORMATION, configuration = "_Value")
TrafficSignalSuffixed applySuffix(TrafficSignal source);

@EnumMapping定义枚举类型的自定义映射。 nameTransformationStrategy指定映射之前应用于枚举常量名称的转换策略。 我们在配置中传递适当的控制值。

这是检查后缀的测试:

@ParameterizedTest
@CsvSource({"Off,Off_Value", "Go,Go_Value"})
void whenTrafficSignalIsMappedWithSuffix_thenGetTrafficSignalSuffixed(TrafficSignal source, TrafficSignalSuffixed expected) {
    TrafficSignalSuffixed result = TrafficSignalMapper.INSTANCE.applySuffix(source);
    assertEquals(expected, result);
}

2.将前缀应用于源枚举
我们还可以对源枚举应用前缀来获取目标枚举。例如,Go变为 Value_Go :

public enum TrafficSignalPrefixed { Value_Off, Value_Stop, Value_Go }

让我们定义映射:

@EnumMapping(nameTransformationStrategy = MappingConstants.PREFIX_TRANSFORMATION, configuration = "Value_")
TrafficSignalPrefixed applyPrefix(TrafficSignal source);

PREFIX_TRANSFORMATION告诉 MapStruct 将前缀“ Value_ ”应用于源枚举。

让我们检查一下前缀映射:

@ParameterizedTest
@CsvSource({"Off,Value_Off", "Go,Value_Go"})
void whenTrafficSignalIsMappedWithPrefix_thenGetTrafficSignalPrefixed(TrafficSignal source, TrafficSignalPrefixed expected) {
    TrafficSignalPrefixed result = TrafficSignalMapper.INSTANCE.applyPrefix(source);
    assertEquals(expected, result);
}

3.从源枚举中删除后缀
我们从源枚举中删除后缀以获得目标枚举。例如,Go_Value变为Go。

让我们定义映射:

@EnumMapping(nameTransformationStrategy = MappingConstants.STRIP_SUFFIX_TRANSFORMATION, configuration = "_Value")
TrafficSignal stripSuffix(TrafficSignalSuffixed source);

STRIP_SUFFIX_TRANSFORMATION告诉 MapStruct从源枚举中删除后缀“ _Value ”。

这是检查剥离后缀的测试:

@ParameterizedTest
@CsvSource({"Off_Value,Off", "Go_Value,Go"})
void whenTrafficSignalSuffixedMappedWithStripped_thenGetTrafficSignal(TrafficSignalSuffixed source, TrafficSignal expected) {
    TrafficSignal result = TrafficSignalMapper.INSTANCE.stripSuffix(source);
    assertEquals(expected, result);
}

4.从源枚举中剥离前缀
我们从源枚举中删除前缀以获取目标枚举。例如,Value_Go变为Go 。

让我们定义映射:

@EnumMapping(nameTransformationStrategy = MappingConstants.STRIP_PREFIX_TRANSFORMATION, configuration = "Value_")
TrafficSignal stripPrefix(TrafficSignalPrefixed source);

STRIP_PREFIX_TRANSFORMATION告诉 MapStruct从源枚举中删除前缀“ Value_ ”。

这是检查剥离前缀的测试:

@ParameterizedTest
@CsvSource({"Value_Off,Off", "Value_Stop,Stop"})
void whenTrafficSignalPrefixedMappedWithStripped_thenGetTrafficSignal(TrafficSignalPrefixed source, TrafficSignal expected) {
    TrafficSignal result = TrafficSignalMapper.INSTANCE.stripPrefix(source);
    assertEquals(expected, result);
}

5.将小写应用于源枚举
我们将小写字母应用于源枚举来获取目标枚举。例如,Go变为go:

public enum TrafficSignalLowercase { off, stop, go }

让我们定义映射:

@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "lower")
TrafficSignalLowercase applyLowercase(TrafficSignal source);

CASE_TRANSFORMATION和较低的配置告诉 MapStruct 将小写应用于源枚举。

这是检查小写映射的测试方法:

@ParameterizedTest
@CsvSource({"Off,off", "Go,go"})
void whenTrafficSignalMappedWithLower_thenGetTrafficSignalLowercase(TrafficSignal source, TrafficSignalLowercase expected) {
    TrafficSignalLowercase result = TrafficSignalMapper.INSTANCE.applyLowercase(source);
    assertEquals(expected, result);
}

6.将大写应用于源枚举
我们将大写字母应用于源枚举以获取目标枚举。例如,Mon变为MON:

public enum <em>TrafficSignalUppercase</em> { OFF, STOP, GO }

让我们定义映射:

@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "upper")
TrafficSignalUppercase applyUppercase(TrafficSignal source);

CASE_TRANSFORMATION和 upper 配置告诉 MapStruct 将大写应用于源枚举。

这是验证大写映射的测试:

@ParameterizedTest
@CsvSource({"Off,OFF", "Go,GO"})
void whenTrafficSignalMappedWithUpper_thenGetTrafficSignalUppercase(TrafficSignal source, TrafficSignalUppercase expected) {
    TrafficSignalUppercase result = TrafficSignalMapper.INSTANCE.applyUppercase(source);
    assertEquals(expected, result);
}

7.将大写字母应用于源枚举
我们将标题大小写应用于源枚举以获取目标枚举。例如,go变成Go:

@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "captial")
TrafficSignal lowercaseToCapital(TrafficSignalLowercase source);

CASE_TRANSFORMATION和大写配置告诉 MapStruct 将源枚举大写。

这是检查大写字母的测试:

@ParameterizedTest
@CsvSource({"OFF_VALUE,Off_Value", "GO_VALUE,Go_Value"})
void whenTrafficSignalUnderscoreMappedWithCapital_thenGetStringCapital(TrafficSignalUnderscore source, String expected) {
    String result = TrafficSignalMapper.INSTANCE.underscoreToCapital(source);
    assertEquals(expected, result);
}

枚举映射的其他用例
当我们将枚举映射回其他类型时,可能会出现一些情况。让我们在本节中看看它们。

1.将枚举映射到字符串
让我们定义映射:

@ValueMapping(target = "Off", source = "Off")
@ValueMapping(target =
"Go", source = "Go")
@ValueMapping(target =
"Stop", source = "Stop")
String trafficSignalToString(TrafficSignal source);

@ValueMapping将枚举值映射到字符串。例如,我们将Go枚举映射到“Go”字符串值,等等。

这是检查字符串映射的测试:

@Test
void whenTrafficSignalIsMapped_thenGetString() {
    TrafficSignal source = TrafficSignal.Go;
    String targetTrafficSignalStr = TrafficSignalMapper.INSTANCE.trafficSignalToString(source);
    assertEquals("Go", targetTrafficSignalStr);
}

它验证映射是否将枚举TrafficSignal.Go映射到字符串文字“Go”。

2.将枚举映射到整数或其他数字类型
由于多个构造函数,直接映射到整数可能会导致歧义。我们添加一个默认映射器方法,将枚举转换为整数。另外,我们还可以定义一个具有整数属性的类来解决这个问题。

让我们定义一个包装类:

public class TrafficSignalNumber
{
    private Integer number;
    // getters and setters
}

让我们使用默认方法将枚举映射到整数:

@Mapping(target = "number", source = ".")
TrafficSignalNumber trafficSignalToTrafficSignalNumber(TrafficSignal source);

default Integer convertTrafficSignalToInteger(TrafficSignal source) {
    Integer result = null;
    switch (source) {
        case Off:
            result = 0;
            break;
        case Stop:
            result = 1;
            break;
        case Go:
            result = 2;
            break;
    }
    return result;
}

这是检查整数结果的测试:

@ParameterizedTest
@CsvSource({"Off,0", "Stop,1"})
void whenTrafficSignalIsMapped_thenGetInt(TrafficSignal source, int expected) {
    Integer targetTrafficSignalInt = TrafficSignalMapper.INSTANCE.convertTrafficSignalToInteger(source);
    TrafficSignalNumber targetTrafficSignalNumber = TrafficSignalMapper.INSTANCE.trafficSignalToTrafficSignalNumber(source);
    assertEquals(expected, targetTrafficSignalInt.intValue());
    assertEquals(expected, targetTrafficSignalNumber.getNumber().intValue());
}

处理未知的枚举值
我们需要通过设置默认值、处理空值或根据业务逻辑抛出异常来 处理不匹配的枚举值。

1. MapStruct 对任何未映射的属性引发异常
如果源枚举在目标类型中没有对应的枚举,MapStruct 会引发错误。此外,MapStruct 还可以将剩余或未映射的值映射到默认值。

我们有两个仅适用于源的选项:ANY_REMAINING和ANY_UNMAPPED。然而,我们一次只需要使用这些选项之一。

2.映射剩余属性
ANY_REMAINING选项将 任何剩余的同名源值映射到默认值。

让我们定义一个简单的交通信号:

public enum SimpleTrafficSignal { Off, On }

值得注意的是,它的值数量少于TrafficSignal。然而,MapStruct 需要我们映射所有枚举值。

让我们定义映射:

@ValueMapping(target = "Off", source = "Off")
@ValueMapping(target =
"On", source = "Go")
@ValueMapping(target =
"Off", source = "Stop")
SimpleTrafficSignal toSimpleTrafficSignal(TrafficSignal source);

我们明确映射到Off。如果有很多这样的值,映射它们会很不方便。我们可能会错过映射一些值。这就是ANY_REMAINING有帮助的地方。

让我们定义映射:

@ValueMapping(target = "On", source = "Go")
@ValueMapping(target =
"Off", source = MappingConstants.ANY_REMAINING)
SimpleTrafficSignal toSimpleTrafficSignalWithRemaining(TrafficSignal source);

在这里,我们将Go映射到On。然后使用MappingConstants.ANY_REMAINING,我们将任何剩余值映射到Off。现在这不是一个更干净的实现吗?

这是检查剩余映射的测试:

@ParameterizedTest
@CsvSource({"Off,Off", "Go,On", "Stop,Off"})
void whenTrafficSignalIsMappedWithRemaining_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
    SimpleTrafficSignal targetTrafficSignal = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithRemaining(source);
    assertEquals(expected, targetTrafficSignal);
}

它验证除值Go 之外的所有其他值是否都映射到Off 。

3.映射未映射的属性
我们可以指示 MapStruct 映射未映射的值(无论名称如何),而不是剩余的值。

让我们定义映射:

@ValueMapping(target = "On", source = "Go")
@ValueMapping(target =
"Off", source = MappingConstants.ANY_UNMAPPED)
SimpleTrafficSignal toSimpleTrafficSignalWithUnmapped(TrafficSignal source);

这是检查未映射映射的测试:

@ParameterizedTest
@CsvSource({"Off,Off", "Go,On", "Stop,Off"})
void whenTrafficSignalIsMappedWithUnmapped_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
    SimpleTrafficSignal target = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithUnmapped(source);
    assertEquals(expected, target);
}

它验证除值Go 之外的所有其他值是否都映射到Off 。

处理空值
MapStruct 可以使用NULL关键字处理空源和空目标。

假设我们需要将null输入映射到 Off, 转到On  ,并将任何其他未映射的值映射 到null。

让我们定义映射:

@ValueMapping(target = "Off", source = MappingConstants.NULL)
@ValueMapping(target =
"On", source = "Go")
@ValueMapping(target = MappingConstants.NULL, source = MappingConstants.ANY_UNMAPPED)
SimpleTrafficSignal toSimpleTrafficSignalWithNullHandling(TrafficSignal source);

我们使用MappingConstants.NULL将空值设置为目标。它还用于指示空输入。

这是检查空映射的测试:

@CsvSource({",Off", "Go,On", "Stop,"})
void whenTrafficSignalIsMappedWithNull_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
    SimpleTrafficSignal targetTrafficSignal = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithNullHandling(source);
    assertEquals(expected, targetTrafficSignal);
}

引发异常
让我们考虑一个场景,我们引发异常而不是将其映射到默认值或null。

让我们定义映射:

@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);

我们使用MappingConstants.THROW_EXCEPTION为任何未映射的输入引发异常。

这是检查抛出异常的测试:

@ParameterizedTest
@CsvSource({",", "Go,On", "Stop,"})
void whenTrafficSignalIsMappedWithException_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
    if (source == TrafficSignal.Go) {
        SimpleTrafficSignal targetTrafficSignal = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithExceptionHandling(source);
        assertEquals(expected, targetTrafficSignal);
    } else {
        Exception exception = assertThrows(IllegalArgumentException.class, () -> {
            TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithExceptionHandling(source);
        });
        assertEquals(
"Unexpected enum constant: " + source, exception.getMessage());
    }
}

它验证结果是否是Stop的异常,否则它是预期信号。