现象
@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进行解析
可以看到,spring找到所有的HttpMessageConverter进行遍历,直到找到canRead的convert,然后进行解析。我们看下这些convert的属性。
此时我们
body:{"anEnum":1}
Content-Type:application/json;charset=UTF-8
因此我们将会使用索引为7的convert,即MappingJackson2HttpMessageConverter。
继续向下步入方法
我们可以看到,此处调用的Jackson的objectMapper的readValue方法反序列化body的值,然后返回。
接下来开始翻查Jackson对枚举实例化的处理。
排查Jackson
com.fasterxml.Jackson.databind.deser.BeanDeserializer#deserializeFromObject中,会针对传入json的每一个属性开始实例化。
步入prop.deserializeAndSet(p, ctxt, bean)
可以看到,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**
可以看到
- 1中,判断是否开启DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS,若未开启,则进入2
- 2中,将String解析为int类型的index,进入3
- 3中,按照索引取得枚举实例
综合以上,我们不难看出,正是ObjectMapper中的 DeserializationFeature.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"])]
成功,然后我们就可以使用异常处理器进行处理了。