分享一个 Jackson keyUsing 注解的坑
作为一个 10 年的老 Java,应该可以说是对 Jackson 知根知底了吧,然而最近却栽在了 Jackson Map Key 的序列化问题上面,郁闷了好一阵子,不吐不快。
Map Key 的特殊性
我们知道,json 的基本结构是 Key-Value 对,其中 Key 的类型固定是一个 string 字符串。
但在 Java 的体系里面,一个 Map 的 Key 可以是一个任意对象。
比如在我们最近的项目中,由于用到了大量的范围定位技术,其中 Key 是一个 Range,对于这样的 Map,其序列化之后是啥样呢?
public class Range {
private int start;
private int end;
}
public static void main(String[] args) throws Exception {
Map<Range, String> map = new HashMap<>();
map.put(new Range(1, 2), "start point");
map.put(new Range(1, 3), "end point");
String json = JsonUtils.getMapper().writeValueAsString(map);
System.out.println(json);
}
其默认输出结果是这样的:
{"Range{start=1, end=2}":"start point","Range{start=1, end=3}":"end point"}
观察这个结果会发现,Jackson 在输出 Map 的 Key 对象时,默认是调用了 Range 的 toString() 方法。而这显然不是我们想要的结果。我们希望 Key可以被当成一个普通对象进行序列化,类似于下面这样的结果:
{"{\"start\":1,\"end\":2}":"start point","{\"start\":1,\"end\":3}":"end point"}
这样,后端无需做代码改造,前端拿到数据之后,对 Key 再做一次反序列化,就可以了。
自定义 Key Serializer/Deserializer
为了解决这个问题,我们可以自己写一个 Json Serializer/Deserializer,代码如下:
public class PojoKeySerializer extends JsonSerializer {
@Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
ObjectMapper mapper = (ObjectMapper) gen.getCodec();
gen.writeFieldName(mapper.writeValueAsString(value));
}
}
public class PojoKeyDeserializer extends KeyDeserializer {
private final Class clazz;
public PojoKeyDeserializer(Class clazz) {
this.clazz = clazz;
}
@Override
public Object deserializeKey(String key, DeserializationContext ctxt) throws IOException {
ObjectMapper mapper = (ObjectMapper) ctxt.getParser().getCodec();
return mapper.readValue(key, clazz);
}
}
然后把它注册上去
SimpleModule module = new SimpleModule();
module.addKeySerializer(Range.class, new PojoKeySerializer());
module.addKeyDeserializer(Range.class, new PojoKeyDeserializer(Range.class));
mapper.registerModule(module);
由于项目中有多个类似的对象用作 Key,因此上面的 PojoKeySerializer 和 PojoKeyDeserializer 都是通用的,支持任意的 Pojo 对象。
需要额外注意的是,序列化实现的接口是 JsonSerializer,而反序列化的接口是 KeyDeserializer,有点不一样,至于原因,不是本文的重点,如想了解,可以在评论区留言。
注解实现方式
采用 SimpleModule 的方式,虽然可以满足需求,但有一个问题,就是每新增一个类,都需要在这里配置一下,一个功能,两处编写,很多新同学容易忘掉,造成潜在问题。最好的方式应该是类似下面这样的。
@JsonSerialize(keyUsing = PojoKeySerializer.class)
@JsonDeserialize(keyUsing = PojoKeyDeserializer.class)
public class Range {
}
利用 Jackson 的注解,直接写在对应的Pojo类上,这样就清爽多了。
然而,真正的麻烦也来了。
反序列化的麻烦
认真读到这里的老铁会发现,反序列化器 PojoKeyDeserializer 是一个带构造参数的类,因为反序列化时需要知道对应的类型。
我们可以尝试用下面的代码,反序列化一下前文的json数据:
String json = "{\"{\\\"start\\\":1,\\\"end\\\":2}\":\"start point\",\"{\\\"start\\\":1,\\\"end\\\":3}\":\"end point\"}";
Map<Range, String> anotherMap = JsonUtils.getMapper().readValue(json, new TypeReference<Map<Range, String>>() {});
它会报错:
Exception in thread "main" com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Class com.xcodemap.example.PojoKeyDeserializer has no default (no arg) constructor
这说明,Jackson 并不会给 PojoKeyDeserializer 注入构造参数。
序列化上下文
就这个构造参数的问题,在网上找了很多资料,问了多个AI,看了不少源码,最终找到了一个比较理想的方案,就是实现 ContextualKeyDeserializer,具体代码如下:
public class PojoKeyDeserializer2 extends KeyDeserializer implements ContextualKeyDeserializer {
private ThreadLocal<JavaType> javaTypeThreadLocal = new ThreadLocal<>();
@Override
public KeyDeserializer createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException {
JavaType javaType = ctxt.getContextualType().containedType(0);
javaTypeThreadLocal.set(javaType);
return this;
}
}
看这个类的实现,可以大致明白,Jackson 通过调用 createContextual 方法,透传上下文信息比如JavaType,然后返回一个新的 KeyDeserializer。在这里,有一个小技巧,为了避免频繁创建对象,利用 ThreadLocal 存储类型信息。
源码分析
我们可以使用 Idea 的插件 XCodeMap 画个动态序列图,跟一下源码(XCodeMap 是一个阅读神器,没有它,写这篇文章,我得多掉几根头发,可以查看它们官网 xcodemap.tech)。
Jackson 的基本原理就是,针对每个 Key 和 Value,都去寻找对应的序列化器。然后最顶层,没有Key,所以就当作 Value 处理,执行 findRootValueDeserializer,本例中,返回的就是 MapDeserializer。
普通 Java 对象的 Key 就是字段 Field 的name,不需要特别处理。但是对于 Map 对象,其 Key 本身是一个 Java 对象,需要额外处理,因此需要执行 findKeyDeserializer,在本例中,其返回结果是PojoKeyDeserializer2。
我们看一下这个关键函数 findKeyDeserializer:
可以看红框部分,如果实现了 ContextualKeyDeserializer,就调用 createContextual 方法,并用其返回结果替换自身。
后记
核心内容就是这样的,更多的细节,老铁们可以去翻阅一下源码哦。
卖个关子,除了实现 ContextualKeyDeserializer,其实还有一种方式,也可以实现类似的效果,知道的老铁可以在评论区留个言哦。
转载自:https://juejin.cn/post/7390218064590274595