深⼊解析Spring使⽤枚举接收参数和返回值机制并提供⾃定义最佳实践
Spring对应枚举传参/返回值默认是⽤字⾯量实现的(实际情况更复杂),⽽《阿⾥巴巴Java开发⼿册》规定接⼝返回值不可以使⽤枚举类型(包括含枚举类型的POJO对象),为此,本⽂探究了Spring内部对枚举参数的传递和处理机制,并提供了⼀套⾃定义⽅案。
⽂章⽬录
⼀⽬标与思路
0 起因
《阿⾥巴巴Java开发⼿册》将接⼝中枚举的使⽤分为两类,即 接⼝参数和接⼝返回值,并规定:
接⼝参数可以使⽤枚举类型,但接⼝返回值不可以使⽤枚举类型(包括含枚举类型的POJO对象)。
知乎有相关讨论和作者亲答,详情可见:
现摘录⼀部分作者回答如下:
由于升级原因,导致双⽅的枚举类不尽相同,在接⼝解析,类反序列化时出现异常。
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){
}
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 ⽬标
⽬标
1. 直接使⽤ 枚举类型 接收参数和返回值
2. 系统⾃动将 参数中的 code 转换为 枚举类型,⾃动将 返回值中的枚举类型转换为 code
实现效果
对于实现通⽤code枚举接⼝的枚举类型,有如下效果:
1. 使⽤ bean(application/x-www-form-urlencoded)接收时,⽀持 code ⾃动转换为 枚举类型,同时兼容 字⾯量转换为枚举类
型。注意:表单接收的参数都视为 String,即是将String转为 枚举类型
2. 使⽤ @RequestBody (application/json)接收时,默认只⽀持 code ⾃动转换为枚举类型。如果需要同时⽀持 code 和 字⾯量
(或者只⽀持字⾯量),可以在具体的枚举类⾥添加@JsonCreator注解的⽅法,下⽂会给出参考实现。
3. 可以使⽤ @RequestParam 和 @PathVariable 接收枚举类型参数
4. 使⽤ @ResponseBody / @RestController(返回 Json)时,默认将 枚举类型转换为 code。
5. 在接收参数/返回值都不允许使⽤ ordinal ,这只会导致混乱。
3 SpringMVC 对枚举参数的处理
此处只对 restful 接⼝进⾏讨论。对于 restful 接⼝,Spring MVC 的返回值是使⽤ @ResponseBody 进⾏处理的。
⽽参数的接收⽅式则较多,对于⾮简单类型,如 Enum ,⼀般的接收⽅法为 Bean 接收或 @ResponseBody 接收。
Spring使⽤Bean接收枚举参数
简单来说 Spring 默认使⽤Bean接收枚举参数时⽀持 字⾯量,这也是我们常见的做法。
参考⾃:
GET 请求和 POST Form 请求中的字符串到枚举的转化是通过
onvert.support.StringToEnumConverterFactory 来实现的.
该类实现了接⼝ ConverterFactory ,通过调⽤ Enum.valueOf(Class, String) 实现了这个功能。
向下追溯源码可以发现该⽅法实际上是从⼀个 Map<String, Enum> 的字典中获取了转换后的实际值,着这个 String 类型的Key 的获取⽅式就是 () 返回的结果,即枚举的字⾯值。
Spring使⽤@RequestBody 接收枚举参数
简单来说 Spring使⽤@RequeseBody 接收枚举参数时⽀持 字⾯量和 ordinal
对于@RequestBody,Spring会将其内容视为⼀段 Json,所做⼯作为使⽤ Jackson 完成反序列化。其实现会经过Jackson的EnumDeserializer的deserialize⽅法。感兴趣的可以去看看源码,这⾥不贴出来,讲⼀下思路:
1. 使⽤字⾯量(String)进⾏反序列化
2. 判断是否是 int 类型,如果是使⽤ ordinal 进⾏反序列化,如果数字不在 ordinal ⾥⾯,则抛异常
3. 判断是否是数组,是的话交由数组处理,否则抛异常
Spring使⽤@ResponseBody 返回值
如我们平常使⽤所见,返回的是字⾯量
4 思路
参照Spring对枚举参数的处理,我们可以提供覆盖/替换Spring的处理来达到我们的效果,
经本⼈测试,⽐较好的实现⽅案有(不考虑反射):
1. ⾃定义Bean接收枚举参数规则:
1. 可⾏⽅案
通过Spring MVC注⼊特定类型⾃定义转换器实现从code到 枚举的⾃动转换
2. 做法
使⽤ WebMvcConfigurer的addFormatters注⼊⾃定义ConverterFactory,该⼯⼚负责⽣成 通⽤code枚举接⼝的实现类对应的转换器
详见第⼆部分–代码实现。
3. 参考资料
2. ⾃定义@RequestBody 和@ResponseBody处理枚举参数
1. 可⾏⽅案
使⽤@JsonValue⾃定义特定枚举类的Jackson序列化/反序列化⽅式
1. 具体做法
使⽤ @JsonValue注解标记 获取code值的枚举实例⽅法。
2. 注意事项
该code值是使⽤jackson序列化/反序列化时枚举对应的值,会覆盖原来从字⾯量反序列化回枚举的默认实现。
如果想要保留原来从字⾯量反序列化回枚举类的功能,需要⾃定义⼀个@JsonCreator 的构造/静态⼯⼚⽅法。
3. 相关代码
代码如下:
@JsonValue
public int getCode(){
return code;
}
enum怎么用@JsonCreator
public static ReturnCodeEnum create(String name){
try{
return ReturnCodeEnum.valueOf(name);
}catch(IllegalArgumentException e){
int code=Integer.parseInt(name);
for(ReturnCodeEnum value : ReturnCodeEnum.values()){
de==code){
return value;
}
}
}
throw new IllegalArgumentException("No element matches "+name);
throw new IllegalArgumentException("No element matches "+name);
}
2. 不可⾏⽅案
1. 替换@RequestBody和@ResponseBody或相关处理器 / ⾃定义HttpMessageConverter
1. 例⼦
如使⽤⾃定义的 @ResponseBody 注解及对应 HandlerMethodReturnValueHandler
使⽤⾃定义 HttpMessageConverter 实现对 json 返回资源的完全控制
2. 不可⾏原因
我们平时使⽤ @ResponseBody 是交给 RequestResponseBodyMethodProcessor这个类处理,所以我们也可以弃⽤
@ResponseBody并⾃⼰写⼀个注解和对负责处理该注解的 HandlerMethodReturnValueHandler。这样我们就可
以完全控制返回值的处理了。这样也就相对于放弃了 @ResponseBody。
⾃⼰实现 HttpMessageConverter 则是在更⾼的层次进⾏处理,代价太⼤。
3. 相关资料
⾃定义注解和HandlerMethodReturnValueHandler可以参考:
2. 使⽤@JsonCreator在接⼝层⾯定义反序列化规则
1. 不可⾏原因
@JsonCreator只适⽤于枚举类不适⽤于接⼝。
@JsonCreator本质上是要在没有类实例的时候使⽤的,所以只能标记在 构造⽅法或者静态⼯⼚⽅法上,接⼝的话
不可⾏,传统的接⼝⽅法属实例⽅法,新增的 default ⽅法也属实例⽅法,另外的 static ⽅法⼜不可继承。所以这
个思路只限于具体类型,不适⽤于接⼝。
2. 相关资料
相关Jackson资料参考:
3. 适⽤@JsonDeserialize在接⼝层⾯定义反序列化规则
1. 不可⾏原因
注解⾃定义从 json字符串 转换为 实体类的⽅法也只适⽤于枚举类不适⽤于接⼝。
使⽤@JsonDeserialize(using = ⾃定义反序列化类.class),在⾃定义Jackson反序列化类实现
deserialize(JsonParser p, DeserializationContext ctxt)⽅法。
可以获取 json字符串(即 code),但没办法通过接⼝使⽤code获取枚举对象,理由同上,接⼝没有可⽤的同时可
继承的⽅法。
2. 相关资料
⾃定义Jackson序列化/反序列化类参考:
⼆代码实现
1 通⽤code枚举接⼝
/**
* @version V1.0
* @author: linshenkx
* @date: 2019/1/12
* @description: 带编号的枚举接⼝
*/
public interface CodedEnum {
/**
* 使⽤jackson序列化/反序列化时枚举对应的值
* 如果想要保留原来从字⾯量反序列化回枚举类的功能, * 需要⾃定义⼀个 @JsonCreator 的构造/静态⼯⼚⽅法 * @return ⾃定义枚举code
*/
@JsonValue
int getCode();
}
2 转换器⼯⼚类
代码实现
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论