问题描述
在升级SpringBoot至2.x(2.0.3.RELEASE
)版本时,一个简单的rest请求抛出了一个异常:
Failed to write HTTP message: org.springframework.http.converter.HttpMessageNotWritableException: Could not write JSON: java.lang.Integer cannot be cast to java.lang.String; nested exception is com.fasterxml.jackson.databind.JsonMappingException: java.lang.Integer cannot be cast to java.lang.String (through reference chain: java.util.HashMap["props"]->java.util.Properties["age"])
,
根据异常信息提示,定位到方法代码(由于业务代码比较复杂,将代码简化
)如下:
@RequestMapping(value = "/wtf")
@ResponseBody
public Map wtf() {
Properties properties = new Properties();
properties.put("username","trump");
properties.put("age_string","72"); //正常
properties.put("age",72);//出错
Map map = Maps.newHashMap();
map.put("props", properties);
return map;
}
,分析代码是在序列化Properties时抛出的异常,进一步将上述代码简化如下:
@RequestMapping(value = "/wtf1")
@ResponseBody
public Properties wtf1() { //异常....
Properties properties = new Properties();
properties.put("username","trump");
properties.put("age_string","72");
properties.put("age",72);
return properties;
}
@RequestMapping(value = "/wtf2")
@ResponseBody
public Map wtf2() {//正常
Properties properties = new Properties();
properties.put("username","trump");
properties.put("age_string","72");
properties.put("age",72);
return properties;
}
发现wtf1
会抛出异常,而wtf2
正常。
经查有不少人遇到了这个问题,给出问题的原因是:SpringBoot 2.x
中的Jackson
新版本序列化Properties
时带来的问题,但是并未找到具体的可行性方法。此时Jackson的版本为2.9.x
源码追踪
抛开SpringBoot,写一个简单的main方法测试:
public static void main(String[] args) throws JsonProcessingException {
Properties properties = new Properties();
properties.put("age",72);
properties.put("age_str","72");
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
ObjectWriter writer = mapper.writer();
String json = writer.writeValueAsString(properties);
System.out.println(json);
}
依旧报错:
Jackson序列化的流程简单描述可以如下:
- 1.创建ObjectWriter()对象
-
- 使用
SerializerProvider
查找匹配序列化对象value
的clazz
对应的JsonSerializer
类型
- 使用
- 2.1 如果找到匹配的
JsonSerializer
则跳转到5
- 2.2 如果没有找到匹配的
JsonSerializer
,则到3 - 3 使用
TypeFactory
寻找与clazz
对应的JavaType
- 3.1 如果找到
JavaType
则跳转到4
- 3.2 如果未找到,则通过
TypeFactory
创建一个新的JavaType
- 3.1 如果找到
- 4 通过
JavaType
创建与之对应的JsonSerializer
- 5 调用
serialize.serialize(T value, JsonGenerator gen, SerializerProvider serializers)
方法进行序列化。
具体的处理流程图如下:
经过源码分析,定位到com.fasterxml.jackson.databind.type.TypeFactory#_fromClass()方法执行过程中
,有如下代码:
//com.fasterxml.jackson.databind.type.TypeFactory
protected JavaType _fromClass(ClassStack context, Class<?> rawType, TypeBindings bindings){
JavaType superClass;
JavaType[] superInterfaces;
//略....
if (rawType.isInterface()) {
superClass = null;
superInterfaces = _resolveSuperInterfaces(context, rawType, bindings);
} else {
// Note: even Enums can implement interfaces, so cannot drop those
superClass = _resolveSuperClass(context, rawType, bindings);
superInterfaces = _resolveSuperInterfaces(context, rawType, bindings);
}
//第1310行
// 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);
}
//略....
return result;
}
这里可以看出来19-Oct-2015之后,Jackson在这里做了特殊处理,默认properties的key和value类型都是String类型,都是用StringSerializer
,而`StringSerializer#serialize()'代码如下:
//com.fasterxml.jackson.databind.ser.std.StringSerializer
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeString((String) value);
}
问题就出在这里了,将一个int强转换为string执行到这里就出错了。
那么jackson会为什么会这样做呢,为什么针对Properties
会多此一举,增加一段特殊的判断呢,我们查看Properties文件看到有如下的一段注释:
原来如此:Properties默认的key和value都是String类型,是我们使用Properties方式不对。
解决方案
方案1
在我们给Properties设置值的时候,提前将value转换为String
方案2
手动指定Properties对应的JavaType和JsonSerializer
,具体代码如下:
public static void main(String[] args) throws JsonProcessingException {
Properties properties = new Properties();
properties.put("age",18);
ObjectMapper mapper = new ObjectMapper();
mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
//新建propertiesType,并添加至typeFactory的_typeCache缓存中
MapType propertiesType = MapType.construct(Properties.class, SimpleType.constructUnsafe(String.class), SimpleType.constructUnsafe(Object.class));
LRUMap<Object, JavaType> cache = new LRUMap<Object, JavaType>(16, 200);
cache.put(Properties.class, propertiesType);
//指定typeFactory
TypeFactory typeFactory = TypeFactory.defaultInstance().withCache(cache);
mapper.setTypeFactory(typeFactory);
ObjectWriter writer = mapper.writer();
String json = writer.writeValueAsString(properties);
System.out.println(json);
}