记一次mongoDB的_id属性造成的bug

1,221 阅读6分钟

难题:

突然接到bug报告,说dev环境接口返回的数据id属性变成了对象.我还想,这么奇怪的吗,长啥样啊.

随后看了截图,这也太奇怪了吧.

简化示意如下

{
  "code": 0,
  "msg": "操作成功",
  "data": {
    "id": "61a***********",
    "extend": {
      "_id": {
        "timestamp": 1638595732,
        "date": 1638595732000
      },
      "Obj": {
        "_id": {
          "timestamp": 1638595732,
          "date": 1638595732000
        }
      }
    },
    "anotherObj": {
      "id": "345******"
    }
  }
}

奇怪点

  • data包裹的对象中,与extend同级的id显示是正常的,extend对象自己的id展示为_id的对象了,还有两个属性.
  • another中的id显示也是正常的.
  • 目前看出来只有这一个接口是返回嵌套对象 从mongo db查出来反序列化的部分数据不正常.其他接口不管嵌套几层,多少对象,id显示均是正常的.
  • extend中向下嵌套的对象id属性也是对象形式
  • 除了id变为对象之外,其他地方看起来是正确的

先从我成功解决问题的方法来讲讲思路

我首先检查了是不是前不久换的注解的锅

@Id + @Field("id") 变为@MongoId(targetType = FieldType.OBJECT_ID)

前不久升级了公司用的spring版本,从2.0.3RELEASE到2.3.12RELEASE.

更改了一些明显报错和其他问题后,编译成功并上test进行测试.

过了不久看到邮件报警说很多错误.都是诸如用户不存在的错误.且都是mongo db查询数据库的直接报错.

经过排查,查询的id是正确的,数据库有相应的数据.可就是查不出来. 又是直接用id,感觉问题会出在id上,找了几个类看看属性id上的注解.

发现好多类为了使用pojo中非 'id'的属性作为mongo db中的object.

使用了 @Id + @Filed("id")的组合注解

@Id
@Filed("id")
public String appId;

尝试去除@Field注解后,可以查询并返回数据,但是appID为null;

经过查找资料,找到了@MongoId(targetType = FieldType.OBJECT_ID)注解,效果相当于前两者之和.

更换注解之后,可查询到并正常返回了结果.

转念一想,如果果真是这个注解导致的id解析异常,为啥其他接口返回又是正常的呢.所以应该不是这个问题

查看接口异常返回对象的结构

该对象结构简化如下

@MongoId(targetType = FieldType.OBJECT_ID)
public String id;

public Object extent;

public anotherObj obj;

anotherObj 类型的对象解析结果是正确的的,那么结果可能出在Object上.

是不是Object的对象缺少对象信息,查询到的结果mongo driver无法转换为正确的对象呢?(前提是版本升级前这个功能是正常使用的,并没有出现这种情况)

为此我先做了个实验,写了个简单的测试类,往数据库中写入嵌套对象.

POJO对象

public class POJO {
  @MongoId(FieldType.OBJECT_ID)
  public String id;
  public String name;
  Object item;
}

Son对象

public class Son {
  public int sonFeaturedProperty;
}

查询数据库,pojo表中内容为

{
  "_id": ObjectId("61ab32ae135add26607f6548"),
  "item": {
    "_id": ObjectId("61ab32ae135add26607f6547"),
    "sonFeaturedProperty": 0,
    "age": "23",
    "sex": "female",
  }
}

son表中内容为

{
  "_id": ObjectId("61ab32ae135add26607f6547"),
  "sonFeaturedProperty": 0,
  "age": "23",
  "sex": "female"
}

然后使用mongo repository和template两种方式使用id查询pojo对象

    POJO pojo=pojoRepo.findById(id).get();
    POJO pojo2=this.mongoTemplate.findById(id,POJO.class);

打印出结果

得到

{
  "id": "61ab32ae135add26607f6548",
  "name": null,
  "item": {
    "id": "61ab32ae135add26607f6547",
    "age": "23",
    "sex": "female",
    "sonFeaturedProperty": 0
  }
}

{
  "id": "61ab32ae135add26607f6548",
  "name": null,
  "item": {
    "id": "61ab32ae135add26607f6547",
    "age": "23",
    "sex": "female",
    "sonFeaturedProperty": 0
  }
}

id是正常显示的,并没有异常. 说明直接存储Object的对象并不会丢失对象信息,从数据库查询并返回时可以正确反序列化为相应的对象;

那个_id对象的内容从何而来呢? 原来的那个string的id又去了哪里? 那会不会是jackson转换的问题呢?

_id对象的内容从何而来呢?

通过查看 ObjectId 的构造,发现其包含了几个基本属性

public final class ObjectId {
  //忽略其他属性
  private final int timestamp;
  private final int counter;
  private final int randomValue1;
  private final short randomValue2;
}

ObjectId是一个12字节的 BSON 类型字符串.按照字节顺序,一次代表:

4字节:UNIX时间戳

3字节:表示运行MongoDB的机器

2字节:表示生成此_id的进程

3字节:由一个随机数开始的计数器生成的值

由此可以猜测出,_id对象中的两个属性,timestamp和date大概就是来自ObjectId中的timestamp.

原来的那个string的id又去了哪里?

正常情况下_id应该是一个16进制的12位字符串,同时这也是我们所期望的返回结果.这里可以看出就是ObjectId在转换过程中没有正确的转为java中的对象.

所以string的id不见,取而代之的是_id的对象.

那会不会是jackson转换的问题呢?

因为知道这个对象的类型,可不可以通过jackson将其转换为一个java对象呢?

将查出来的pojo对象的item拿出来,使用jackson转换成一个Son对象.发现id属性变为了null,其他属性被转了过来.

POJO mongoTemplateById=this.mongoTemplate.findById(id,POJO.class);
        Son son=objectMapper.convertValue(mongoTemplateById.getItem(),Son.class);

img.png

img_1.png

结合jackson转换linkedhashmap的原理,应该是没能正确识别到类型信息,即使传入了Son.class也不行.

只好一步一步的debug来查找问题了

打好断点,一步一步debug,直到走到这一步时,发现了猫腻. 在进行外层转换时,可以看到,rawType和typeToRead是可以正常识别到为POJO.

img_2.png

继续往下走,到内部转换的时候,内容是这样的

img_3.png

rawType和typeToRead都是Map类型!

这个bson是mongo db数据库存储对象的类型,从我们查询对象还是bson的时候,就不能识别到对象类型了. 查了一下数据库,发现数据库长这样

img_4.png 没有看到_class这个属性,联系同事问了一下,前几天的更新中去除掉了数据库的_class属性.

结合刚才的debug过程,我有理由怀疑,是这个_class的缺失,导致查询时无法直到数据库中存储对象的具体类型.

于是乎我将同事的修改还原,再次测试,这次结果正常了...

总结

使用Object作为对象的属性值时,又去除了mongo db用于记录对象类型的_class属性.

导致mongo db查询到的对象无法反序列化为指定对象.

结合到实际操作,推荐不去除mongo db中的_ class属性,避免这种坑爹的现象发生.