1秒登錄
根據上一篇文章在springboot程序中jackson自定義注解和字段解析器的經驗,一開始的操作步驟如下
序列化的時候繼承了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 這個注解已經加到了請求參數上,所以再添加一個允許加注解到字段即可 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並且實現ContextualDeserializer接口,實現ContextualDeserializer接口實現的createContextual接口會創建新的 JsonDeserializer對象並且替換掉當前的this對象。 2、線程安全性問題 由於引入了額外的JavaType成員變量,可能會存在線程安全性問題,但是通過源碼可以得知,針對每個成員變量,如果默認的不支持,則會創建相應的單獨的序列化工具,也就不存在線程安全性問題了。
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 這個注解已經加到了請求參數上,所以再添加一個允許加注解到字段即可 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並且實現ContextualDeserializer接口,實現ContextualDeserializer接口實現的createContextual接口會創建新的 JsonDeserializer對象並且替換掉當前的this對象。 2、線程安全性問題 由於引入了額外的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()); } }
這個注解是加到字段上的,但是之前的一篇文章 spring mvc請求體偷梁換柱:HandlerMethodArgumentResolver 這個注解已經加到了請求參數上,所以再添加一個允許加注解到字段即可
這段代碼和原先是一樣的
/** * @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; } }
上述步驟不多,但是似乎已經天衣無縫,信誓旦旦的來測試個
然后順利得到了一個空指針異常
最后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 接口,照葫蘆畫瓢,那就試試,改造后的代碼如下
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); } }
其實改完之后我是蒙圈的,我有幾點疑問
總之是騾子是馬,拉出來溜溜,這么一改,果然就好用了,但是用起來不痛快,畢竟還存在着疑問呢,帶着疑惑,我進行了源碼追蹤。
在相關的代碼打上斷點
然后運行測試代碼
com.fasterxml.jackson.databind.util.ClassUtil#createInstance
這段代碼使用反射技術利用無參構造方法創建了HdxAesDataDeserializer對象。那么調用時機如何呢,根據調用鏈繼續追蹤,可以看到調用點最終在這里
這段代碼會單獨處理對象的每個成員變量的反序列化,然后每次都會在com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#constructSettableProperty方法中尋找合適的反序列化工具
如果沒找到,則創建合適的反序列化工具
這說明了一個問題,每個成員變量在反序列化的時候如果是自定義的注解和反序列化類,每次都會新建反序列化類,也就不存在線程安全性問題了。
追查調用鏈,還是在com.fasterxml.jackson.databind.deser.BeanDeserializerFactory#constructSettableProperty方法中被調用的,這和上一步創建HdxAesDataDeserializer對象是同一個方法,也就是中1標志的位置,2處標志的位置則是現在createContextual方法被調用的位置。
可以看到,在調用默認構造方法創建了HdxAesDataDeserializer對象之后,又調用了一次createContextual方法使用帶參數的構造方法創建了HdxAesDataDeserializer對象並替換了老的deser對象。
到這里就明白了,原來createContextual方法返回新的JsonSerilizer對象是為了替換掉老的對象。
這時候使用的deser對象已經是createContextual返回的對象了,就可以正常使用JavaType進行反序列化了。
最重要的是反序列化工具要繼承 JsonDeserializer並且實現ContextualDeserializer接口,實現ContextualDeserializer接口實現的createContextual接口會創建新的 JsonDeserializer對象並且替換掉當前的this對象。 2、線程安全性問題 由於引入了額外的JavaType成員變量,可能會存在線程安全性問題,但是通過源碼可以得知,針對每個成員變量,如果默認的不支持,則會創建相應的單獨的序列化工具,也就不存在線程安全性問題了。
由於引入了額外的JavaType成員變量,可能會存在線程安全性問題,但是通過源碼可以得知,針對每個成員變量,如果默認的不支持,則會創建相應的單獨的序列化工具,也就不存在線程安全性問題了。
本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。