在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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章