难题:
突然接到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);
结合jackson转换linkedhashmap的原理,应该是没能正确识别到类型信息,即使传入了Son.class也不行.
只好一步一步的debug来查找问题了
打好断点,一步一步debug,直到走到这一步时,发现了猫腻. 在进行外层转换时,可以看到,rawType和typeToRead是可以正常识别到为POJO.
继续往下走,到内部转换的时候,内容是这样的
rawType和typeToRead都是Map类型!
这个bson是mongo db数据库存储对象的类型,从我们查询对象还是bson的时候,就不能识别到对象类型了. 查了一下数据库,发现数据库长这样
没有看到_class这个属性,联系同事问了一下,前几天的更新中去除掉了数据库的_class属性.
结合刚才的debug过程,我有理由怀疑,是这个_class的缺失,导致查询时无法直到数据库中存储对象的具体类型.
于是乎我将同事的修改还原,再次测试,这次结果正常了...
总结
使用Object作为对象的属性值时,又去除了mongo db用于记录对象类型的_class属性.
导致mongo db查询到的对象无法反序列化为指定对象.
结合到实际操作,推荐不去除mongo db中的_ class属性,避免这种坑爹的现象发生.