如何解决读取BsonUndefined的问题

2022-11-08 12:49:42 浏览数 (1)

因为MongoDB数据中有的字段值为'undefined',程序程序访问到这个数据时会抛出如下类似的exception:

代码语言:javascript复制
org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type org.bson.BsonUndefined to type XXXXX.

我们可以通过以下几步解决这个问题:

1)我们首先需要分析是什么情况导致数据中存在undefined值。

从BSON的规范 https://bsonspec.org/spec.html 看,undefined已经是depricated。数据库中出现undefined的多半说明程序有问题,所以需要找出是在哪里、什么情况下向MongoDB写入了undefined value并进行修改。

2)如果这个问题仅存在于开发/测试环境,而不是遗留数据导致。

那么修正代码出错的地方并清理数据库中的脏数据就可以了。

3)如果这种脏数据是遗留数据并且在生产环境也是存在的。

那么我们可以通过添加一个converter类

代码语言:javascript复制
BsonUndefinedToNullObjectConverterFactory implements ConverterFactory<BsonUndefined, Object>

把undefined 转为任意类型对象的null,就可以避免如上的Exception。代码如stackoverflow 上的[这篇帖子](https://stackoverflow.com/questions/37066690/no-converter-found-capable-of-converting-from-type-org-bson-bsonundefined)。

为了把Converter 传给mongoTemplate,我们需要定义一个MongoCustomConversions Bean。为什么需要这样的Bean,可以参考 AbstractMongoClientConfiguration的源代码。另外,因为AbstractMongoClientConfiguration中已经定义了一个MongoCustomConversions bean,我们就需要给自己的Bean加上@Primary,以便让spring-data-mongo优先使用我们设置了自定义converter的 MongoCustomConversions bean。

如果你的spring application还是通过XML方式进行beans定义与组装的,那么你就不能通过定义一个ConverterFactory来方便地把undefined 转为__任意类型__对象的null了。这是因为XML不支持类型化参数。这时,只能把converter一个个地定义出来。XML的组装大致如下:

代码语言:javascript复制
<mongo:mapping-converter id="mappingConverter" >
    <mongo:custom-converters>
        <mongo:converter>
            <bean class="your.package.UndefinedToLongNullReadConverter"/>
        </mongo:converter>
        <mongo:converter>
            <bean class="your.package.UndefinedToStringNullReadConverter"/>
        </mongo:converter>
    </mongo:custom-converters>
</mongo:mapping-converter>

4)到这里即使数据库里有脏数据,程序也能‘愉快’地运行了。问题似乎已经被彻底解决了,其实没有。

因为生产环境的脏数据还没有被清理,我们现在只是容忍了脏数据的存在。在当前微服务架构下,这样的数据可能会被多个不同的微服务访问到,这就意味着这些微服务都要使用如上所述的一个converter才能避免exception。我们有必要发现这些脏数据存在的位置,并进行清理。

通过在converter 返回 o -> null 之前,执行下面的代码就可以通过log看到是哪个DAO触发了这个转换,进而可以分析出哪个collection存在脏数据。如果我们清理了这个collection的所有脏数据之后这种undefined脏数据还是会产生出来,那么我们就应该好好review一下之前的代码是哪里有问题并进行修改了。

代码语言:javascript复制
StackTraceElement[] causes = Thread.currentThread().getStackTrace();
for(StackTraceElement st : causes){
    if (st.toString().indexOf("YOUR_DAO_PACKAGE") >= 0) {
        log.warn(st.toString());
    } else {
        log.info(st.toString());
    }
}

如果是使用JDK9及以上,那么可以使用StackWalker避免getStackTrace()的性能损耗。可以参考 https://stackoverflow.com/questions/2347828/how-expensive-is-thread-getstacktrace。

写了个Demo来复现并解决这个问题,代码可参考[这里](https://github.com/dhyuan/demo_projects/tree/master/mongo_testcontainer)。


Reference: https://docs.spring.io/spring-data/mongodb/docs/current/reference/html/#mapping-chapter

0 人点赞