MapStruct教程-枚舉的五種用法
你好,我是看山。
日常開發(fā)中,我們經(jīng)常會用到枚舉,有時候會涉及枚舉之間的映射、枚舉與int或String之間的映射等。本文一起看下,MapStruct中如何實現(xiàn)。
一、將一個枚舉映射到另一個枚舉
(一)用例說明
- 在 REST API 中,將外部API狀態(tài)碼轉(zhuǎn)換為我們應用內(nèi)部的狀態(tài)枚舉;
- 與第三方庫集成時,兩個服務間枚舉定義不同,通常需要處理枚舉映射。
(二)使用MapStruct實現(xiàn)映射
這里我們會用到@ValueMapping注解,可以實現(xiàn)源常量值到目標常量值的映射。
我們看下實際應用。首先定義一個表示交通信號的枚舉TrafficSignal:
public enum TrafficSignal {
OFF,
STOP,
GO;
}
在定義一個表示道路標記的源枚舉RoadSign:
public enum RoadSign {
OFF,
HALT,
MOVE;
}
接下來,我們定義一個映射:
@Mapper
public interface TrafficSignalMapper {
TrafficSignalMapper INSTANCE = Mappers.getMapper(TrafficSignalMapper.class);
@ValueMapping(target = "GO", source = "MOVE")
@ValueMapping(target = "STOP", source = "HALT")
TrafficSignal toTrafficSignal(RoadSign source);
}
看下生成的實現(xiàn):
public class TrafficSignalMapperImpl implements TrafficSignalMapper {
@Override
public TrafficSignal toTrafficSignal(RoadSign source) {
if ( source == null ) {
return null;
}
TrafficSignal trafficSignal;
switch ( source ) {
case MOVE: trafficSignal = TrafficSignal.GO;
break;
case HALT: trafficSignal = TrafficSignal.STOP;
break;
case OFF: trafficSignal = TrafficSignal.OFF;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + source );
}
return trafficSignal;
}
}
可以看到,因為OFF是名字相同,雖然沒有定義映射關系,MapStruct會自動匹配。剩下兩個枚舉值根據(jù)我們的定義匹配上了。
這里需要注意的是,我們需要確保將源枚舉的所有值都映射到目標枚舉,如果沒有完全匹配上,會走到default分支,拋出IllegalArgumentException異常。
二、將字符串映射到枚舉
我們繼續(xù)看下字符串與枚舉之間的映射。有了前面的基礎,我們這里直接上手,還是使用@ValueMapping注解,字符串的值都是小寫,需要轉(zhuǎn)換為TrafficSignal枚舉:
@ValueMapping(target = "OFF", source = "off")
@ValueMapping(target = "GO", source = "move")
@ValueMapping(target = "STOP", source = "halt")
TrafficSignal stringToTrafficSignal(String source);
我們看下生成的代碼:
@Override
public TrafficSignal stringToTrafficSignal(String source) {
if ( source == null ) {
return null;
}
TrafficSignal trafficSignal;
switch ( source ) {
case "off": trafficSignal = TrafficSignal.OFF;
break;
case "move": trafficSignal = TrafficSignal.GO;
break;
case "halt": trafficSignal = TrafficSignal.STOP;
break;
case "OFF": trafficSignal = TrafficSignal.OFF;
break;
case "STOP": trafficSignal = TrafficSignal.STOP;
break;
case "GO": trafficSignal = TrafficSignal.GO;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + source );
}
return trafficSignal;
}
可以看到,除了我們定義的三個映射,MapStruct還會自動將枚舉的name()也作為映射依據(jù),換句話說,如果我們輸入的字符串與枚舉正好是一一對應的,那就可以不用定義映射關系了。
三、處理自定義名稱轉(zhuǎn)換
還有一種情況是,需要映射的枚舉值有統(tǒng)一的約束,比如遵循不同的大小寫、前綴或后綴等,比如,一個信號可以是Go、go、GO、Go_Value、Value_Go等。
(一)后綴
假如我們的目標枚舉相較于源枚舉有統(tǒng)一的后綴,比如:GO到GO_VALUE。
public enum TrafficSignalSuffixed {
OFF_VALUE,
STOP_VALUE,
GO_VALUE
}
此時,我們可以用到@EnumMapping注解,定義名稱轉(zhuǎn)換策略是后綴,然后定義后綴值:
@EnumMapping(nameTransformationStrategy = SUFFIX_TRANSFORMATION, configuration = "_VALUE")
TrafficSignalSuffixed applySuffix(TrafficSignal source);
@EnumMapping為枚舉類型定義自定義映射,nameTransformationStrategy指定在映射之前應用于枚舉常量名稱的轉(zhuǎn)換策略,并使用configuration定義控制值。
生成結果是:
@Override
public TrafficSignalSuffixed applySuffix(TrafficSignal source) {
if ( source == null ) {
return null;
}
TrafficSignalSuffixed trafficSignalSuffixed;
switch ( source ) {
case OFF: trafficSignalSuffixed = TrafficSignalSuffixed.OFF_VALUE;
break;
case STOP: trafficSignalSuffixed = TrafficSignalSuffixed.STOP_VALUE;
break;
case GO: trafficSignalSuffixed = TrafficSignalSuffixed.GO_VALUE;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + source );
}
return trafficSignalSuffixed;
}
需要注意,@EnumMapping應用的場景是枚舉值完全符合指定策略,如果其中有某個值不符合,編譯時會出現(xiàn)異?!癟he following constants from the source enum have no corresponding constant in the target enum and must be be mapped via adding additional mappings: xxx.”
(二)前綴
假如我們的目標枚舉相較于源枚舉有統(tǒng)一的前綴綴,比如:GO到VALUE_GO。
public enum TrafficSignalPrefixed {
VALUE_OFF,
VALUE_STOP,
VALUE_GO;
}
定義映射:
@EnumMapping(nameTransformationStrategy = PREFIX_TRANSFORMATION, configuration = "VALUE_")
TrafficSignalPrefixed applyPrefix(TrafficSignal source);
PREFIX_TRANSFORMATION是告訴MapStruct,需要在源枚舉增加前綴VALUE_。
生成的代碼是:
public TrafficSignalPrefixed applyPrefix(TrafficSignal source) {
if ( source == null ) {
return null;
}
TrafficSignalPrefixed trafficSignalPrefixed;
switch ( source ) {
case OFF: trafficSignalPrefixed = TrafficSignalPrefixed.VALUE_OFF;
break;
case STOP: trafficSignalPrefixed = TrafficSignalPrefixed.VALUE_STOP;
break;
case GO: trafficSignalPrefixed = TrafficSignalPrefixed.VALUE_GO;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + source );
}
return trafficSignalPrefixed;
}
(三)去除后綴
假如我們的目標枚舉相較于源枚舉缺少統(tǒng)一的后綴,比如:GO_VALUE到GO。
我們可以使用STRIP_SUFFIX_TRANSFORMATION指定去除后綴:
@EnumMapping(nameTransformationStrategy = STRIP_SUFFIX_TRANSFORMATION, configuration = "_VALUE")
TrafficSignal stripSuffix(TrafficSignalSuffixed source);
(四)去除前綴
假如我們的目標枚舉相較于源枚舉缺少統(tǒng)一的前綴,比如:VALUE_GO到GO。
我們可以使用STRIP_PREFIX_TRANSFORMATION指定去除前綴:
@EnumMapping(nameTransformationStrategy = STRIP_PREFIX_TRANSFORMATION, configuration = "VALUE_")
TrafficSignal stripPrefix(TrafficSignalPrefixed source);
(五)小寫
假如我們的目標枚舉是源枚舉的小寫,比如:GO變?yōu)間o:
public enum TrafficSignalLowercase {
off,
stop,
go;
}
我們需要使用CASE_TRANSFORMATION策略,并定義策略是lower。
定義映射:
@EnumMapping(nameTransformationStrategy = CASE_TRANSFORMATION, configuration = "lower")
TrafficSignalLowercase applyLowercase(TrafficSignal source);
(六)大寫
假如我們的目標枚舉是源枚舉的小寫,比如:go變?yōu)镚O:
還是使用CASE_TRANSFORMATION策略,并定義策略是upper。
@EnumMapping(nameTransformationStrategy = CASE_TRANSFORMATION, configuration = "upper")
TrafficSignal applyUppercase(TrafficSignalLowercase source);
(七)首字母大寫
我們還可以指定首字母大寫的映射,例如,go變?yōu)镚o。
定義下目標枚舉
public enum TrafficSignalCapital {
Off,
Stop,
Go;
}
還是使用CASE_TRANSFORMATION策略,并定義策略是capital。
@EnumMapping(nameTransformationStrategy = CASE_TRANSFORMATION, configuration = "capital")
TrafficSignalCapital lowercaseToCapital(TrafficSignalLowercase source);
四、枚舉映射的其他用例
還有些場景中,我們需要將枚舉映射到其他類型,接下來,一起看看如何處理。
(一)將枚舉映射到字符串
定義映射:
@ValueMapping(target = "off", source = "OFF")
@ValueMapping(target = "go", source = "GO")
@ValueMapping(target = "stop", source = "STOP")
String trafficSignalToString(TrafficSignal source);
我們使用@ValueMapping將枚舉值映射到字符串,其實是和從字符串轉(zhuǎn)枚舉相似的配置邏輯。
(二)將枚舉映射到整數(shù)或其他數(shù)字類型
因為數(shù)字類型存在多個構造函數(shù),直接映射到整數(shù)可能會導致歧義??梢远x一個具有整數(shù)屬性的類來解決這個問題。
定義一個包裝類:
public class TrafficSignalNumber {
private Integer number;
}
使用默認方法將枚舉映射到整數(shù):
@Mapping(target = "number", source = ".")
TrafficSignalNumber trafficSignalToTrafficSignalNumber(TrafficSignal source);
生成的代碼是:
public TrafficSignalNumber trafficSignalToTrafficSignalNumber(TrafficSignal source) {
if ( source == null ) {
return null;
}
TrafficSignalNumber trafficSignalNumber = new TrafficSignalNumber();
if ( source != null ) {
trafficSignalNumber.setNumber( source.ordinal() );
}
return trafficSignalNumber;
}
五、處理未知枚舉值
前面提到過,在處理枚舉值值,當有未映射的枚舉值時,MapStruct會拋出異常。
不過,很多時候,當映射失敗的時候,我們需要有不同的操作,比如:設置默認值、設置空值、拋出異常等。
(一)未映射拋出異常
拋出異常是默認行為,前面的示例中都是屬于這種類型。
(二)映射剩余屬性
比如,我們有一個簡單的交通信號枚舉:
public enum SimpleTrafficSignal {
OFF,
ON;
}
需要將toSimpleTrafficSignal映射到SimpleTrafficSignal,但是MapStruct要求所有枚舉值都需要映射,不能遺漏,所以我們可以這樣寫:
@ValueMapping(target = "OFF", source = "OFF")
@ValueMapping(target = "OFF", source = "STOP")
@ValueMapping(target = "ON", source = "GO")
SimpleTrafficSignal toSimpleTrafficSignal(TrafficSignal source);
我們顯式地將STOP和OFF都映射到OFF,但是如果值特別多的時候,這樣寫就顯得很傻,我們可以使用ANY_REMAINING配置:
@ValueMapping(target = "ON", source = "GO")
@ValueMapping(target = "OFF", source = ANY_REMAINING)
SimpleTrafficSignal toSimpleTrafficSignalWithRemaining(TrafficSignal source);
生成的代碼是:
public SimpleTrafficSignal toSimpleTrafficSignalWithRemaining(TrafficSignal source) {
if ( source == null ) {
return null;
}
SimpleTrafficSignal simpleTrafficSignal;
switch ( source ) {
case GO: simpleTrafficSignal = SimpleTrafficSignal.ON;
break;
case OFF: simpleTrafficSignal = SimpleTrafficSignal.OFF;
break;
default: simpleTrafficSignal = SimpleTrafficSignal.OFF;
}
return simpleTrafficSignal;
}
也就是,除了GO明確映射外,其他的都映射為OFF。
(三)映射未映射的屬性
我們可以所有未映射到值全部映射為指定的枚舉,比如,所有沒有配置的都映射為OFF,我們可以使用ANY_UNMAPPED配置:
@ValueMapping(target = "ON", source = "GO")
@ValueMapping(target = "OFF", source = ANY_UNMAPPED)
SimpleTrafficSignal toSimpleTrafficSignalWithUnmapped(TrafficSignal source);
生成的代碼是:
@Override
public SimpleTrafficSignal toSimpleTrafficSignalWithUnmapped(TrafficSignal source) {
if ( source == null ) {
return null;
}
SimpleTrafficSignal simpleTrafficSignal;
switch ( source ) {
case GO: simpleTrafficSignal = SimpleTrafficSignal.ON;
break;
default: simpleTrafficSignal = SimpleTrafficSignal.OFF;
}
return simpleTrafficSignal;
}
(四)處理空值
MapStruct可以使用NULL關鍵字處理空的源和空的目標。
假設我們需要將空輸入映射到OFF,將GO映射到ON,將任何其他未映射的值映射到空。
我們可以這樣定義映射:
@ValueMapping(target = "OFF", source = NULL)
@ValueMapping(target = "ON", source = "GO")
@ValueMapping(target = NULL, source = MappingConstants.ANY_UNMAPPED)
SimpleTrafficSignal toSimpleTrafficSignalWithNullHandling(TrafficSignal source);
生成代碼是:
public SimpleTrafficSignal toSimpleTrafficSignalWithNullHandling(TrafficSignal source) {
if ( source == null ) {
return SimpleTrafficSignal.OFF;
}
SimpleTrafficSignal simpleTrafficSignal;
switch ( source ) {
case GO: simpleTrafficSignal = SimpleTrafficSignal.ON;
break;
default: simpleTrafficSignal = null;
}
return simpleTrafficSignal;
}
(五)指定值拋出異常
還有一種場景,就是為空或者未映射時,拋出異常,我們可以使用THROW_EXCEPTION策略:
定義映射:
@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);
生成的代碼是:
@Override
public SimpleTrafficSignal toSimpleTrafficSignalWithExceptionHandling(TrafficSignal source) {
if ( source == null ) {
throw new IllegalArgumentException( "Unexpected enum constant: " + source );
}
SimpleTrafficSignal simpleTrafficSignal;
switch ( source ) {
case GO: simpleTrafficSignal = SimpleTrafficSignal.ON;
break;
default: throw new IllegalArgumentException( "Unexpected enum constant: " + source );
}
return simpleTrafficSignal;
}