在springboot程序中自定义注解和反序列化实现

Published on 2021-11-19 17:38 in 分类: 随笔 with 狂盗一枝梅
分类: 随笔

根据上一篇文章在springboot程序中jackson自定义注解和字段解析器的经验,一开始的操作步骤如下

一、初始解决方案

1、定义反序列化组件

序列化的时候继承了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 这个注解已经加到了请求参数上,所以再添加一个允许加注解到字段即可

image-20211119161540842

3、对注解的反序列化支持

image-20211119161702379

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、测试和新问题

上述步骤不多,但是似乎已经天衣无缝,信誓旦旦的来测试个

然后顺利得到了一个空指针异常

image-20211119162652624

最后debug得到的出问题的代码在这里,ctxt.getContextualType()获取到的JavaType是空值。。

image-20211119162742109

二、问题排查和解决方案

谷歌查了下,看到了有价值的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对象,就没有线程安全性问题了。

总之是骡子是马,拉出来溜溜,这么一改,果然就好用了,但是用起来不痛快,毕竟还存在着疑问呢,带着疑惑,我进行了源码追踪。

三、源码追踪和解惑

在相关的代码打上断点

image-20211119164822674

然后运行测试代码

1、最先运行无参构造方法

com.fasterxml.jackson.databind.util.ClassUtil#createInstance

image-20211119165533673

这段代码使用反射技术利用无参构造方法创建了HdxAesDataDeserializer对象。那么调用时机如何呢,根据调用链继续追踪,可以看到调用点最终在这里

image-20211119165912001

这段代码会单独处理对象的每个成员变量的反序列化,然后每次都会在com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#constructSettableProperty方法中寻找合适的反序列化工具

image-20211119170219502

如果没找到,则创建合适的反序列化工具

image-20211119170758459

这说明了一个问题,每个成员变量在反序列化的时候如果是自定义的注解和反序列化类,每次都会新建反序列化类,也就不存在线程安全性问题了。

2、createContextual方法被调用

追查调用链,还是在com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#constructSettableProperty方法中被调用的,这和上一步创建HdxAesDataDeserializer对象是同一个方法,也就是中1标志的位置,2处标志的位置则是现在createContextual方法被调用的位置。

image-20211119172057757

可以看到,在调用默认构造方法创建了HdxAesDataDeserializer对象之后,又调用了一次createContextual方法使用带参数的构造方法创建了HdxAesDataDeserializer对象并替换了老的deser对象。

到这里就明白了,原来createContextual方法返回新的JsonSerilizer对象是为了替换掉老的对象。

3、deserialize方法最后被调用

这时候使用的deser对象已经是createContextual返回的对象了,就可以正常使用JavaType进行反序列化了。

四、总结

1、反序列化关键点

最重要的是反序列化工具要继承 JsonDeserializer并且实现ContextualDeserializer接口,实现ContextualDeserializer接口实现的createContextual接口会创建新的 JsonDeserializer对象并且替换掉当前的this对象。

2、线程安全性问题

由于引入了额外的JavaType成员变量,可能会存在线程安全性问题,但是通过源码可以得知,针对每个成员变量,如果默认的不支持,则会创建相应的单独的序列化工具,也就不存在线程安全性问题了。

image-20211119165912001


#jackson
目录