Spring对应枚举传参/返回值默认是用字面量实现的(实际情况更复杂),而《阿里巴巴Java开发手册》规定接口返回值不可以使用枚举类型(包括含枚举类型的POJO对象),为此,本文探究了Spring内部对枚举参数的传递和处理机制,并提供了一套自定义方案。
一 目标与思路
0 起因
《阿里巴巴Java开发手册》将接口中枚举的使用分为两类,即 接口参数和接口返回值,并规定:
接口参数可以使用枚举类型,但接口返回值不可以使用枚举类型(包括含枚举类型的POJO对象)。
知乎有相关讨论和作者亲答,详情可见:Java枚举什么不好,《阿里巴巴JAVA开发手册》对于枚举规定的考量是什么?
现摘录一部分作者回答如下:
由于升级原因,导致双方的枚举类不尽相同,在接口解析,类反序列化时出现异常。
Java中出现的任何元素,在Gosling的角度都会有背后的思考和逻辑(尽管并非绝对完美,但Java的顶层抽象已经是天才级了),比如:接口、抽象类、注解、和本文提到的枚举。枚举有好处,类型安全,清晰直接,还可以使用等号来判断,也可以用在switch中。它的劣势也是明显的,就是不要扩展
。可是为什么在返回值和参数进行了区分呢,如果不兼容,那么两个都有问题,怎么允许参数可以有枚举。当时的考虑,如果参数也不能用,那么枚举几乎无用武之地了。参数输出,毕竟是本地决定的,你本地有的,传送过去,向前兼容是不会有问题的。但如果是接口返回,就比较恶心了,因为解析回来的这个枚举值,可能本地还没有,这时就会抛出序列化异常。
比如:你的本地枚举类,有一个天气Enum:SUNNY, RAINY, CLOUDY,如果根据天气计算心情的方法:guess(WeatcherEnum xx),传入这三个值都是可以的。返回值:Weather guess(参数),那么对方运算后,返回一个SNOWY,本地枚举里没有这个值,傻眼了。
当然,使用 code 照样不能处理,对此,开发手册作者的回答如下
主要是从防止这种序列化异常角度来考虑,使用code至少不会出大乱子。而catch序列化异常,有点像catch(NullPointerException e)一样代码过度,因为它是可预检异常。
1 统一称谓
假如有一枚举类如下:
public enum ReturnCodeEnum {
OK(200),
ERROR(500)
;
private final int code;
ReturnCodeEnum(int code){
this.code=code;
}
public int getCode() {
return code;
}
}
枚举实例有两个默认属性,name
和 ordinal
,可通过 name()和ordinal()方法分别获得。其中 name 为枚举字面量(如 OK),ordinal 为枚举实例默认次序(从0开始)
需要注意的是,不建议使用枚举的 ordinal,因为枚举实例应该是无序的,ordinal 提供的顺序是不可靠的,所以我们应该使用自定义的枚举字段 code。
后文为方便阐述,以 字面量(name)、默认次序(ordinal)和 code来展开阐述。如 OK 的 字面量为 OK,ordinal 为 0 ,code为 200。
2 目标
目标
- 直接使用 枚举类型 接收参数和返回值
- 系统自动将 参数中的 code 转换为 枚举类型,自动将 返回值中的枚举类型转换为 code
实现效果
对于实现通用code枚举接口的枚举类型,有如下效果:
- 使用 bean(application/x-www-form-urlencoded)接收时,支持 code 自动转换为 枚举类型,同时兼容 字面量转换为枚举类型。注意:表单接收的参数都视为 String,即是将String转为 枚举类型
- 使用 @RequestBody (application/json)接收时,默认只支持 code 自动转换为枚举类型。如果需要同时支持 code 和 字面量(或者只支持字面量),可以在具体的枚举类里添加@JsonCreator注解的方法,下文会给出参考实现。
- 可以使用 @RequestParam 和 @PathVariable 接收枚举类型参数
- 使用 @ResponseBody / @RestController(返回 Json)时,默认将 枚举类型转换为 code。
- 在接收参数/返回值都不允许使用 ordinal ,这只会导致混乱。
3 SpringMVC 对 枚举参数的处理
此处只对 restful 接口进行讨论。对于 restful 接口,Spring MVC 的返回值是使用 @ResponseBody 进行处理的。
而参数的接收方式则较多,对于非简单类型,如 Enum ,一般的接收方法为 Bean 接收或 @ResponseBody 接收。
Spring使用Bean接收枚举参数
简单来说 Spring 默认使用Bean接收枚举参数时支持 字面量
,这也是我们常见的做法。
参考自:Spring与枚举参数
GET 请求和 POST Form 请求中的字符串到枚举的转化是通过 org.springframework.core.convert.support.StringToEnumConverterFactory 来实现的.
该类实现了接口 ConverterFactory ,通过调用 Enum.valueOf(Class, String) 实现了这个功能。
向下追溯源码可以发现该方法实际上是从一个 Map<String, Enum> 的字典中获取了转换后的实际值,着这个 String 类型的 Key 的获取方式就是 Enum.name() 返回的结果,即枚举的字面值
。
Spring使用@RequestBody 接收枚举参数
简单来说 Spring使用@RequeseBody 接收枚举参数时支持 字面量和 ordinal
对于@RequestBody,Spring会将其内容视为一段 Json,所做工作为使用 Jackson 完成反序列化。其实现会经过Jackson的EnumDeserializer的deserialize方法。感兴趣的可以去看看源码,这里不贴出来,讲一下思路:
- 使用字面量(String)进行反序列化
- 判断是否是 int 类型,如果是使用 ordinal 进行反序列化,如果数字不在 ordinal 里面,则抛异常
- 判断是否是数组,是的话交由数组处理,否则抛异常
Spring使用@ResponseBody 返回值
如我们平常使用所见,返回的是字面量
4 思路
参照Spring对枚举参数的处理,我们可以提供覆盖/替换Spring的处理来达到我们的效果,
经本人测试,比较好的实现方案有(不考虑反射):
- 自定义Bean接收枚举参数规则:
- 可行方案
通过Spring MVC注入特定类型自定义转换器实现从code到 枚举的自动转换
- 做法
使用 WebMvcConfigurer的addFormatters
注入自定义ConverterFactory,该工厂负责生成 通用code枚举接口的实现类对应的转换器
详见第二部分–代码实现。
- 参考资料
Spring Boot绑定枚举类型参数
- 自定义@RequestBody 和@ResponseBody处理枚举参数
-
可行方案
使用@JsonValue
自定义特定枚举类的Jackson序列化/反序列化方式
- 具体做法
使用 @JsonValue
注解标记 获取code值的枚举实例方法。
- 注意事项
该code值是使用jackson序列化
/反序列化
时枚举对应的值,会覆盖原来从字面量反序列化回枚举的默认实现。
如果想要保留原来从字面量反序列化回枚举类的功能,需要自定义一个@JsonCreator
的构造/静态工厂方法。
- 相关代码
代码如下:@JsonValue
public int getCode() {
return code;
}