【问题标题】:Persisting Path objects with spring-data-mongodb respositories具有 spring-data-mongodb 存储库的持久路径对象
【发布时间】:2021-09-22 11:53:34
【问题描述】:

在一个项目中,我使用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;
  ...

  // constructor, getters, setters
}

使用默认 Spring MongoDB 存储库,允许通过其 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())
        // enable optimistic locking for @Version and eTag usage
        .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))
//        .applyToClusterSettings(
//            builder -> builder.requiredClusterType(ClusterType.REPLICA_SET)
//                .hosts(Arrays.asList(new ServerAddress("host1", 27017),
//                                     new ServerAddress("host2", 27017)))
//                .build())
        .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) 保存任务时,Java 最终会出现 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)
    ...

在使用@Transient 注释Task 类中的路径对象taskDir 时,我能够持久化任务,因此问题似乎与Java/Spring/MongoDB 无法处理Path 有关直接对象。

我的下一个尝试是在 MongoConfig 类中配置一个自定义转换器以在 PathString 表示之间进行转换:

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

虽然错误仍然存​​在。然后我定义了Task 对象和DBObject 之间的直接转换,如guide 所示

@Override
protected void configureConverters(
  MongoCustomConversions.MongoConverterConfigurationAdapter converterConfigurationAdapter) {
  LOG.info("configuring converters");
  converterConfigurationAdapter.registerConverter(new Converter<Task, DBObject>() {
    @Override
    public DBObject convert(@Nonnull Task source) {
    DBObject dbObject = new BasicDBObject();
      if (source.getTaskDirectory() != null) {
        dbObject.put("taskDir", source.getTaskDirectory().normalize().toAbsolutePath().toString());
      }
      ...
      return dbObject;
    }
  });
}

我仍然得到StackOverflowError 作为回报。通过我添加的日志语句,我看到 Spring 调用了 configureConverters 方法,因此应该注册了自定义转换器。

为什么我仍然得到StackOverflowError?我如何需要告诉 Spring 将 Path 对象视为 Strings,同时保持并在读取时将 String 值再次转换为 Path 对象?


更新:

我现在已经按照the official documentation 中给出的示例将转换器重构为自己的类

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 类中的配置现在如下所示:

  @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.

但是,我看到大多数转换器被多次添加,即在应用程序点击存储库上的 save 方法之前,我实际上找到了 4 次 Adding converter from class java.lang.Character to class java.lang.String as writing converter. 的日志。由于我的自定义转换器仅在所有这些转换器出现在日志中的第三次添加,所以我感觉它们被某种方式覆盖,因为上次“迭代”中的日志不包括我的自定义转换器。

重现该问题的测试用例如下:

@ŚpringBootTest
@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);
  }

   ...
}

test-method 仅用于调查当前的持久性问题,在正常情况下根本不应该存在,因为集成测试会测试大文件的上传、预处理等。然而,由于 Spring 无法(至少看起来如此)存储包含 Path 对象的实体,因此该集成测试失败了。

请注意,对于简单实体,我在使用概述的设置持久化它们时没有问题,而且我也将它们显示在 dockerized MongoDB 中。

我还没有时间深入研究为什么 Spring 对 Path 对象有这样的问题,或者为什么我的自定义转换器在 CustomConversions 日志输出的最后一次迭代中突然消失了。

【问题讨论】:

    标签: java spring-data-mongodb


    【解决方案1】:

    事实证明,mongoTemplate 的配置方式确实“覆盖”了任何指定的自定义转换器,因此 Spring 无法使用这些转换器并将 Path 转换为 String,反之亦然。

    在将MongoConfig 更改为下面的之后,我终于可以使用我的自定义转换器,从而按预期保留实体:

    @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())
            // enable optimistic locking for @Version and eTag usage
            .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))
    //        .applyToClusterSettings(
    //            builder -> builder.requiredClusterType(ClusterType.REPLICA_SET)
    //                .hosts(Arrays.asList(new ServerAddress("host1", 27017),
    //                                     new ServerAddress("host2", 27017)))
    //                .build())
            .build();
        LOG.info("Mongo client initialized. Connecting with user {} to DB {}",
                 mongoSettings.getUser(), mongoSettings.getDb());
        return MongoClients.create(clientSettings);
      }
    
      @Override
      @Nonnull
      protected String getDatabaseName() {
        return mongoSettings.getDb();
      }
    
      @Bean
      public MongoDatabaseFactory ourMongoDBFactory() {
        return new SimpleMongoClientDatabaseFactory(ourMongoClient(), getDatabaseName());
      }
    
      @Bean(name = MONGO_TEMPLATE_REF)
      public MongoTemplate ourMongoTemplate() throws Exception {
        return new MongoTemplate(ourMongoDBFactory(), mappingMongoConverter());
      }
    
      @Bean
      public MappingMongoConverter mappingMongoConverter() throws Exception {
        DbRefResolver dbRefResolver = new DefaultDbRefResolver(ourMongoDBFactory());
        MongoCustomConversions customConversions = customConversions();
        MongoMappingContext context = mongoMappingContext(customConversions);
        MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, context);
        // this one is actually needed otherwise the StackOverflowError re-appears!
        converter.setCustomConversions(customConversions);
        return converter;
      }
    
      @Bean
      @Override
      @Nonnull
      public MongoCustomConversions customConversions() {
        return  new MongoCustomConversions(
            Arrays.asList(new PathWriteConverter(), new PathReadConverter())
        );
      }
    }
    

    因此,不是将MongoClient 和数据库名称直接传递给mongoTemplate,而是将一个包含上述值的MongoDatabaseFactory 对象和一个MappingMongoConverter 对象作为输入传递给模板。

    不幸的是,必须在mappingMongoConverter() 方法内两次传递customConversion 对象。如果不这样做,StackOverflowError 会重新出现。

    使用给定的配置,现在可以进行从PathStringStringPath 的转换,因此目前不需要从TaskDocument 的自定义转换,反之亦然。

    【讨论】:

      猜你喜欢
      • 2021-04-22
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-12-12
      • 1970-01-01
      • 2018-04-18
      • 2015-06-18
      • 1970-01-01
      相关资源
      最近更新 更多