由@Convert注解引出的jackson对枚举的反序列化规则
对于一些状态字段以前时兴用常量,现在时兴用枚举,虽然阅读体验极佳,但是传值的时候还是会有些麻烦,需要设置一下转换器.比如:
class A{ @Convert(converter=TestTypeConverter.class) private TestType test; public TestType getTestType() { return test; } public void setTestType(TestType test) { this.test= test; } }
我们定义了如上一个类,其中的一个成员变量是枚举,为了能正常的接收前端的值,一般会给这个枚举定义个转换器来实现String到枚举的转换.但是呢同事发现不定义这个转换器依然可以接收前端的值,这引起了我的兴趣,所以打算一探究竟..
项目使用的Spring推荐的Jackson作为json的编解码,因为前后端都是用json传值,所以这个问题也就转化成Jackson究竟做了什么内部的优化能达到不定义转换器的情况下依然可以正确的反序列化枚举.然后我们看一下这个示例的枚举TestType:
public enum TestType { ZERO(0,"0页"), ONE(1,"1页"), TWO(2,"2页"); @JsonCreator public static IsAllType get(int value) { try { return map.get(value); } catch (Exception e) { return null; } } private int value; private String text; IsAllType(int value, String text){ this.value=value; this.text=text; } @JsonValue public int getValue() { return value; } }
示例,所以删减了部分无关代码.要debug跟踪,所以就通过json字符串转枚举这一过程来模拟前端传值这一操作,例子如下:
public static void main(String[] args) { A a = JSONUtils.readValue("{\"test\": \"1\"}", A.class); }
经过一顿debug发现,Jackson会优先使用@JsonCreator注解定义的方法进行构造枚举值,由于定义了构造方法,某种意义上这就是一个简易的枚举转换器.
到此那个不定义converter就能接收值的问题看似已经结束,但是我的好奇心是很旺盛的..我把这个@JsonCreator注解注释掉看看会怎么样?然后发现依然可以正常接收值..这就有点意思了..又是一顿debug发现如下关键代码
public static EnumResolver constructUsingMethod(Class<Enum<?>> enumCls, Method accessor) { Enum<?>[] enumValues = enumCls.getEnumConstants(); HashMap<String, Enum<?>> map = new HashMap<String, Enum<?>>(); // from last to first, so that in case of duplicate values, first wins for (int i = enumValues.length; --i >= 0; ) { Enum<?> en = enumValues[i]; try { Object o = accessor.invoke(en); if (o != null) { map.put(o.toString(), en); } } catch (Exception e) { throw new IllegalArgumentException("Failed to access @JsonValue of Enum value "+en+": "+e.getMessage()); } } return new EnumResolver(enumCls, enumValues, map); }
jackson会构造出一个map,这个map的key是枚举值的value值,value是枚举类中对应的枚举值,然后通过这个map依然可以实现值到枚举类的转换.那么何为枚举值的value值?
@JsonValue public int getValue() { return value; }
jackson通过@JsonValue注解定义的方法返回值作为是枚举值的value值,通过这个value值又反向建立了关联,那我把这个@JsonValue注解也注释掉看看会怎么样?一运行发现还是可以接收值..
接着一顿debug发现如下关键代码:
public static EnumResolver constructUsingToString(Class<Enum<?>> enumCls) { Enum<?>[] enumValues = enumCls.getEnumConstants(); HashMap<String, Enum<?>> map = new HashMap<String, Enum<?>>(); // from last to first, so that in case of duplicate values, first wins for (int i = enumValues.length; --i >= 0; ) { Enum<?> e = enumValues[i]; map.put(e.toString(), e); } return new EnumResolver(enumCls, enumValues, map); }
通过获取枚举类中所有的枚举值,然后它构建了一个map,只不过这个map的key有点特殊,是枚举值的ordinal值,value为枚举值,所以此时如果枚举值的value是从0开始,也就是恰巧和ordinal值重合时,这样转换不会有问题,否则则会大错特错..debug的途中发现一些好玩的开发者留言..
public static EnumResolver constructUnsafe(Class<?> rawEnumCls, AnnotationIntrospector ai) { /* This is oh so wrong... but at least ugliness is mostly hidden in just * this one place. */ Class<Enum<?>> enumCls = (Class<Enum<?>>) rawEnumCls; return constructFor(enumCls, ai); } /** * Method that needs to be used instead of {@link #constructUsingToString} * if static type of enum is not known. */ @SuppressWarnings({ "unchecked" }) public static EnumResolver constructUnsafeUsingToString(Class<?> rawEnumCls) { // oh so wrong... not much that can be done tho Class<Enum<?>> enumCls = (Class<Enum<?>>) rawEnumCls; return constructUsingToString(enumCls); } /** * Method used when actual String serialization is indicated using @JsonValue * on a method. */ @SuppressWarnings({ "unchecked" }) public static EnumResolver constructUnsafeUsingMethod(Class<?> rawEnumCls, Method accessor) { // wrong as ever but: Class<Enum<?>> enumCls = (Class<Enum<?>>) rawEnumCls; return constructUsingMethod(enumCls, accessor); }
到此基本上我的好奇心也耗尽了...
总结: 1.jackson会优先使用@JsonCreator注解标注的构造方法构造枚举值
2.jackson会通过@JsonValue注解标注的方法作为value值构建value与枚举值的map映射
3.jackson会通过枚举值的ordinal值与枚举值构建map映射
ps:看来spring推荐jackson也并无道理,的确做了一些看不见的内部优化..