使用 MapStruct 對映列舉

banq發表於2024-04-13

在 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 = <font>"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 = <font>"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 = <font>"_Value")
TrafficSignalSuffixed applySuffix(TrafficSignal source);

@EnumMapping定義列舉型別的自定義對映。 nameTransformationStrategy指定對映之前應用於列舉常量名稱的轉換策略。 我們在配置中傳遞適當的控制值。

這是檢查字尾的測試:

@ParameterizedTest
@CsvSource({<font>"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 = <font>"Value_")
TrafficSignalPrefixed applyPrefix(TrafficSignal source);

PREFIX_TRANSFORMATION告訴 MapStruct 將字首“ Value_ ”應用於源列舉。

讓我們檢查一下字首對映:

@ParameterizedTest
@CsvSource({<font>"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 = <font>"_Value")
TrafficSignal stripSuffix(TrafficSignalSuffixed source);

STRIP_SUFFIX_TRANSFORMATION告訴 MapStruct從源列舉中刪除字尾“ _Value ”。

這是檢查剝離字尾的測試:

@ParameterizedTest
@CsvSource({<font>"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 = <font>"Value_")
TrafficSignal stripPrefix(TrafficSignalPrefixed source);

STRIP_PREFIX_TRANSFORMATION告訴 MapStruct從源列舉中刪除字首“ Value_ ”。

這是檢查剝離字首的測試:

@ParameterizedTest
@CsvSource({<font>"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 = <font>"lower")
TrafficSignalLowercase applyLowercase(TrafficSignal source);

CASE_TRANSFORMATION和較低的配置告訴 MapStruct 將小寫應用於源列舉。

這是檢查小寫對映的測試方法:

@ParameterizedTest
@CsvSource({<font>"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 = <font>"upper")
TrafficSignalUppercase applyUppercase(TrafficSignal source);

CASE_TRANSFORMATION和 upper 配置告訴 MapStruct 將大寫應用於源列舉。

這是驗證大寫對映的測試:

@ParameterizedTest
@CsvSource({<font>"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 = <font>"captial")
TrafficSignal lowercaseToCapital(TrafficSignalLowercase source);

CASE_TRANSFORMATION和大寫配置告訴 MapStruct 將源列舉大寫。

這是檢查大寫字母的測試:

@ParameterizedTest
@CsvSource({<font>"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 = <font>"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(<font>"Go", targetTrafficSignalStr);
}

它驗證對映是否將列舉TrafficSignal.Go對映到字串文字“Go”。

2.將列舉對映到整數或其他數字型別
由於多個建構函式,直接對映到整數可能會導致歧義。我們新增一個預設對映器方法,將列舉轉換為整數。另外,我們還可以定義一個具有整數屬性的類來解決這個問題。

讓我們定義一個包裝類:

public class TrafficSignalNumber
{
    private Integer number;
    <font>// getters and setters<i>
}

讓我們使用預設方法將列舉對映到整數:

@Mapping(target = <font>"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({<font>"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 = <font>"Off", source = "Off")
@ValueMapping(target =
"On", source = "Go")
@ValueMapping(target =
"Off", source = "Stop")
SimpleTrafficSignal toSimpleTrafficSignal(TrafficSignal source);

我們明確對映到Off。如果有很多這樣的值,對映它們會很不方便。我們可能會錯過對映一些值。這就是ANY_REMAINING有幫助的地方。

讓我們定義對映:

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

在這裡,我們將Go對映到On。然後使用MappingConstants.ANY_REMAINING,我們將任何剩餘值對映到Off。現在這不是一個更乾淨的實現嗎?

這是檢查剩餘對映的測試:

@ParameterizedTest
@CsvSource({<font>"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 = <font>"On", source = "Go")
@ValueMapping(target =
"Off", source = MappingConstants.ANY_UNMAPPED)
SimpleTrafficSignal toSimpleTrafficSignalWithUnmapped(TrafficSignal source);

這是檢查未對映對映的測試:

@ParameterizedTest
@CsvSource({<font>"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 = <font>"Off", source = MappingConstants.NULL)
@ValueMapping(target =
"On", source = "Go")
@ValueMapping(target = MappingConstants.NULL, source = MappingConstants.ANY_UNMAPPED)
SimpleTrafficSignal toSimpleTrafficSignalWithNullHandling(TrafficSignal source);

我們使用MappingConstants.NULL將空值設定為目標。它還用於指示空輸入。

這是檢查空對映的測試:

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

引發異常
讓我們考慮一個場景,我們引發異常而不是將其對映到預設值或null。

讓我們定義對映:

@ValueMapping(target = <font>"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({<font>",", "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的異常,否則它是預期訊號。

相關文章