1. 场景 在我们实际开发中,枚举类型应用十分广泛,可以避免在项目中定义大量的『魔法值』变量。但是,在web开发中,如何将枚举对象作为请求参数传进Controller,做到类型自动转换?直接使用 @RequestParam
和 @RequestBody
断然是不够的,这里就需要我们自定义 Converter
来实现类型转化了。
2. 需求 比如一个用户对象,里面的性别属性,我们定义一个枚举类型 Gender
。
GenderEnum.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Getter public enum GenderEnum { MALE(0 ), FEMALE(1 ); private Integer code; GenderEnum(int code) { this .code = code; } }
请求对象封装 QueryRequest
QueryRequest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Data public class QueryRequest { private GenderEnum gender; }
在Controller层直接使用如下方式,期望参数自动进行类型转换
EnumTestController.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Slf 4j@RestController @RequestMapping ("/enum" )public class EnumTestController { @GetMapping ("/get" ) public Dict testGet (QueryRequest request) { log.info("【get-request】= {}" , JSONUtil.toJsonStr(request)); return Dict.create().set("get-request" , request); } @PostMapping ("/post" ) public Dict testPost (@RequestBody QueryRequest request) { log.info("【post-request】= {}" , JSONUtil.toJsonStr(request)); return Dict.create().set("post-request" , request); } }
这么写的时候,gender只能接收到 MALE
、FEMALE
这样的参数,除此以外,均会报类型不匹配的错误信息,此时是无法处理 0
、1
这样的参数的。
需求:
接收到 MALE
、FEMALE
这样的参数,可以自动转为对应的枚举值; 接收到 0
、1
这样的参数,也可以自动转为对应的枚举值。 3. 解决 此时,本文的『主角』Converter 就可以隆重登场了。
注意,Converter
是 org.springframework.core.convert.converter.Converter
,别导错包了。
IntegerCodeToGenderEnumConverter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public class IntegerCodeToGenderEnumConverter implements Converter <Integer , GenderEnum > { private Map<Integer, GenderEnum> enumMap = Maps.newHashMap(); public IntegerCodeToGenderEnumConverter () { for (GenderEnum genderEnum : GenderEnum.values()) { enumMap.put(genderEnum.getCode(), genderEnum); } } @Override public GenderEnum convert (Integer source) { GenderEnum genderEnum = enumMap.get(source); if (ObjectUtil.isNull(genderEnum)) { throw new IllegalArgumentException("无法匹配对应的枚举类型" ); } return genderEnum; } }
其实这里已经可以实现类型转换了,但是引出另外一个问题,当我们的枚举类特别多的时候,我们就需要写很多个 自定义的Converter来满足类型转化。
所以我们不使用上面直接使用 Converter
的这种方法,我们引入 ConverterFactory
来解决这个问题。
3.1. 抽取公共枚举接口 以后的枚举类都需要实现这个接口
BaseEnum.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public interface BaseEnum { Integer getCode () ; }
3.2. 调整GenderEnum枚举类 GenderEnum.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Getter public enum GenderEnum implements BaseEnum { MALE(0 ), FEMALE(1 ); private Integer code; GenderEnum(int code) { this .code = code; } }
3.3. 创建通用 Integer -> Enum
的 Converter 类 IntegerToEnumConverter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class IntegerToEnumConverter <T extends BaseEnum > implements Converter <Integer , T > { private Map<Integer, T> enumMap = Maps.newHashMap(); public IntegerToEnumConverter (Class<T> enumType) { T[] enums = enumType.getEnumConstants(); for (T e : enums) { enumMap.put(e.getCode(), e); } } @Override public T convert (Integer source) { T t = enumMap.get(source); if (ObjectUtil.isNull(t)) { throw new IllegalArgumentException("无法匹配对应的枚举类型" ); } return t; } }
3.4. 创建对应的自定义 ConverterFactory 工厂类 IntegerCodeToEnumConverterFactory.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class IntegerCodeToEnumConverterFactory implements ConverterFactory <Integer , BaseEnum > { private static final Map<Class, Converter> CONVERTERS = Maps.newHashMap(); @Override public <T extends BaseEnum> Converter<Integer, T> getConverter (Class<T> targetType) { Converter<Integer, T> converter = CONVERTERS.get(targetType); if (converter == null ) { converter = new IntegerToEnumConverter<>(targetType); CONVERTERS.put(targetType, converter); } return converter; } }
3.5. 创建通用 String -> Enum
的 Converter 类和对应的 ConverterFactory
工厂类 因为 Post 请求可以对传入的 json 属性定义类型,但是 Get 请求后台接收到的参数都是 String 类型,因此需要在创建一个Converter类
StringToEnumConverter.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class StringToEnumConverter <T extends BaseEnum > implements Converter <String , T > { private Map<String, T> enumMap = Maps.newHashMap(); public StringToEnumConverter (Class<T> enumType) { T[] enums = enumType.getEnumConstants(); for (T e : enums) { enumMap.put(e.getCode().toString(), e); } } @Override public T convert (String source) { T t = enumMap.get(source); if (ObjectUtil.isNull(t)) { throw new IllegalArgumentException("无法匹配对应的枚举类型" ); } return t; } }
StringCodeToEnumConverterFactory.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 public class StringCodeToEnumConverterFactory implements ConverterFactory <String , BaseEnum > { private static final Map<Class, Converter> CONVERTERS = Maps.newHashMap(); @Override public <T extends BaseEnum> Converter<String, T> getConverter (Class<T> targetType) { Converter<String, T> converter = CONVERTERS.get(targetType); if (converter == null ) { converter = new StringToEnumConverter<>(targetType); CONVERTERS.put(targetType, converter); } return converter; } }
3.6. 将转化器工厂添加进 Spring Boot 配置 WebMvcConfig.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addFormatters (FormatterRegistry registry) { registry.addConverterFactory(new IntegerCodeToEnumConverterFactory()); registry.addConverterFactory(new StringCodeToEnumConverterFactory()); } }
这样我们就可以优雅的实现枚举对象参数的自动转换了,并且支持多个类型转换。其中的类型转换失败异常,可以使用全局异常拦截来处理,不会的小伙伴可以参考以前的文章。https://xkcoding.com/2018/08/20/spring-boot-global-exception-handler.html
4. 测试 4.1. Get 请求
4.2. Post 请求
5. 代码地址 https://github.com/xkcoding/owntest/tree/master/spring-boot-test