美文网首页
Java Jackson 序列化Properties的方式在2.

Java Jackson 序列化Properties的方式在2.

作者: 天蝎座的火龙果 | 来源:发表于2018-01-30 00:08 被阅读0次

    问题描述

    从老的rdb_ib这个项目迁移到新的Spring框架之后, 发现原先模型中的Properties这个类型在序列化的时候报错了,由于我们在Configuration这个模型中用Properties这个类型保存了我们数据库的options,我们的options中有一个max_active参数是一个Integer, 然后在转JSON的过程中, 会报Jackson转换异常, 无法将Integer转化成String

    2018-01-28 17:51:02.092 [qtp1805672691-28] ERROR ConfigurationsController  - Caught unhandled exception ClassCastException: java.lang.Integer cannot be cast to java.lang.String
            at com.fasterxml.jackson.databind.ser.std.StringSerializer.serialize(StringSerializer.java:49)
            at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFieldsUsing(MapSerializer.java:736)
     [wrapped] com.fasterxml.jackson.databind.JsonMappingException: java.lang.Integer cannot be cast to java.lang.String (through reference chain: java.util.ArrayList[0]->com.joowing.rdb_ib.model.Configuration["o
    ptions"]->java.util.Hashtable["max_active"])
            at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:388)
            at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:348)
            at com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:343)
            at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFieldsUsing(MapSerializer.java:742)
            at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:534)
            at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:30)
            at com.fasterxml.jackson.databind.ser.BeanPropertyWriter.serializeAsField(BeanPropertyWriter.java:704)
            at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:689)
            at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase._serializeWithObjectId(BeanSerializerBase.java:611)
            at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:148)
            at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serializeContents(IndexedListSerializer.java:119)
            at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:79)
            at com.fasterxml.jackson.databind.ser.impl.IndexedListSerializer.serialize(IndexedListSerializer.java:18)
            at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:292)
            at com.fasterxml.jackson.databind.ObjectMapper._configAndWriteValue(ObjectMapper.java:3697)
            at com.fasterxml.jackson.databind.ObjectMapper.writeValueAsString(ObjectMapper.java:3073)
            at net.happyonroad.util.ParseUtils.toJSONString(ParseUtils.java:166)
    

    问题探究历程

    由于在老的体系中是正常的, 所以第一时间认为是Spring框架带来的, 然后在新的框架中并未像老的框架一样对Jackson做相应的配置, 因此复查两个体系的代码, 同时咨询老司机, 发现并未对这个东西做相关的配置, 表明此路不通.

    开始走第二个方式,探究Jackson是如何处理模型序列化这样的过程的

    具体方式是:

    在报错的代码堆栈上,打各种断点,通过渐渐理解Jackson在Java中是如何序列化模型的, 来找到这个问题的根原因

    Jackson的序列化机制

    他的建模思路其实非常简单, 当我们需要序列化一个Configuration模型, 我们对它的定义如下:

    public class Configuration  {
        private Properties options;
       
        public Properties getCredentials() {
            return options;
        }
    
        public void setOptions(Properties options) {
            this.options = options;
        }
    
    }
    

    那么对应的, Jackson在序列化这个机制的时候, 需要为这个对象构建一个JsonSerializer<T>, 因为我们并未对这个对象做过具体的配置, 他会使用默认的BeanSerializer来序列化Configuration这个对象

    当他通过BeanSerializer这个东西来构建这个对象的时候, 会获取这个对象的BasicBeanDescription, 简单来说, 他们扫描所有的getXXX方法,将这个作为一个具体的字段

    显然, 他扫到了我们有一个字段:
    名称是options, 类型是Properties

    然后, 他会为了这个类型构建一个JsonSerializer<T>, 由于他是个Map, 因此会使用一个MapSerializer来处理, 这个时候, 情况出现了:

    package com.fasterxml.jackson.databind.ser.std;
    
    import java.io.IOException;
    import java.lang.reflect.Type;
    import java.util.*;
    
    import com.fasterxml.jackson.annotation.JsonFormat;
    import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
    import com.fasterxml.jackson.annotation.JsonInclude;
    import com.fasterxml.jackson.core.*;
    import com.fasterxml.jackson.databind.*;
    import com.fasterxml.jackson.databind.annotation.JacksonStdImpl;
    import com.fasterxml.jackson.databind.introspect.AnnotatedMember;
    import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonFormatVisitorWrapper;
    import com.fasterxml.jackson.databind.jsonFormatVisitors.JsonMapFormatVisitor;
    import com.fasterxml.jackson.databind.jsontype.TypeSerializer;
    import com.fasterxml.jackson.databind.ser.ContainerSerializer;
    import com.fasterxml.jackson.databind.ser.ContextualSerializer;
    import com.fasterxml.jackson.databind.ser.PropertyFilter;
    import com.fasterxml.jackson.databind.ser.impl.PropertySerializerMap;
    import com.fasterxml.jackson.databind.type.TypeFactory;
    import com.fasterxml.jackson.databind.util.ArrayBuilders;
    
    /**
     * Standard serializer implementation for serializing {link java.util.Map} types.
     *<p>
     * Note: about the only configurable setting currently is ability to filter out
     * entries with specified names.
     */
    @JacksonStdImpl
    public class MapSerializer
        extends ContainerSerializer<Map<?,?>>
        implements ContextualSerializer {
    
        /**
         * Declared type of keys
         */
        protected final JavaType _keyType;
    
        /**
         * Declared type of contained values
         */
        protected final JavaType _valueType;
    }
    

    MapSerializer中会记录它的_valueType, 而Properties对应的_valueTypeString, 也就是说, 我们放在options这个Map中所有的Value都会用String的方式来序列化, 自然就会报错, 因为我放到options里面有一个Integer

    一开始觉得这个是非常不合理的, 以为Properties是继承与Hashtable<Object, Object>,一定是哪里有问题

    再进一步查询他是如何生成这个有问题的MapSerializer的时候, 进一步了解了Jackson的Type体系.如果你是一个Map,他会根据的Key和Value的类型中找具体的Serializer来处理, 但是看到他的
    TypeFactory之后, 发现真正的原因(line 1269, Jackson 2.8.10):

                // 19-Oct-2015, tatu: Bit messy, but we need to 'fix' java.util.Properties here...
                if (rawType == Properties.class) {
                    result = MapType.construct(rawType, bindings, superClass, superInterfaces,
                            CORE_TYPE_STRING, CORE_TYPE_STRING);
                }
    

    追溯了一下代码历史, 发现是这个链接加入了这个代码, 并于 2.6 版本生效, 我们老的项目用的是2.4这个版本, 距今至少有3年时间了, 对于这个问题, StackOverflow上也有相关的解释, 其中一个理由我比较认可:

    The problem you have is that you are misusing java.util.Properties: it is NOT a multi-level tree structure, but a simple String-to-String map. So while it is technically possibly to add non-String property values (partly since this class was added before Java generics, which made allowed better type safety), this should not be done. For nested structured, use java.util.Map or specific tree data structures.

    As to Properties, javadocs say for example:

    The Properties class represents a persistent set of properties.
    The Properties can be saved to a stream or loaded from a stream.
    Each key and its corresponding value in the property list is a string.
    

    If the store or save method is called on a "compromised" Properties
    object that contains a non-String key or value, the call will fail.
    Now: if and when you have such "compromised" Properties instance, your best bet with Jackson or Gson is to construct a java.util.Map (or perhaps older Hashtable), and serialize it. That should work without issues.

    总结

    总结一下这次查错的过程:

    1. 通过代码断点来理解上下文究极问题解决方式, 是一个工程师必备的技能
    2. 遇到问题, 首先要考虑版本的问题
    3. 使用原生库的一些类的时候, 不能滥用, 按照官方期望的方式来使用

    通过这次解决问题, 我也获得了以下知识点:

    1. Jackson在序列化机制上的核心模型
    2. Spring是如何和Jackson做关联的

    收获很大!

    带着问题来读代码, 是最有效的理解代码和代码运行机制的一种方式!

    相关文章

      网友评论

          本文标题:Java Jackson 序列化Properties的方式在2.

          本文链接:https://www.haomeiwen.com/subject/pgxqzxtx.html