Java Jackson 序列化Properties的方式在2.4和2.8版本的区别

问题描述

从老的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做关联的

收获很大!

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,457评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,837评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,696评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,183评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,057评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,105评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,520评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,211评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,482评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,574评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,353评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,897评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,174评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,489评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,683评论 2 335

推荐阅读更多精彩内容