使用 MapStruct 映射枚舉
一、簡介
在本教程中,我們將學習如何使用 MapStruct 將一種枚舉類型對應到另一種枚舉類型,將枚舉對應到內建 Java 類型(例如int
和String
,反之亦然。
2. Maven
讓我們將以下依賴項新增到 Maven pom.xml
中:
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>1.6.0.Beta1</version>
</dependency>
MapStruct的最新穩定版本可從 Maven 中央儲存庫取得。
3. 將一個枚舉對應到另一個枚舉
在本節中,我們將學習執行各種映射。
3.1.了解用例
現在,讓我們來看看一些現實世界的場景。
在 REST API 回應映射中,MapStruct 將外部 API 狀態碼轉換為應用程式的內部狀態枚舉。
對於微服務中的資料轉換,MapStruct 透過繪製相似的枚舉來促進服務之間的平滑資料交換。
與第三方庫的整合通常涉及處理第三方枚舉。 MapStruct 透過將它們轉換為我們應用程式的枚舉來簡化這一過程。
3.2.使用 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
的映射。 Move
TrafficSignal
。 Go.
我們必須透過單元測試徹底測試映射方法,以確保行為準確並檢測潛在問題。
4. 將String
映射到枚舉
讓我們將文字文字值轉換為枚舉值。
4.1.了解用例
我們的應用程式將用戶輸入收集為字串。我們將這些字串映射到枚舉值來表示不同的命令或選項。例如,我們將“add”
對應到Operation.ADD, “subtract”
到Operation.SUBTRACT
,等等。
我們在應用程式配置中將設定指定為字串。我們將這些字串映射到枚舉值以確保類型安全的配置。例如,我們將“EXEC”
對應到Mode.EXEC, “TEST”
到Mode.TEST, etc
。
我們將外部 API 字串對應到應用程式中的枚舉值。例如,我們將“active”
映射到Status.ACTIVE, “inactive”
到Status.INACTIVE, etc
。
4.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);
}
它驗證“Move”
映射到TrafficSignal.Go
。
5. 處理自訂名稱轉換
枚舉名稱可能僅因命名約定而有所不同。它可能遵循不同的大小寫、前綴或後綴約定。例如,訊號可以是Go
、 go
、 GO
、 Go_Value
、 Value_Go
。
5.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
指定映射之前應用於枚舉常數名稱的轉換策略。我們在configuration
中傳遞適當的控制值。
這是檢查後綴的測試:
@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.將前綴應用於來源枚舉
我們也可以對來源枚舉應用前綴來取得目標枚舉。例如, 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);
}
5.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);
}
5.4.從源枚舉中剝離前綴
我們從來源枚舉中刪除前綴以獲取目標枚舉。例如Go
Value_
變成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.5.將小寫應用於來源枚舉
我們將小寫字母應用於來源枚舉來取得目標枚舉。例如, Go
變成go
:
public enum TrafficSignalLowercase { off, stop, go }
讓我們定義映射:
@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "lower")
TrafficSignalLowercase applyLowercase(TrafficSignal source);
CASE_TRANSFORMATION
和lower
配置告訴 MapStruct 將小寫應用於來源枚舉。
這是檢查小寫映射的測試方法:
@ParameterizedTest
@CsvSource({"Off,off", "Go,go"})
void whenTrafficSignalMappedWithLower_thenGetTrafficSignalLowercase(TrafficSignal source, TrafficSignalLowercase expected) {
TrafficSignalLowercase result = TrafficSignalMapper.INSTANCE.applyLowercase(source);
assertEquals(expected, result);
}
5.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);
}
5.7.將大寫字母應用於來源枚舉
我們將標題大小寫應用於來源枚舉以獲取目標枚舉。例如, go
變成Go
:
@EnumMapping(nameTransformationStrategy = MappingConstants.CASE_TRANSFORMATION, configuration = "captial")
TrafficSignal lowercaseToCapital(TrafficSignalLowercase source);
CASE_TRANSFORMATION
和capital
配置告訴 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);
}
6. 其他枚舉映射的用例
當我們將枚舉映射回其他類型時,可能會出現一些情況。讓我們在本節中看看它們。
6.1.將枚舉映射到String
讓我們定義映射:
@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”
。
6.2.將枚舉對應到Integer
或其他數字類型
由於多個構造函數,直接映射到整數可能會導致歧義。我們新增一個預設映射器方法,將枚舉轉換為整數。另外,我們也可以定義一個具有整數屬性的類別來解決這個問題。
讓我們定義一個包裝類別:
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());
}
7. 處理未知的枚舉值
我們需要透過設定預設值、處理空值或根據業務邏輯拋出異常來處理不匹配的枚舉值。
7.1. MapStruct 對任何未映射的屬性引發異常
如果來源枚舉在目標類型中沒有對應的枚舉,MapStruct 會引發錯誤。此外,MapStruct 還可以將剩餘或未映射的值對應到預設值。
我們有兩個僅適用於來源的選項: ANY_REMAINING
和ANY_UNMAPPED
。然而,我們一次只需要使用這些選項之一。
7.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
Go.
7.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
Go.
7.4.處理空值
MapStruct 可以使用NULL
關鍵字處理null
源和null
目標。
假設我們需要將null
輸入映射到Off
, Go
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
將null
值設定為目標。它也用於指示null
輸入。
這是檢查空映射的測試:
@CsvSource({",Off", "Go,On", "Stop,"})
void whenTrafficSignalIsMappedWithNull_thenGetTrafficSignal(TrafficSignal source, SimpleTrafficSignal expected) {
SimpleTrafficSignal targetTrafficSignal = TrafficSignalMapper.INSTANCE.toSimpleTrafficSignalWithNullHandling(source);
assertEquals(expected, targetTrafficSignal);
}
7.5。引發例外
讓我們考慮一個場景,我們引發異常而不是將其映射到預設值或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
的異常,否則它是預期訊號。
八、結論
在本文中,我們學習了使用 MapStruct @ValueMapping
在枚舉類型和其他資料類型(例如字串或整數)之間進行對應。無論是將一個枚舉對應到另一個枚舉還是優雅地處理未知的枚舉值, @ValueMapping
可以在映射任務中提供靈活性和優勢。透過堅持最佳實踐並處理空輸入和不匹配的值,我們提高了程式碼的清晰度和可維護性。
與往常一樣,範例的原始程式碼可在 GitHub 上取得。