解决 MongoDB 的 ObjectId 序列化问题

1,750 阅读3分钟

前言

MongoDBObjectId 是一个 12 字节的 BSON 类型数据(我们在可视化数据库工具中看到的是 24 位的 16 进制形式),其具体数据结构如下:

  • 4 个字节为时间戳(timestamp);
  • 然后的 3 个字节机器标识码(randomValue1);
  • 再之后的 2 个字节为进程 id (randomValue2);
  • 最后的 3 个字节是随机计数器值 (counter)。

讲完 ObjectId 的存储格式后,再来说下如何解决序列化问题,本文使用了基于 GsonJava 序列化工具包,本文使用的完整代码也已上传到 GitHub,下面就介绍具体的解决办法。

基础

由于接下来的解决办法需要自定义 ObjectId 的序列化类,因此需要从底层的具体存储格式开始:

Gson gson = new Gson();
TestEntity id = new TestEntity(new ObjectId("600a47a0076abd67f0d588f6"));
System.out.println(gson.fromJson(gson.toJson(id), Document.class));

// TestEntity 的类结构如下
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TestEntity {

    private ObjectId _id;
}

/**
 * output: 
 * Document{{_id=
 * 		{
 *			timestamp=1.611286432E9, 
 *			counter=1.399423E7, 
 *			randomValue1=486077.0, 
 *			randomValue2=26608.0
 *		}
 * }}
 */

从上面的代码和输出结果可以看出,如果为了插入数据在序列化实体时转换为 Document 类型,就会出现不和预期的结果,下面就结合前言中介绍的 ObjectId 底层存储结构来介绍 600a47a0 076abd 67f0 d588f6 这个十六进制字符串和 {timestamp=1.611286432E9, counter=1.399423E7, randomValue1=486077.0, randomValue2=26608.0} 的对应关系:

image-20210122160059028

可以看到 24 位的 16 进制字符串正好和对象里面的对应字段的值相等,下面就正式开始序列化的解决办法。

序列化问题解决

在前面介绍了本文使用了基于 GsonJava 序列化工具包,下面的解决方法也需要使用里面的 @JsonAdapter 这个注解,这个注解需要传入一个自定义的序列化解析类,这个自定义的类需要继承 TypeAdapter<T> 类,下面是具体的代码展示:

@Data
@NoArgsConstructor
@AllArgsConstructor
public class TestEntity {

    // 自定义一个名为 MongoObjectId 的类进行 ObjectId 的自定义序列化处理
    @JsonAdapter(MongoObjectId.class)
    private ObjectId _id;
}

下面是这个自定义序列化解析类的代码:

public class MongoObjectId extends TypeAdapter<ObjectId> {

    @Override
    public void write(JsonWriter out, ObjectId value) throws IOException {
        // 对于需要被序列化的 value
        // 如果为空就在 out 中写入空值
        // 否则写入 ObjectId 的 16 进制字符串形式
        // tips: ObjectId 类的 toString 方法被重写实现了返回 ObjectId 的 16 进制字符串形式
        if (value == null) {
            out.nullValue();
            return;
        }
        out.value(value.toString());
    }

    @Override
    public ObjectId read(JsonReader in) throws IOException {
        // in.beginObject() 代表读取的是一个对象
        // 即 {timestamp=1.611286432E9, counter=1.399423E7, randomValue1=486077.0, randomValue2=26608.0}
        in.beginObject();
        int i = 0;
        // 将接下来要读取的四个数据先保存在数组中
        long[] nums = new long[4];
        while (in.hasNext()) {
            // 先读取对象名
            in.nextName();
            // 然后将值存储到数组中
            nums[i++] = in.nextLong();
        }
        // in.endObject() 表示对象读取完毕
        in.endObject();
        if (i == 0) {
            return null;
        }
        // 将获取的数据转换为 16 进制的字符串形式用于获取 ObjectId 对象
        return new ObjectId(String.format("%08x%06x%04x%06x", nums[0], nums[2], nums[3], nums[1]));
    }

}

然后再使用上述的测试代码:

Gson gson = new Gson();
TestEntity id = new TestEntity(new ObjectId("600a47a0076abd67f0d588f6"));
System.out.println(gson.fromJson(gson.toJson(id), Document.class));

/**
 * 输出结果如下:
 * Document{{_id=600a47a0076abd67f0d588f6}}
 */

可以发现解决了 ObjectId 的序列化问题,之后只要是 ObjectId 类型的变量,加上 @JsonAdapter(MongoObjectId.class) 注解即可。

总结

本文简单介绍了 MongoDBObjectId 数据类型的底层存储格式,以及如何通过 Gson 中的一个注解解决序列化问题,希望能够对你有所帮助。