在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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM