spring中使用jackson反系列化实体类中Enum类型可以使用数字下标实例化的问题

spring中使用Jackson反系列化实体类中Enum类型可以使用数字下标实例化的问题

现象

@RequiredArgsConstructor
@Getter
public enum TestEnum {
    A("A", "一"),
    B("B", "二"),
    C("C", "三");

    private final String value;
    private final String convert;
}
@Data
public class TestReq {
    private TestEnum anEnum;
}
@RestController
public class TestController {

    @PostMapping("/test")
    public TestReq test(@RequestBody TestReq testReq) {
        return testReq;
    }
}

三个类如上。此时,当我们传入

{“anEnum”:1}

响应为

{
“anEnum”: “B”
}

现象就是传入1的情况下,会使用下标来寻找Enum的实例。大多数情况是无伤大雅甚至说很人(hua)性(she)化(tian)的(zu),但是在我们想要强校验参数时候就需要避免这种情况了。

排查1

首先我先在网上检索了一下关于枚举索引被映射为枚举的信息,发现寥寥无几,有几条还大多指向了ConvertFactory这个接口。
因为我们使用applicaiton/json的Content-Type,此时Spring使用Jackson转换为实体类,因此推测问题在Jackson中。
查找源码,在AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,Type targetType)
方法中,spring会找到一个合适的HttpMessageConverter对body进行解析
readWithMessageConverters方法
可以看到,spring找到所有的HttpMessageConverter进行遍历,直到找到canRead的convert,然后进行解析。我们看下这些convert的属性。
convert属性
此时我们

body:{"anEnum":1}
Content-Type:application/json;charset=UTF-8

因此我们将会使用索引为7的convert,即MappingJackson2HttpMessageConverter

继续向下步入方法
org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter#readJavaType
我们可以看到,此处调用的Jackson的objectMapper的readValue方法反序列化body的值,然后返回。

接下来开始翻查Jackson对枚举实例化的处理。

排查Jackson

com.fasterxml.Jackson.databind.deser.BeanDeserializer#deserializeFromObject中,会针对传入json的每一个属性开始实例化。
com.fasterxml.Jackson.databind.deser.BeanDeserializer#deserializeFromObject
步入prop.deserializeAndSet(p, ctxt, bean)

deserializeAndSet
可以看到,Jackson解析到此时**{“anEnum”:1}JsonToken.VALUE_NUMBER_INT**,故进入下面的分支。

可以看到主要的有三步。

  • 1中,获得传入索引,此时为1
  • 2中,判断是否开启DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS,如果开启则直接抛出异常,否则进入3
  • 3中,根据索引获取枚举实例

3中索引的取值为枚举的ordinal属性的值


若我们传入的参数为

{"anEnum":"1"}

字符串的"1",此时会进入

if (curr == JsonToken.VALUE_STRING || curr == JsonToken.FIELD_NAME) 

分支中,同样我们也可以看下该分支的逻辑。

可以看到此时Jackson解析为JsonToken.VALUE_STRING,会进入lookup中根据传入String进行find操作,此时lookup中K-V对应如图。
在这里插入图片描述

可以参照EnumResolver类查看Jackson解析枚举得到的map

因此,代码会进入下一块中,即**_deserializeAltString**
_deserializeAltString
可以看到

  • 1中,判断是否开启DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS,若未开启,则进入2
  • 2中,将String解析为int类型的index,进入3
  • 3中,按照索引取得枚举实例

综合以上,我们不难看出,正是ObjectMapper中的 DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS和我们想要达成的结果相关。
FAIL_ON_NUMBERS_FOR_ENUMS
可以看到,注释的主要意思是:

首先这个Feature是默认false的,在integer类型json反序列化的时候,如果值为false,那么数字书可以被接受并映射匹配到枚举的oridnal()中。
如果值为true,那么不允许使用数字,并且引发JsonMappingException

因为,我们将这个ObjectMapper的configure中设置
FAIL_ON_NUMBERS_FOR_ENUMS为true即可满足我们的要求。

解决

我们要操作MappingJackson2HttpMessageConverter的ObjectMapper。

AbstractJackson2HttpMessageConverter提供了两个方法

正好合用。我们可以使用ApplicationContext获取JackSon的HttpMessageConverter实现类,然后调用getObjectMapper后操作该ObjectMapper的configure即可。
如下代码

@RestController
@RequiredArgsConstructor
public class TestController {
    private final ApplicationContext applicationContext;

    @PostMapping("/test")
    public TestReq test(@RequestBody TestReq testReq) {
        return testReq;
    }

    @PostConstruct
    public void init() {
        Arrays
                .stream(applicationContext.getBeanNamesForType(AbstractJackson2HttpMessageConverter.class))
                .forEach(it -> {
                    AbstractJackson2HttpMessageConverter bean = applicationContext.getBean(it, AbstractJackson2HttpMessageConverter.class);
                    bean.getObjectMapper().configure(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS, true);
                });
    }
}

除了使用@PostContruct也可以使用实现CommandLineRunner接口或者其他方法
使用applicationContext获取所有AbstractJackson2HttpMessageConverter实现类是因为spring存在两个Jackson的HttpMessageConverter实现类


启动测试后,控制台响应

2021-04-22 16:35:42.079  WARN 6468 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `an.boot.demo.model.TestEnum` from String "1": value not one of declared Enum instance names: [A, B, C]; nested exception is com.fasterxml.Jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `an.boot.demo.model.TestEnum` from String "1": value not one of declared Enum instance names: [A, B, C]
 at [Source: (PushbackInputStream); line: 1, column: 11] (through reference chain: an.boot.demo.model.TestReq["anEnum"])]

成功,然后我们就可以使用异常处理器进行处理了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值