一个反序列化问题引发的思考

项目场景:

两个团队合作,A团队和B团队合作,A团队通过kafka发送了一段消息是JSON字符串,B团队接收到这段JSON字符串之后反序列化后,然后进行下一步的数据处理


问题描述

B团队发送过来的JSON字符串,经过反序列化后,发现某个字段为空,但是消息体里是有该数据的。
此处为模拟场景,数据均是示例

@Data
static class MyTest {
    private Long updateTime;
    private Long uTime;
}

A团队发送过来的JSON消息是

{"updateTime":1666852055159,"uTime":1666852055159}

我们把这个字符串反序列化成MyTest对象之后,再序列化之后,得到了

{"updateTime":1666852055159}

debug一下代码,可以发现,uTime字段没有设置进去
一个反序列化问题引发的思考_第1张图片

原因分析:

因为参与过A团队的code review,印象中对方用的是Gson进行对象的JSON序列化,然后发送到MQ;而我们接收到消息之后是使用Jackson进行JSON反序列化。所以第一反应,就是两边的JSON序列化和反序列化使用组件不同导致。
于是我们针对同一个对象,分别进行Gson的JSON序列化和Jackson的序列化

public static void main(String[] args) {
        long time = System.currentTimeMillis();
        MyTest myTest = new MyTest();
        myTest.setUTime(time);
        myTest.setUpdateTime(time);
        Gson gson = new Gson();
        String json = gson.toJson(myTest);
        System.out.println("gson:" + json);
        System.out.println("jackson:" + JsonTool.writeToString(myTest));
    }

输出结果如下:

gson:{"updateTime":1666854574241,"uTime":1666854574241}
jackson:{"updateTime":1666854574241,"utime":1666854574241}

可以发现,Gson将uTime序列化还是uTime,但是Jackson将uTime序列化成utime了,所以在反序列化的时候,Jackson无法将JSON字符串里的uTime的值绑定到uTime字段上。

后来通过查看源码发现,默认情况下,Jackson生成JSON的key的逻辑

1. 把所有field找到,作为key
2. 把所有set方法后面的字符串基于规则X生成key
3. 把所有get方法后面的字符串基于规则X生成key
4. 所有没有set和get方法的key都remove掉

规则X的逻辑:

第一个字母强制小写,从第二个字母开始找小写字母,如果是大写,就改为小写,如果是小写就把剩余的字母拼接到已有的部分key上。
譬如setUTime方法,去掉set只剩下UTime,U强制小写,有了u;继续找下一个字母,T,不是小写,改为小写,有了ut,继续找下一个字母i,是小写,就把剩余的 ime 一起拼接到已有的 ut 后面,于是有了utime。

而原本字段uTime,推测不到set方法和get方法,导致被remove掉了,setUTime和getUTime和新生成的key,“utime”绑定了。
规则X的逻辑见代码 BeanUtil#legacyManglePropertyName

protected static String legacyManglePropertyName(final String basename, final int offset)
{
  final int end = basename.length();
  if (end == offset) { // empty name, nope
    return null;
  }
  // next check: is the first character upper case? If not, return as is
  char c = basename.charAt(offset);
  char d = Character.toLowerCase(c);

  if (c == d) {
    return basename.substring(offset);
  }
  // otherwise, lower case initial chars. Common case first, just one char
  StringBuilder sb = new StringBuilder(end - offset);
  sb.append(d);
  int i = offset+1;
  // 注意此处逻辑,从第二个字母开始遍历,找到第一个小写字母,将其整体拼接;如果是大写字母,就拼接其小写字母
  // 譬如 setUTime,从 T 开始,由于T不是小写字母,如果拼接t,后面的i是小写字母,就把i到结尾整体拼接,得到time
  for (; i < end; ++i) {
    c = basename.charAt(i);
    d = Character.toLowerCase(c);
    if (c == d) {
      sb.append(basename, i, end);
      break;
    }
    sb.append(d);
  }
  return sb.toString();
}

而Gson默认的就是使用field的key。


解决方案:

以下三种方法都可以,具体场景具体选择即可

  1. 不复用,或者重写A团队提供的jar包,在uTime字段上增加注解@JsonProperty
  2. A团队和B团队使用同一种JSON序列化组件
  3. A团队在提供的jar里包含自己的序列化组件

你可能感兴趣的:(工作中遇到的问题,json,java)