根据上一篇文章在springboot程序中jackson自定义注解和字段解析器的经验,一开始的操作步骤如下
一、初始解决方案
1、定义反序列化组件
序列化的时候继承了StdSerializer
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
@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
2、线程安全性问题
由于引入了额外的JavaType成员变量,可能会存在线程安全性问题,但是通过源码可以得知,针对每个成员变量,如果默认的不支持,则会创建相应的单独的序列化工具,也就不存在线程安全性问题了。
注意:本文归作者所有,未经作者允许,不得转载