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


源码追踪

抛开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);
	}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值