zoukankan      html  css  js  c++  java
  • 在springboot程序中自定义注解和反序列化实现

    根据上一篇文章在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);
        }
    }
    

    其实改完之后我是蒙圈的,我有几点疑问

    1. 我不明白为什么实现了ContextualDeserializer接口之后实现的方法createContextual要返回一个新的JsonDeserializer对象,这个对象用在什么地方的,和当前的this对象有什么区别,如果是这么搞,岂不是HdxAesDataDeserializer对象创建HdxAesDataDeserializer对象。。。搁这里套娃呢?
    2. 这么搞的话,需要引入一个成员变量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

  • 相关阅读:
    大数据经典学习路线(及供参考)
    Redis配置规范
    mysql中datetime和timestamp类型的区别
    bigint(10)和bigint(20)的区别
    Redis分布式锁,基于StringRedisTemplate和基于Lettuce实现setNx
    Spring security UserDetailsService autowired注入失败错误
    Java Print 打印
    Spring boot Junit Test单元测试
    RESTful作用与特性
    JS匿名函数理解
  • 原文地址:https://www.cnblogs.com/kuangdaoyizhimei/p/15578482.html
Copyright © 2011-2022 走看看