springboot Jackson序列化Properties異常解析
問題描述
在升級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
Jackson序列化原始碼分析
拋開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);
}