因为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