使用Mock对象更新MongoDB中List字段时出现StackOverflowError
嘿,这个问题我之前在维护老项目时刚好遇到过类似的情况,咱们来拆解一下背后的原因和可行的解决办法:
你遇到的StackOverflowError本质是Spring Data MongoDB 1.9.1版本对动态代理对象(比如Mockito生成的Mock实例)的序列化逻辑不完善导致的无限递归。
Mockito生成的Mock对象是基于Java动态代理创建的子类,当Spring Data MongoDB的转换器尝试将这些Mock对象转换为MongoDB能识别的文档格式时,会通过反射不断去遍历对象的类结构、父类信息、代理相关的属性,而这个过程在旧版本的转换器里没有做边界处理,最终触发了栈溢出。
反观真实的Foo实例,它是普通的POJO类,转换器可以正常遍历属性完成序列化,所以不会出问题。
1. 测试场景替换为真实Foo实例(最推荐)
测试DAO的更新逻辑时,没必要用Mock对象来模拟实体——毕竟你要验证的是DAO能不能正确把数据写入MongoDB,用真实的Foo实例才更贴合实际场景。比如把测试代码改成:
@Test public void givenPersistedBar_whenUpdateFoos_thenUpdatesThem() { // 创建真实的Foo实例,哪怕只填充必要属性 List<Foo> foos = Collections.nCopies(10, new Foo()); barDao.updateFoos(ID, foos); }
这个方法最简单,也符合单元测试的设计初衷,完全可以规避代理对象的序列化问题。
2. 自定义Mongo序列化转换器(特殊场景适用)
如果因为某些特殊需求必须在测试中使用Mock对象,可以自定义一个Converter来处理Foo类型的转换,手动拦截Mock对象并提取需要持久化的属性:
public class FooMockConverter implements Converter<Foo, DBObject> { @Override public DBObject convert(Foo source) { DBObject dbObject = new BasicDBObject(); // 判断是否是Mockito代理对象 if (source instanceof MockitoProxy) { // 用反射提取内部属性(需要依赖Mockito的Whitebox工具) dbObject.put("id", Whitebox.getInternalState(source, "id")); // 按需添加其他需要持久化的属性 } else { // 正常处理真实Foo实例 dbObject.put("id", source.getId()); // ...其他属性映射 } return dbObject; } }
然后把这个转换器注册到你的MongoTemplate中:
// 假设你已经有mongoDbFactory和mappingContext实例 MappingMongoConverter converter = new MappingMongoConverter(mongoDbFactory, mappingContext); converter.setCustomConversions(new CustomConversions(Arrays.asList(new FooMockConverter()))); MongoTemplate mongoTemplate = new MongoTemplate(mongoDbFactory, converter);
不过这个方法比较繁琐,而且测试场景用Mock对象更新数据库本身就不太合理,只建议在极端情况下使用。
3. 升级Spring Data MongoDB版本(长期解决方案)
你当前使用的spring-data-mongodb:1.9.1是2016年的旧版本,后续的1.10.x及更高版本(比如2.x系列)对动态代理对象的序列化逻辑做了优化,能避免这种无限递归的问题。
不过要注意版本兼容性:
- Spring 4.5.2可以兼容
spring-data-mongodb:1.10.x(最高到1.10.19.RELEASE) - 如果要升级到2.x系列,需要把Spring版本升级到5.x及以上
升级依赖能从根源上解决这类老版本的兼容性问题,同时还能获得更多新特性和Bug修复。
内容的提问来源于stack exchange,提问作者Jeep87c

