你好,我是看山。
日常开发中,我们经常会用到枚举,有时候会涉及枚举之间的映射、枚举与int或String之间的映射等。本文一起看下,MapStruct中如何实现。
一、将一个枚举映射到另一个枚举
(一)用例说明
- 在 REST API 中,将外部API状态码转换为我们应用内部的状态枚举;
- 与第三方库集成时,两个服务间枚举定义不同,通常需要处理枚举映射。
(二)使用MapStruct实现映射
这里我们会用到@ValueMapping注解,可以实现源常量值到目标常量值的映射。
我们看下实际应用。首先定义一个表示交通信号的枚举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);
}
看下生成的实现:
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会自动匹配。剩下两个枚举值根据我们的定义匹配上了。
这里需要注意的是,我们需要确保将源枚举的所有值都映射到目标枚举,如果没有完全匹配上,会走到default分支,抛出IllegalArgumentException异常。
二、将字符串映射到枚举
我们继续看下字符串与枚举之间的映射。有了前面的基础,我们这里直接上手,还是使用@ValueMapping注解,字符串的值都是小写,需要转换为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()也作为映射依据,换句话说,如果我们输入的字符串与枚举正好是一一对应的,那就可以不用定义映射关系了。
三、处理自定义名称转换
还有一种情况是,需要映射的枚举值有统一的约束,比如遵循不同的大小写、前缀或后缀等,比如,一个信号可以是Go、go、GO、Go_Value、Value_Go等。
(一)后缀
假如我们的目标枚举相较于源枚举有统一的后缀,比如:GO到GO_VALUE。
public enum TrafficSignalSuffixed {
OFF_VALUE,
STOP_VALUE,
GO_VALUE
}
此时,我们可以用到@EnumMapping注解,定义名称转换策略是后缀,然后定义后缀值:
@EnumMapping(nameTransformationStrategy = SUFFIX_TRANSFORMATION, configuration = "_VALUE")
TrafficSignalSuffixed applySuffix(TrafficSignal source);
@EnumMapping为枚举类型定义自定义映射,nameTransformationStrategy指定在映射之前应用于枚举常量名称的转换策略,并使用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应用的场景是枚举值完全符合指定策略,如果其中有某个值不符合,编译时会出现异常“The following constants from the source enum have no corresponding constant in the target enum and must be be mapped via adding additional mappings: xxx.”
(二)前缀
假如我们的目标枚举相较于源枚举有统一的前缀缀,比如: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;
}
(三)去除后缀
假如我们的目标枚举相较于源枚举缺少统一的后缀,比如:GO_VALUE到GO。
我们可以使用STRIP_SUFFIX_TRANSFORMATION指定去除后缀:
@EnumMapping(nameTransformationStrategy = STRIP_SUFFIX_TRANSFORMATION, configuration = "_VALUE")
TrafficSignal stripSuffix(TrafficSignalSuffixed source);
(四)去除前缀
假如我们的目标枚举相较于源枚举缺少统一的前缀,比如:VALUE_GO到GO。
我们可以使用STRIP_PREFIX_TRANSFORMATION指定去除前缀:
@EnumMapping(nameTransformationStrategy = STRIP_PREFIX_TRANSFORMATION, configuration = "VALUE_")
TrafficSignal stripPrefix(TrafficSignalPrefixed source);
(五)小写
假如我们的目标枚举是源枚举的小写,比如:GO变为go:
public enum TrafficSignalLowercase {
off,
stop,
go;
}
我们需要使用CASE_TRANSFORMATION策略,并定义策略是lower。
定义映射:
@EnumMapping(nameTransformationStrategy = CASE_TRANSFORMATION, configuration = "lower")
TrafficSignalLowercase applyLowercase(TrafficSignal source);
(六)大写
假如我们的目标枚举是源枚举的小写,比如:go变为GO:
还是使用CASE_TRANSFORMATION策略,并定义策略是upper。
@EnumMapping(nameTransformationStrategy = CASE_TRANSFORMATION, configuration = "upper")
TrafficSignal applyUppercase(TrafficSignalLowercase source);
(七)首字母大写
我们还可以指定首字母大写的映射,例如,go变为Go。
定义下目标枚举
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将枚举值映射到字符串,其实是和从字符串转枚举相似的配置逻辑。
(二)将枚举映射到整数或其他数字类型
因为数字类型存在多个构造函数,直接映射到整数可能会导致歧义。可以定义一个具有整数属性的类来解决这个问题。
定义一个包装类:
public class TrafficSignalNumber {
private Integer number;
}
使用默认方法将枚举映射到整数:
@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;
}