如何解决 No converter found capable of converting from type org.bson.BsonUndefined 问题

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

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 类

BsonUndefinedToNullObjectConverterFactory implements ConverterFactory<BsonUndefined, Object>

把 undefined 转为任意类型对象的 null,就可以避免如上的 Exception。代码如 stackoverflow 上的这篇帖子。把 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 的组装大致如下:

<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 一下之前的代码是哪里有问题并进行修改了。

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 来复现并解决这个问题,代码可参考这里

Reference:

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