问题背景
先说一下问题背景:整个项目是一个大的分布式系统,由十几个子系统组成,本人负责其中两个系统。分布式服务框架采用了公司封装好的jar包,当然还有一些其他的底层框架。由于某些原因,公司更换了底层分布式服务框架和一些其他的框架,其中分布式服务框架主要是更改了一些包名和类名,其他基本没变。在更换底层框架之前,系统已经经过了两个版本的迭代,并且已在生产环境上线。
在更换底层框架之后,系统跑起来没问题,经过简单自测,也没有发现问题。因为原系统本身是上过线的,所以就觉得既然改造之后能够正常编译和运行,因此肯定是没问题的,就提交给测试了。结果在集成测试中,发现其中一个系统的一个重要接口的参数校验不通过,然后整个集成测试流程就卡住了。
解决过程
于是赶紧排除问题。
系统中有多个接口,虽然每个接口的功能不一样,但是接收参数的方式是一样的,其他接口都正常,而这个接口的参数就校验不过,首先想到的是参数不符合要求,但是仔细看了一下发现请求参数并没有任何问题。
通过比较正常接口的参数模型和绑定不上的参数模型,发现后者中有Date类型的字段,且该字段上有json序列化和反序列化注解。根据经验判断(日期的格式转换容易出问题),应该就是这个字段的原因。经过测试,发现的确如此,去掉该字段后参数就能正常绑定,显然问题原因是日期字段格式转换有问题。进一步发现,日期字段上面的json序列化和反序列化所使用的类在底层分布式服务框架和系统应用工具包中都有,而这里采用的是系统应用工具包里面的工具类。两者序列化/反序列化的方法一样,但是采用的依赖包不一样,分布式服务框架中采用的是jackson 1.x版本的codehaus包,而工具类用的是2.0版本的fastxml包。
考虑到是因为两者不兼容导致日期解析不对,但又不能直接使用底层框架中的序列化/反序列化类,因为项目中还使用到了很多工具类中有而底层框架中没有的其他方法。为了保持版本一致,将所有的依赖fastxml包的工具类的依赖包都替换成1.x版本的codehaus包,除了JsonUtil工具类之外。为什么这个类不改呢?因为依赖包的主版本号都不一致,很多api也不一样了,如果要改动这个类的话,就要修改很多方法,同时系统中使用到JsonUtil工具类的地方很多,都要做相应的修改,这个工作量还是不小的,再考虑到这个类不涉及日期解析(实际上toString方法暗含,所以留了坑),应该问题不大。
把相应的依赖包都替换之后,经过测试发现参数可以正常校验,正常绑定了,大松一口气,赶紧提交给测试。但是好景不长,第二天发现又出现问题了。原因就在没有修改的JsonUtil工具类上,该类的toString方法将对象转换成json字符串,然而凡是对象中含有日期的,转换成字符串时只有年月日,没有时分秒,即yyyy-MM-dd的格式,后面的时分秒丢失了。
没办法,只好把JsonUtil工具类一并修改了,这个工作量有点大,在花了一个下午和晚上加班之后,终于把要改的地方都改完了。再次提交测试。测试了一天之后又出现了新的问题。跟其他系统交互时,有个时间字段应该是yyyy-MM-dd hh:mm:ss类型,而实际中却是时间的long型,导致其他系统接收到该参数后解析异常。
一气之下,将代码全部回滚。这次不管测试怎么催,都没有急着动手,而是静下心来考虑。突然灵感来了,既然接口参数只是一个数据转换对象,为什么非得把dateTime设置为Date类型呢?上游系统传过来的是一个json字符串,我用String类型接收,然后在需要的时候通过日期格式化工具格式化一下不就行了嘛。然后简单的将日期字段的类型改为String类型,并在需要的地方转换成日期格式,问题完美解决。
问题根源探究
但是问题解决了还不够,知其然还要知其所以然。既然原来参数能够正常绑定为什么改造后不行呢?在所有的过滤器和拦截器中打上断点进行debug,发现进来的请求参数都是正常的,一直执行到springmvc的invokeHandleMethod方法的RequestContextUtils.getInputFlashMap(request)这里的时候,得到的请求参数变成了null。但是springmvc是不会有问题的,为什么到这里变成了空值呢?问题似乎走进了死胡同。然后我发现一开始从过滤器进来的request中就有一个属性是objectMapper,该属性一看就是为了把json字符串转换成对应参数对象中的属性字段的。而这时候我才想到,系统的参数绑定注解并不是用的springmvc中的requestBody,而是自定义的参数绑定注解。再debug请求参数解析器,发现日期经过objectMapper转换的时候产生了异常:not a valid representation (error: Can not parse date “xxxxxxxx xx:xx:xx”: not compatible with any of standard forms (“yyyy-MM-dd’T’HH:mm:ss.SSSZ”, “yyyy-MM-dd’T’HH:mm:ss.SSS’Z’”, “EEE, dd MMM yyyy HH:mm:ss zzz”, “yyyy-MM-dd”))。
终于真相大白,原来是自定义参数解析器中的objectMapper用错了。之前参数解析器用的是fastxml的objectMapper的,而更换之后则用的是codehaus的objectMapper。这样一个看似不起眼的问题,导致出现问题的这两天被各种催促,心情烦躁无比。最后问题解决时大松一口气,搞清楚问题的原因后则更有成就感。下面针对出现的问题进行分析。
首先是第一个问题。日期字段用fastxml包中的方法进行反序列化,以codehaus为基础的解析器的objectMapper无法解析。原因是此时对象映射器做反序列化时会调用默认的deserialize方法(没有指定日期格式,则会反序列化异常),而根本不会调用日期字段上注解的反序列化方法。因此导致请求参数反序列化出现异常,无法正确绑定到接口方法的参数接收对象上。也就是说,反序列化注解和使用的方法所依赖的包必须和对象映射器所依赖的包保持一致,反序列化重写的方法才能被使用。下面看一个例子:
String json = "{\"name\":\"haha\",\"id\":1,\"time\":\"2017-08-25 17:08:05\"}";
ObjectMapper objMapper = new ObjectMapper();
try {
Student acc = objMapper.readValue(json, Student.class);
System.out.println(acc);
} catch (JsonParseException e) {
e.printStackTrace();
} catch (JsonMappingException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
这里对象映射器使用的是codehaus包。Student类中在time字段上使用注解:
@JsonDeserialize(using = JacksonDateTimeParse.class)
这里的注解使用的是fastxml包,反序列化类如下:
public class JacksonDateTimeParse extends JsonDeserializer<Date> {
private static final Logger LOG = LoggerFactory.getLogger(JacksonDateTimeParse.class);
@Override
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
String date = jsonParser.getText();
if (date == null || date.trim().length() == 0) {
return null;
}
try {
return format.parse(date);
} catch (Exception e) {
LOG.error("日期解析出错",e);
}
return null;
}
}
类依赖的包也是fastxml的包。对象映射器和反序列化的包不一致,导致反序列化重写的方法根本就不会被调用,将json字符串转换为对象时会报错(因为日期格式不对)。如果将两者依赖的包进行统一,不管是codehaus还是fastxml,只要是一致的就没有问题。
然后再看第二个问题。在替换反序列化方法所在类的依赖包之后,反序列化注解和对象映射器依赖的包保持一致(都为codehaus),虽然可以正确绑定,但是在日期属性输出为字符串的时候,丢掉了年月日后面的时分秒。看一下JsonUtil工具类(该类依赖fastxml)中的toString方法。
public static String toString(Object obj) {
return toString(obj, true);
}
public static String toString(Object obj, boolean includeNull) {
if (null == obj) {
return null;
}
if (obj instanceof String) {
return (String) obj;
}
try {
return getMapper(includeNull).writeValueAsString(obj);
} catch (JsonGenerationException e) {
LOG.error("JsonGenerationException error", e);
} catch (JsonMappingException e) {
LOG.error("JsonMappingException error", e);
} catch (IOException e) {
LOG.error("IOException error", e);
}
return null;
}
使用的getMapper(includeNull).writeValueAsString(obj)将obj输出为字符串。而getMapper是这样的:
private static ObjectMapper getMapper(boolean serializeNull) {
ThreadLocal<ObjectMapper> tl = serializeNull ? INCLUDE_NULL_MAPPER : NOT_INCLUDE_NULL_MAPPER;
if (null == tl.get()) {
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd"));
if (!serializeNull) {
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.disable(SerializationFeature.WRITE_NULL_MAP_VALUES);
}
tl.set(mapper);
}
return tl.get();
}
该方法从ThreadLocal容器中获取对象映射器mapper,然后设置mapper的日期格式为yyyy-MM-dd。这样一来,在序列化的时候,就会调用相应依赖包中的serilize方法,将日期字段序列化为yyyy-MM-dd格式的字符串,而丢掉了后面的时分秒。
再看第三个问题。将JsonUtil依赖的包也改为codehaus,再调用修改后的toString方法将对象输出为json字符串,修改后toString方法中的获取到的对象映射器objectMapper没有设置日期格式,日期字段正常输出。原因是因为Date字段上这个注解:
@JsonSerialize(using = JacksonDateTimeFormat.class)
在JacksonDateTimeFormat类中重写了序列化方法
public class JacksonDateTimeFormat extends JsonSerializer<Date> {
@Override
public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
String formattedDate = "";
if(value != null) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
formattedDate = formatter.format(value);
}
jgen.writeString(formattedDate);
}
}
由于此时注解和序列化方法的依赖包与对象映射器objectMapper的依赖包一致,因此通过objectMapper进行序列化时该序列化方法会被调用,日期格式被设置为了yyyy-MM-dd HH:mm:ss,正常输出。那为什么有的日期字段又会以long型的时间戳进行输出呢?这是因为日期字段上没有加上序列化注解,默认以long型时间戳格式输出。
总结
至此本次出现的问题都可以解释清楚了。当出现问题时,由于时间紧迫,很多东西都没有仔细考虑清楚就急急忙忙地去改,由于不清楚具体的原因导致改了这个问题出现那个问题,头疼医头,脚疼医脚的做法不可取。最好的处理方式还是要找到确定的原因,才能避免连锁反应。回过头来看问题,最好的解决方案仍然是将接收参数对象的日期字段改为String类型。这样就不用管日期的序列化反序列化问题了。然后第二种方法就是仍然采用原来的参数绑定解析器,用fastxml中的objectMapper,日期字段的序列化和反序列化也照原来的方式不变。最差的方法就是将所有fastxml包替换成codehaus包,和底层框架的依赖包统一。但这是不好的,一是因为修改代码工作量大,而是codehaus版本是老的版本,不推荐使用。