You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用spring-data-mongodb存储含Path类型字段的实体出现StackOverflowError

问题描述

环境与基础代码

项目中使用spring-boot-starter-data-mongodb:2.5.3(对应spring-data-mongodb:3.2.3),简化实体类如下:

@Document
public class Task {
  @Id
  private final String id;
  private final Path taskDir;
  // 构造方法、getter、setter
}

配套默认Spring MongoDB repository可通过id查询任务,Mongo配置如下:

@Configuration
@EnableMongoRepositories(basePackages = {
    "path.to.repository"
}, mongoTemplateRef = MongoConfig.MONGO_TEMPLATE_REF)
@EnableConfigurationProperties(MongoSettings.class)
public class MongoConfig extends MongoConfigurationSupport {

  private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
  public static final String MONGO_TEMPLATE_REF = "mongoAlTemplate";

  private final MongoSettings mongoSettings;

  @Autowired
  public MongoConfig(final MongoSettings mongoSettings) {
    this.mongoSettings = mongoSettings;
  }

  @Bean(name = "ourMongo", destroyMethod = "close")
  public MongoClient ourMongoClient() {
    MongoCredential credential =
        MongoCredential.createCredential(mongoSettings.getUser(),
                                         mongoSettings.getDb(),
                                         mongoSettings.getPassword());
    MongoClientSettings clientSettings = MongoClientSettings.builder()
        .readPreference(ReadPreference.primary())
        // 为@Version和eTag启用乐观锁
        .writeConcern(WriteConcern.ACKNOWLEDGED)
        .credential(credential)
        .applyToSocketSettings(
            builder -> builder.connectTimeout(15, TimeUnit.SECONDS)
                .readTimeout(1, TimeUnit.MINUTES))
        .applyToConnectionPoolSettings(
            builder -> builder.maxConnectionIdleTime(10, TimeUnit.MINUTES)
                .minSize(5).maxSize(20))
        .build();
    return MongoClients.create(clientSettings);
  }

  @Override
  @Nonnull
  protected String getDatabaseName() {
    return mongoSettings.getDb();
  }

  @Bean(name = MONGO_TEMPLATE_REF)
  public MongoTemplate ourMongoTemplate() throws Exception {
    return new MongoTemplate(ourMongoClient(), getDatabaseName());
  }
}

报错信息

调用taskRepository.save(task)存储任务时抛出StackOverflowError,错误栈如下:

java.lang.StackOverflowError
    at java.lang.ThreadLocal.get(ThreadLocal.java:160)
    at java.util.concurrent.locks.ReentrantReadWriteLock$Sync.tryReleaseShared(ReentrantReadWriteLock.java:423)
    at java.util.concurrent.locks.AbstractQueuedSynchronizer.releaseShared(AbstractQueuedSynchronizer.java:1341)
    at java.util.concurrent.locks.ReentrantReadWriteLock$ReadLock.unlock(ReentrantReadWriteLock.java:881)
    at org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:239)
    at org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:201)
    at org.springframework.data.mapping.context.AbstractMappingContext.getPersistentEntity(AbstractMappingContext.java:87)
    at org.springframework.data.mapping.context.MappingContext.getRequiredPersistentEntity(MappingContext.java:73)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writePropertyInternal(MappingMongoConverter.java:740)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeProperties(MappingMongoConverter.java:657)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeInternal(MappingMongoConverter.java:633)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writePropertyInternal(MappingMongoConverter.java:746)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeProperties(MappingMongoConverter.java:657)
    at org.springframework.data.mongodb.core.convert.MappingMongoConverter.writeInternal(MappingMongoConverter.java:633)
    ...

给Task类的taskDir字段加@Transient注解即可正常存储,说明问题为Java/Spring/MongoDB无法直接处理Path类型对象。

排查过程

先后尝试在MongoConfig中配置Path与String互转的自定义转换器、配置Task直接转DBObject的转换器,仍报栈溢出,日志显示configureConverters方法已被调用、转换器已注册。
参照官方文档将转换器抽为独立类:

import org.bson.Document;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.WritingConverter;

import javax.annotation.Nonnull;

@WritingConverter
public class TaskWriteConverter implements Converter<Task, Document> {

  @Override
  public Document convert(@Nonnull Task source) {
    Document document = new Document();
    document.put("_id", source.getId());
    if (source.getTaskDir() != null) {
      document.put("taskDir", source.getTaskDir().normalize().toAbsolutePath().toString());
    }
    return document;
  }
}

MongoConfig配置调整为新增重写configureConverters方法:

@Override
  protected void configureConverters(
      MongoCustomConversions.MongoConverterConfigurationAdapter adapter) {
    LOG.info("configuring converters");
    adapter.registerConverter(new TaskWriteConverter());
    adapter.registerConverter(new TaskReadConverter());
    adapter.registerConverter(new Converter<Path, String>() {
      @Override
      public String convert(@Nonnull Path path) {
        return path.normalize().toAbsolutePath().toString();
      }
    });
    adapter.registerConverter(new Converter<String, Path>() {
      @Override
      public Path convert(@Nonnull String path) {
        return Paths.get(path);
      }
    });
  }

将org.springframework.data日志级别调为debug后,可看到自定义转换器已被识别:

2021-09-23 14:09:20.469 [INFO ] [           main] MongoConfig                              configuring converters 
2021-09-23 14:09:20.480 [DEBUG] [           main] CustomConversions                        Adding user defined converter from class com.acme.Task to class org.bson.Document as writing converter. 
2021-09-23 14:09:20.480 [DEBUG] [           main] CustomConversions                        Adding user defined converter from class org.bson.Document to class com.acme.Task as reading converter. 
2021-09-23 14:09:20.481 [DEBUG] [           main] CustomConversions                        Adding user defined converter from interface java.nio.file.Path to class java.lang.String as writing converter. 
2021-09-23 14:09:20.481 [DEBUG] [           main] CustomConversions                        Adding user defined converter from class java.lang.String to interface java.nio.file.Path as reading converter.

但转换器会被重复加载多次,自定义转换器仅在第三次加载时出现,最后一次加载日志中没有自定义转换器,疑似被覆盖。
复现问题的集成测试如下:

@SpringBootTest
@AutoConfigureMockMvc
@PropertySource("classpath:application-test.properties")
public class SomeIT {
  
  @Autowired
  private TaskRepository taskRepository;

  @Test
  public void testTaskPersistence() throws Exception {
    Task task = new Task("1234", Paths.get("/home/roman"));
    taskRepository.save(task);
  }

}

当前简单实体可正常存储到docker部署的MongoDB中,仅含Path字段的实体存储失败。

问题

为什么自定义转换器不生效?如何配置才能让Spring持久化时将Path转为String存储,读取时自动转回Path对象?


解决方案

根本原因

你自定义的转换器未生效的核心问题是:你声明MongoTemplate时用了new MongoTemplate(ourMongoClient(), getDatabaseName())这个简化构造方法,该方法会初始化默认的MappingMongoConverter,不会加载你重写configureConverters方法注册的自定义转换器。你看到的转换器重复加载、最后一次加载无自定义转换器的现象,是Spring Boot自动配置的Mongo组件和你自定义配置的加载逻辑冲突,最终生效的是未携带自定义转换的默认转换器实例。

修复方法

不需要修改已注册的转换器逻辑,仅需调整MongoTemplate的声明逻辑,让它使用父类MongoConfigurationSupport生成的、已经注入了自定义转换器的MappingMongoConverter即可:

  1. 先在MongoConfig中新增MongoDatabaseFactory bean声明:
@Bean
public MongoDatabaseFactory mongoDbFactory() {
    return new SimpleMongoClientDatabaseFactory(ourMongoClient(), getDatabaseName());
}
  1. 修改ourMongoTemplate的实现,传入父类生成的转换器:
@Bean(name = MONGO_TEMPLATE_REF)
public MongoTemplate ourMongoTemplate() throws Exception {
    return new MongoTemplate(mongoDbFactory(), mappingMongoConverter());
}

修改完成后重启项目,你注册的Path与String互转的转换器就会正常生效,不需要额外编写整个Task实体的转换器,taskDir字段会自动完成序列化与反序列化,不会再抛出栈溢出错误。


内容的提问来源于stack exchange,提问作者Roman Vottner

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.03 14:27:05