1. 程式人生 > >springboot Jackson序列化Properties異常解析

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()物件
    1. 使用SerializerProvider查詢匹配序列化物件valueclazz對應的JsonSerializer型別
  • 2.1 如果找到匹配的JsonSerializer則跳轉到 5
  • 2.2 如果沒有找到匹配的JsonSerializer,則到3
  • 3 使用TypeFactory尋找與clazz對應的JavaType
    • 3.1 如果找到JavaType則跳轉到 4
    • 3.2 如果未找到,則通過TypeFactory建立一個新的JavaType
  • 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);
	}