根据上一篇文章在springboot程序中jackson自定义注解和字段解析器的经验,一开始的操作步骤如下
序列化的时候继承了StdSerializer,本来想继承StdDeserializer,但是它有个构造参数必须指定 com.fasterxml.jackson.databind.deser.std.StdDeserializer#StdDeserializer(com.fasterxml.jackson.databind.JavaType) protected StdDeserializer(JavaType valueType) { // 26-Sep-2017, tatu: [databind#1764] need to add null-check back until 3.x _valueClass = (valueType == null) ? Object.class : valueType.getRawClass(); _valueType = valueType; } 没弄明白为什么要指定这个valueType,而且要放到构造方法,所以我直接继承了JsonDeserializer,根据DeserializationContext对象也可以直接拿到JavaType呀,我可真是个大聪明~ @Slf4j @AllArgsConstructor @NoArgsConstructor public class HdxAesDataDeserializer extends JsonDeserializer<Object> { @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { String valueAsString = p.getValueAsString(); String s = HdxAesUtil.decryptHex(valueAsString); return ObjectMapperFactory.getObjectMapper().readValue(s, ctxt.getContextualType()); } } 2、定义反序列化自定义注解 这个注解是加到字段上的,但是之前的一篇文章 spring mvc请求体偷梁换柱:HandlerMethodArgumentResolver 这个注解已经加到了请求参数上,所以再添加一个允许加注解到字段即可 3、对注解注释的字段反序列化支持 4、注册到ObjectMapper 这段代码和原先是一样的 /** * @author kdyzm * @date 2021/10/27 */ @Configuration public class JsonConfig { /** * @param builder * @return * @link {https://stackoverflow.com/questions/34965201/customize-jackson-objectmapper-to-read-custom-annotation-and-mask-fields-annotat} * @see JacksonAutoConfiguration.JacksonObjectMapperConfiguration#jacksonObjectMapper(Jackson2ObjectMapperBuilder) */ @Bean @Primary ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper mapper = builder.createXmlMapper(false).build(); AnnotationIntrospector sis = mapper.getSerializationConfig().getAnnotationIntrospector(); AnnotationIntrospector is1 = AnnotationIntrospectorPair.pair(sis, new HdxAesDataAnnotationIntrospector()); mapper.setAnnotationIntrospector(is1); return mapper; } } 5、测试和新问题 上述步骤不多,但是似乎已经天衣无缝,信誓旦旦的来测试个 然后顺利得到了一个空指针异常 最后debug得到的出问题的代码在这里,ctxt.getContextualType()获取到的JavaType是空值。。 二、问题排查和解决方案 谷歌查了下,看到了有价值的github issue:Give Custom Deserializers access to the resolved target Class of the currently deserialized object 还有stackoverflow上的讨论:How to create a general JsonDeserializer 这一切都指向了唯一一种解决方案:实现 ContextualDeserializer 接口,照葫芦画瓢,那就试试,改造后的代码如下 /** * @author kdyzm * @date 2021/11/18 */ @Slf4j @AllArgsConstructor @NoArgsConstructor public class HdxAesDataDeserializer extends JsonDeserializer<Object> implements ContextualDeserializer { private JavaType type; @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { String valueAsString = p.getValueAsString(); String s = HdxAesUtil.decryptHex(valueAsString); return ObjectMapperFactory.getObjectMapper().readValue(s, type); } @Override public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) throws JsonMappingException { //beanProperty is null when the type to deserialize is the top-level type or a generic type, not a type of a bean property JavaType type = deserializationContext.getContextualType() != null ? deserializationContext.getContextualType() : beanProperty.getMember().getType(); return new HdxAesDataDeserializer(type); } } 其实改完之后我是蒙圈的,我有几点疑问 我不明白为什么实现了ContextualDeserializer接口之后实现的方法createContextual要返回一个新的JsonDeserializer对象,这个对象用在什么地方的,和当前的this对象有什么区别,如果是这么搞,岂不是HdxAesDataDeserializer对象创建HdxAesDataDeserializer对象。。。搁这里套娃呢? 这么搞的话,需要引入一个成员变量type,在多线程环境下会不会因此出现线程安全性问题?很明显,如果多线程共享HdxAesDataDeserializer对象,就会出现线程安全性问题,如果每次都新创建HdxAesDataDeserializer对象,就没有线程安全性问题了。 总之是骡子是马,拉出来溜溜,这么一改,果然就好用了,但是用起来不痛快,毕竟还存在着疑问呢,带着疑惑,我进行了源码追踪。 三、源码追踪和解惑 在相关的代码打上断点 然后运行测试代码 1、最先运行无参构造方法 com.fasterxml.jackson.databind.util.ClassUtil#createInstance 这段代码使用反射技术利用无参构造方法创建了HdxAesDataDeserializer对象。那么调用时机如何呢,根据调用链继续追踪,可以看到调用点最终在这里 这段代码会单独处理对象的每个成员变量的反序列化,然后每次都会在com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#constructSettableProperty方法中寻找合适的反序列化工具 如果没找到,则创建合适的反序列化工具 这说明了一个问题,每个成员变量在反序列化的时候如果是自定义的注解和反序列化类,每次都会新建反序列化类,也就不存在线程安全性问题了。 2、createContextual方法被调用 追查调用链,还是在com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#constructSettableProperty方法中被调用的,这和上一步创建HdxAesDataDeserializer对象是同一个方法,也就是中1标志的位置,2处标志的位置则是现在createContextual方法被调用的位置。 可以看到,在调用默认构造方法创建了HdxAesDataDeserializer对象之后,又调用了一次createContextual方法使用带参数的构造方法创建了HdxAesDataDeserializer对象并替换了老的deser对象。 到这里就明白了,原来createContextual方法返回新的JsonSerilizer对象是为了替换掉老的对象。 3、deserialize方法最后被调用 这时候使用的deser对象已经是createContextual返回的对象了,就可以正常使用JavaType进行反序列化了。 四、总结 1、反序列化关键点 最重要的是反序列化工具要继承 JsonDeserializer并且实现ContextualDeserializer接口,实现ContextualDeserializer接口实现的createContextual接口会创建新的 JsonDeserializer对象并且替换掉当前的this对象。 2、线程安全性问题 由于引入了额外的JavaType成员变量,可能会存在线程安全性问题,但是通过源码可以得知,针对每个成员变量,如果默认的不支持,则会创建相应的单独的序列化工具,也就不存在线程安全性问题了。
com.fasterxml.jackson.databind.deser.std.StdDeserializer#StdDeserializer(com.fasterxml.jackson.databind.JavaType)
protected StdDeserializer(JavaType valueType) { // 26-Sep-2017, tatu: [databind#1764] need to add null-check back until 3.x _valueClass = (valueType == null) ? Object.class : valueType.getRawClass(); _valueType = valueType; }
没弄明白为什么要指定这个valueType,而且要放到构造方法,所以我直接继承了JsonDeserializer,根据DeserializationContext对象也可以直接拿到JavaType呀,我可真是个大聪明~ @Slf4j @AllArgsConstructor @NoArgsConstructor public class HdxAesDataDeserializer extends JsonDeserializer<Object> { @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { String valueAsString = p.getValueAsString(); String s = HdxAesUtil.decryptHex(valueAsString); return ObjectMapperFactory.getObjectMapper().readValue(s, ctxt.getContextualType()); } } 2、定义反序列化自定义注解 这个注解是加到字段上的,但是之前的一篇文章 spring mvc请求体偷梁换柱:HandlerMethodArgumentResolver 这个注解已经加到了请求参数上,所以再添加一个允许加注解到字段即可 3、对注解注释的字段反序列化支持 4、注册到ObjectMapper 这段代码和原先是一样的 /** * @author kdyzm * @date 2021/10/27 */ @Configuration public class JsonConfig { /** * @param builder * @return * @link {https://stackoverflow.com/questions/34965201/customize-jackson-objectmapper-to-read-custom-annotation-and-mask-fields-annotat} * @see JacksonAutoConfiguration.JacksonObjectMapperConfiguration#jacksonObjectMapper(Jackson2ObjectMapperBuilder) */ @Bean @Primary ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper mapper = builder.createXmlMapper(false).build(); AnnotationIntrospector sis = mapper.getSerializationConfig().getAnnotationIntrospector(); AnnotationIntrospector is1 = AnnotationIntrospectorPair.pair(sis, new HdxAesDataAnnotationIntrospector()); mapper.setAnnotationIntrospector(is1); return mapper; } } 5、测试和新问题 上述步骤不多,但是似乎已经天衣无缝,信誓旦旦的来测试个 然后顺利得到了一个空指针异常 最后debug得到的出问题的代码在这里,ctxt.getContextualType()获取到的JavaType是空值。。 二、问题排查和解决方案 谷歌查了下,看到了有价值的github issue:Give Custom Deserializers access to the resolved target Class of the currently deserialized object 还有stackoverflow上的讨论:How to create a general JsonDeserializer 这一切都指向了唯一一种解决方案:实现 ContextualDeserializer 接口,照葫芦画瓢,那就试试,改造后的代码如下 /** * @author kdyzm * @date 2021/11/18 */ @Slf4j @AllArgsConstructor @NoArgsConstructor public class HdxAesDataDeserializer extends JsonDeserializer<Object> implements ContextualDeserializer { private JavaType type; @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { String valueAsString = p.getValueAsString(); String s = HdxAesUtil.decryptHex(valueAsString); return ObjectMapperFactory.getObjectMapper().readValue(s, type); } @Override public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) throws JsonMappingException { //beanProperty is null when the type to deserialize is the top-level type or a generic type, not a type of a bean property JavaType type = deserializationContext.getContextualType() != null ? deserializationContext.getContextualType() : beanProperty.getMember().getType(); return new HdxAesDataDeserializer(type); } } 其实改完之后我是蒙圈的,我有几点疑问 我不明白为什么实现了ContextualDeserializer接口之后实现的方法createContextual要返回一个新的JsonDeserializer对象,这个对象用在什么地方的,和当前的this对象有什么区别,如果是这么搞,岂不是HdxAesDataDeserializer对象创建HdxAesDataDeserializer对象。。。搁这里套娃呢? 这么搞的话,需要引入一个成员变量type,在多线程环境下会不会因此出现线程安全性问题?很明显,如果多线程共享HdxAesDataDeserializer对象,就会出现线程安全性问题,如果每次都新创建HdxAesDataDeserializer对象,就没有线程安全性问题了。 总之是骡子是马,拉出来溜溜,这么一改,果然就好用了,但是用起来不痛快,毕竟还存在着疑问呢,带着疑惑,我进行了源码追踪。 三、源码追踪和解惑 在相关的代码打上断点 然后运行测试代码 1、最先运行无参构造方法 com.fasterxml.jackson.databind.util.ClassUtil#createInstance 这段代码使用反射技术利用无参构造方法创建了HdxAesDataDeserializer对象。那么调用时机如何呢,根据调用链继续追踪,可以看到调用点最终在这里 这段代码会单独处理对象的每个成员变量的反序列化,然后每次都会在com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#constructSettableProperty方法中寻找合适的反序列化工具 如果没找到,则创建合适的反序列化工具 这说明了一个问题,每个成员变量在反序列化的时候如果是自定义的注解和反序列化类,每次都会新建反序列化类,也就不存在线程安全性问题了。 2、createContextual方法被调用 追查调用链,还是在com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#constructSettableProperty方法中被调用的,这和上一步创建HdxAesDataDeserializer对象是同一个方法,也就是中1标志的位置,2处标志的位置则是现在createContextual方法被调用的位置。 可以看到,在调用默认构造方法创建了HdxAesDataDeserializer对象之后,又调用了一次createContextual方法使用带参数的构造方法创建了HdxAesDataDeserializer对象并替换了老的deser对象。 到这里就明白了,原来createContextual方法返回新的JsonSerilizer对象是为了替换掉老的对象。 3、deserialize方法最后被调用 这时候使用的deser对象已经是createContextual返回的对象了,就可以正常使用JavaType进行反序列化了。 四、总结 1、反序列化关键点 最重要的是反序列化工具要继承 JsonDeserializer并且实现ContextualDeserializer接口,实现ContextualDeserializer接口实现的createContextual接口会创建新的 JsonDeserializer对象并且替换掉当前的this对象。 2、线程安全性问题 由于引入了额外的JavaType成员变量,可能会存在线程安全性问题,但是通过源码可以得知,针对每个成员变量,如果默认的不支持,则会创建相应的单独的序列化工具,也就不存在线程安全性问题了。
@Slf4j @AllArgsConstructor @NoArgsConstructor public class HdxAesDataDeserializer extends JsonDeserializer<Object> { @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { String valueAsString = p.getValueAsString(); String s = HdxAesUtil.decryptHex(valueAsString); return ObjectMapperFactory.getObjectMapper().readValue(s, ctxt.getContextualType()); } }
这个注解是加到字段上的,但是之前的一篇文章 spring mvc请求体偷梁换柱:HandlerMethodArgumentResolver 这个注解已经加到了请求参数上,所以再添加一个允许加注解到字段即可
这段代码和原先是一样的
/** * @author kdyzm * @date 2021/10/27 */ @Configuration public class JsonConfig { /** * @param builder * @return * @link {https://stackoverflow.com/questions/34965201/customize-jackson-objectmapper-to-read-custom-annotation-and-mask-fields-annotat} * @see JacksonAutoConfiguration.JacksonObjectMapperConfiguration#jacksonObjectMapper(Jackson2ObjectMapperBuilder) */ @Bean @Primary ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) { ObjectMapper mapper = builder.createXmlMapper(false).build(); AnnotationIntrospector sis = mapper.getSerializationConfig().getAnnotationIntrospector(); AnnotationIntrospector is1 = AnnotationIntrospectorPair.pair(sis, new HdxAesDataAnnotationIntrospector()); mapper.setAnnotationIntrospector(is1); return mapper; } }
上述步骤不多,但是似乎已经天衣无缝,信誓旦旦的来测试个
然后顺利得到了一个空指针异常
最后debug得到的出问题的代码在这里,ctxt.getContextualType()获取到的JavaType是空值。。
谷歌查了下,看到了有价值的github issue:Give Custom Deserializers access to the resolved target Class of the currently deserialized object
还有stackoverflow上的讨论:How to create a general JsonDeserializer
这一切都指向了唯一一种解决方案:实现 ContextualDeserializer 接口,照葫芦画瓢,那就试试,改造后的代码如下
ContextualDeserializer
/** * @author kdyzm * @date 2021/11/18 */ @Slf4j @AllArgsConstructor @NoArgsConstructor public class HdxAesDataDeserializer extends JsonDeserializer<Object> implements ContextualDeserializer { private JavaType type; @Override public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { String valueAsString = p.getValueAsString(); String s = HdxAesUtil.decryptHex(valueAsString); return ObjectMapperFactory.getObjectMapper().readValue(s, type); } @Override public JsonDeserializer<?> createContextual(DeserializationContext deserializationContext, BeanProperty beanProperty) throws JsonMappingException { //beanProperty is null when the type to deserialize is the top-level type or a generic type, not a type of a bean property JavaType type = deserializationContext.getContextualType() != null ? deserializationContext.getContextualType() : beanProperty.getMember().getType(); return new HdxAesDataDeserializer(type); } }
其实改完之后我是蒙圈的,我有几点疑问
总之是骡子是马,拉出来溜溜,这么一改,果然就好用了,但是用起来不痛快,毕竟还存在着疑问呢,带着疑惑,我进行了源码追踪。
在相关的代码打上断点
然后运行测试代码
com.fasterxml.jackson.databind.util.ClassUtil#createInstance
这段代码使用反射技术利用无参构造方法创建了HdxAesDataDeserializer对象。那么调用时机如何呢,根据调用链继续追踪,可以看到调用点最终在这里
这段代码会单独处理对象的每个成员变量的反序列化,然后每次都会在com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#constructSettableProperty方法中寻找合适的反序列化工具
如果没找到,则创建合适的反序列化工具
这说明了一个问题,每个成员变量在反序列化的时候如果是自定义的注解和反序列化类,每次都会新建反序列化类,也就不存在线程安全性问题了。
追查调用链,还是在com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#constructSettableProperty方法中被调用的,这和上一步创建HdxAesDataDeserializer对象是同一个方法,也就是中1标志的位置,2处标志的位置则是现在createContextual方法被调用的位置。
可以看到,在调用默认构造方法创建了HdxAesDataDeserializer对象之后,又调用了一次createContextual方法使用带参数的构造方法创建了HdxAesDataDeserializer对象并替换了老的deser对象。
到这里就明白了,原来createContextual方法返回新的JsonSerilizer对象是为了替换掉老的对象。
这时候使用的deser对象已经是createContextual返回的对象了,就可以正常使用JavaType进行反序列化了。
最重要的是反序列化工具要继承 JsonDeserializer并且实现ContextualDeserializer接口,实现ContextualDeserializer接口实现的createContextual接口会创建新的 JsonDeserializer对象并且替换掉当前的this对象。 2、线程安全性问题 由于引入了额外的JavaType成员变量,可能会存在线程安全性问题,但是通过源码可以得知,针对每个成员变量,如果默认的不支持,则会创建相应的单独的序列化工具,也就不存在线程安全性问题了。
由于引入了额外的JavaType成员变量,可能会存在线程安全性问题,但是通过源码可以得知,针对每个成员变量,如果默认的不支持,则会创建相应的单独的序列化工具,也就不存在线程安全性问题了。