# Spring MVC 入参枚举细节

在写网关接口的时候,不由的想到当枚举作为入参时,那前端传入的参数是什么?枚举本质是一个什么数据格式?字符串是 "",对象是 {},集合或数组是 [],那枚举是什么,是 {}?还是 []。

# 入参对象有枚举时传参

准备一个 MatchModeEnum.java 枚举类

@RequiredArgsConstructor
@Getter
public enum MatchModeEnum {
    AND(0, "and"),
    OR(1, "or");
    private final int code;
    private final String name;
}

映射一个对外接口

@GetMapping("/test")
public DataResult test(MatchModeEnum matchMode){
    return matchMode;	
}

通过 Api 调用可以得到以下实验结果

// 第一次
requers: http://localhost:8085/rule/test?matchMode=AND;
response: "AND"; // 返回结果字符串
// 第二次
requers: http://localhost:8085/rule/test?matchMode=and;
response: 400; // 参数异常 
// 第三次
requers: http://localhost:8085/rule/test?matchMode=OR;
response: "OR"; // 返回结果字符串
// 第四次
requers: http://localhost:8085/rule/test?matchMode=0;
response: 400; // 参数异常 
// 第五次
requers: http://localhost:8085/rule/test?matchMode=1;
response: 400; // 参数异常

通过以上调用可知传入的参数是枚举的定义名,且区分大小写。且返回的参数为枚举值的定义名

# 入参是枚举 JSON 字符串时

调整对外接口

@GetMapping("/test")
public DataResult test(@RequestBody MatchModeEnum matchMode){
    return matchMode;
}

通过 Api 调用可以得到以下实验结果

// 第一次
requers: http://localhost:8085/rule/test?matchMode=AND;
response: "AND"; // 返回结果字符串
// 第二次
requers: http://localhost:8085/rule/test?matchMode=and;
response: 400; // 参数异常 
// 第三次
requers: http://localhost:8085/rule/test?matchMode=OR;
response: "OR"; // 返回结果字符串
// 第四次
requers: http://localhost:8085/rule/test?matchMode=0;
response: "AND"; // 返回结果字符串
// 第五次
requers: http://localhost:8085/rule/test?matchMode=1;
response: "AND"; // 返回结果字符串

通过以上调用可知传入的参数是 JSON 字符串时,在转换为枚举时除了可使用枚举的定义名,且区分大小写,还可以使用枚举的定义顺序。

# JSON 字符串中枚举字段绑定源码理解

在程序刚启动,接口调用第一次需要用这个这个枚举对象时,在实例化 com.fasterxml.jackson.databind.deser.std.EnumDeserializer 对象。实例化过程参考下图

image-20210902202229506

实例化组装了两个数据格式 _enumsByIndex 和 _lookupByName 。还看到了 _enumDefaultValue(枚举默认值)和 _caseInsensitive (是否区分大小写)。

看到 _enumsByIndex 时可以知道为什么我们传入 1 或 0 时也可以得到值了。

# 如何在 _lookupByName 中找到枚举

当传入 ADN 或者 OR 这类枚举名字符串时,在 com.fasterxml.jackson.databind.util.CompactStringObjectMap 对象下的 find 方法查找。如下图所示

image-20210902203244039

该方法通过计算传入字符的 hashcode 值与指定数据的位运算符值 14 。然后将 “AND” 取出。再与传入字符串比较是否相等,如果有一处条件满足,则将 14 加 1 成 15 取出 15 号枚举。也就是我们所需要的枚举。

# 入参枚举名不区分大小写

想要实现入参枚举名不区分大小写,在前面实例化 com.fasterxml.jackson.databind.deser.std.EnumDeserializer 对象时第二个参数要为 true 即可将_caseInsensitive (是否区分大小写)设置为 true 。而第二个参数传入的是枚举 MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS。该值默认为 false

//spring boot 配置以下信息即可设置为 ture
spring.jackson.mapper.ACCEPT_CASE_INSENSITIVE_ENUMS = true

以上配置上生效后,仅在 @RequestBody 注解下生效,因为这是 JSON 字符串在反序列化时,对枚举对象的处理。

# _enumDefaultValue(枚举默认值) 属性

记录下前文的 4 个属性中的最后一个 enumDefaultValue (枚举默认值)

首先经测试以下两种设置默认值方式都无法影响 _enumDefaultValue 。这个值仅在接口调用第一次需要用这个这个枚举对象时设置进入。

// 第一种
@GetMapping("/test")  // 赋值
public DataResult test(@RequestParam(required = false, defaultValue = "AND") MatchModeEnum matchMode){
    return new DataResult(matchMode);
}
// 第二种
@Data
public class TestDO {
    private MatchModeEnum matchMode = MatchModeEnum.AND; // 直接赋值
}
@PostMapping("/test")
public DataResult test(@RequestBody TestDO testDo){
    return new DataResult(testDo);
}

经过源码 com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector 类下的 findDefaultEnumValue 方法。

public Enum<?> findDefaultEnumValue(Class<Enum<?>> enumCls) {
    return ClassUtil.findFirstAnnotatedEnumValue(enumCls, JsonEnumDefaultValue.class);
}

所以通过注解 @JsonEnumDefaultValue 可以设置枚举的默认值,同个枚举下多个该注解,仅第一个有效。

为什么我怎么百度都不出来我想要的答案,非要看到源码的这个注解,然后百度一下就全搜到了

# 总结

这是一次比较有意义查找源码,一个一个 debug 下去。只为找到原因所在,但因技术及知识局限,大脑中仍有难以明白且难以找到答案的问题。

疑惑点一

"AND".hashCode() = 64951;
// 转换二进制为:1111110110110111
this._hashMask = 7;
// 转换二进制为:111
1111110110110111 & 111 = 111 // 111 转 10 进制等于 7
// 1111110110110111 & 111 计算方法
// 1111110110110111
// 0000000000000111
// 0000000000000111 // 根据 & 位运算符只有都为 1 的那位才得 1,所以值为 111
// 所以我只要满足一个字符串的哈希值的二进制为 111 结尾就行。而满足的字符串很快就找到两个
"and".hashCode() = 96727; // 二进制 10111100111010111  // 忽略大小写匹配在下一个环节
"g".hashCode() = 103; // 二进制 1100111

可以看到通过其他字符串也是可以得到正确的最后位运算符值 14 。所以最后一行的字符串匹配将尤为的重要。那为什么不弄个更复杂的数字而不是 7 呢?为什么不能直接通过循环匹配呢?为什么要定义这样的数据格式呢?没想到通过枚举名匹配枚举会比下标数字匹配多这么多看不懂的细节。

待续:以后有时间再来看看理解这个为什么吧