问题描述
从老的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
对应的_valueType
是String
, 也就是说, 我们放在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.
总结
总结一下这次查错的过程:
- 通过代码断点来理解上下文的究极问题解决方式, 是一个工程师必备的技能
- 遇到问题, 首先要考虑版本的问题
- 使用原生库的一些类的时候, 不能滥用, 按照官方期望的方式来使用
通过这次解决问题, 我也获得了以下知识点:
- Jackson在序列化机制上的核心模型
- Spring是如何和Jackson做关联的
收获很大!
带着问题来读代码, 是最有效的理解代码和代码运行机制的一种方式!