#java #spring-data-mongodb
Вопрос:
В проекте, который я использую 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 по умолчанию, который позволяет извлекать задачу по ее идентификатору.
Конфигурация 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 возникает ошибка StackOverflow
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)
...
On annotating the path object taskDir
in the Task
class with @Transient
I’m able to persist the task, so the problem seems to be related with Java/Spring/MongoDB being unable to handle Path
objects directly.
My next attempt was to configure a custom converter inside the MongoConfig
class to convert between Path
and String
representations:
@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);
}
});
}
though the error remained. I then defined a direct conversion between the Task
object and a DBObject
as showcased in this 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;
}
});
}
and I still get a StackOverflowError
in return. Through the log statement I added I see that Spring called into the configureConverters
method and therefore should have registered the custom converters.
Why do I still get the StackOverflowError
though? How do I need to tell Spring to treat Path
objects as String
s while persisting and on read-time convert the String
value to a Path
object back again?
Update:
I’ve now followed the example given in the official documentation and refactored the converter to its own class
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;
}
}
The configuration in the MongoConfig
class now looks like this:
@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
to 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.
Однако я вижу, что большинство конвертеров добавляются несколько раз, т. Е. Я нахожу журнал Adding converter from class java.lang.Character to class java.lang.String as writing converter.
фактически 4 раза, прежде чем приложение попадет в save
метод в репозитории. Поскольку мои пользовательские конвертеры добавляются только в 3-й раз, когда все эти конвертеры появляются в журналах, у меня такое чувство, что они каким-то образом перезаписаны, поскольку журналы в последней «итерации» не включают мои пользовательские конвертеры.
Тестовый пример, который воспроизводит эту проблему, выглядит следующим образом:
@Ś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);
}
...
}
Метод тестирования используется только для изучения текущей проблемы сохранения и при нормальных условиях вообще не должен присутствовать, поскольку интеграционный тест проверяет загрузку большого файла, его предварительную обработку и так далее. Однако эти интеграционные тесты завершаются неудачей из-за того, что Spring не может, по крайней мере, так кажется, хранить сущности, содержащие объекты Path.
Обратите внимание, что для простых сущностей у меня нет проблем с сохранением их с описанной настройкой, и я также вижу их в закрепленном MongoDB.
У меня еще не было времени углубиться в то, почему у Spring такие проблемы с Path
объектами или почему мои пользовательские преобразователи внезапно исчезают на последней итерации вывода CustomConversions
журнала.
Ответ №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
объект.
К сожалению, customConversion
в рамках метода необходимо дважды передать объект mappingMongoConverter()
. Если этого не сделать, StackOverflowError
то он появится снова.
При данной конфигурации теперь возможны преобразования из Path
в String
и String
в Path
, и, следовательно, в настоящее время не требуется никаких пользовательских преобразований из Task
в Document
и наоборот.