Использование OffsetDateTime с Spring Boot и MongoDB приводит к MappingException

Я пытаюсь настроить приложение Spring Boot с базой данных MongoDB. Вот выдержка из имеющихся у меня зависимостей (в представлении Gradle).

compile("org.springframework.boot:spring-boot-starter-web:1.5.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-data-jpa:1.5.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-data-mongodb:1.5.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-hateoas:1.5.1.RELEASE")
compile("org.springframework.boot:spring-boot-starter-security:1.5.1.RELEASE")
compile("org.springframework.security:spring-security-test:1.5.1.RELEASE)
testCompile("org.springframework.boot:spring-boot-starter-test:1.5.1.RELEASE")
compile("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.8.8")

Мой аннотированный класс Java @Document содержит атрибут OffsetDateTime.

@Document(collection = "reports")
public class ReportDocument implements Serializable {

    @Id private String id;
    @Version private Long version;
    //...
    private OffsetDateTime start;
    private OffsetDateTime end;
    //...
}

Когда я вызываю REST-контроллер, который извлекает эти документы, он терпит неудачу с исключением

org.springframework.data.mapping.model.MappingException: No property null found on entity class java.time.OffsetDateTime to bind constructor parameter to!
at org.springframework.data.mapping.model.PersistentEntityParameterValueProvider.getParameterValue(PersistentEntityParameterValueProvider.java:74) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
at org.springframework.data.mapping.model.SpELExpressionParameterValueProvider.getParameterValue(SpELExpressionParameterValueProvider.java:63) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:71) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
at org.springframework.data.convert.ClassGeneratingEntityInstantiator.createInstance(ClassGeneratingEntityInstantiator.java:83) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
at org.springframework.data.mongodb.core.convert.MappingMongoConverter.read(MappingMongoConverter.java:257) ~[spring-data-mongodb-1.10.0.RELEASE.jar:na]

Прочитал много форумов. Некоторые люди заменили OffsetDateTime на дату и время библиотеки Joda. Для меня это не тот путь, поскольку Джода заявляет, что использует типы Java 8 DateTime.

Что я делаю не так (я знаю, что проблема всегда перед компьютером) и как ее решить? Кто-нибудь знает об этом?

ОБНОВЛЕНИЕ (от 22 апреля 2017 г.) Мне понравилось, как сказал @Veeram, и я обновил свое приложение с помощью конвертеров (Date -> OffsetDateTime и наоборот).

package com.my.personal.app.converter;

import org.springframework.core.convert.converter.Converter;

import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.util.Date;

public class DateToOffsetDateTimeConverter implements Converter<Date, OffsetDateTime> {

    @Override
    public OffsetDateTime convert(Date source) {
        return source == null ? null : OffsetDateTime.ofInstant(source.toInstant(), ZoneId.systemDefault());
    }

}

а также

package com.my.personal.app.converter;

import org.springframework.core.convert.converter.Converter;

import java.time.OffsetDateTime;
import java.util.Date;

public class OffsetDateTimeToDateConverter implements Converter<OffsetDateTime, Date> {

    @Override
    public Date convert(OffsetDateTime source) {
        return source == null ? null : Date.from(source.toInstant());
    }

}

регистрация конвертеров

package com.my.personal.app;

import com.my.personal.app.converter.DateToOffsetDateTimeConverter;
import com.my.personal.app.converter.OffsetDateTimeToDateConverter;
import com.mongodb.Mongo;
import com.mongodb.MongoClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.convert.CustomConversions;
import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;

import java.util.ArrayList;
import java.util.List;

@Configuration
public class MongoConfig extends AbstractMongoConfiguration {


    @Override
    protected String getDatabaseName() {
        return "my-personal-database";
    }

    @Override
    public Mongo mongo() throws Exception {
        return new MongoClient("localhost");
    }

    @Bean
    @Override
    public CustomConversions customConversions() {
        List<Converter<?, ?>> converterList = new ArrayList<Converter<?, ?>>();
        converterList.add(new DateToOffsetDateTimeConverter());
        converterList.add(new OffsetDateTimeToDateConverter());
        return new CustomConversions(converterList);
    }

    @Bean
    @Override
    public MongoTemplate mongoTemplate() throws Exception {
        MappingMongoConverter converter = new MappingMongoConverter(
                new DefaultDbRefResolver(mongoDbFactory()), new MongoMappingContext());
        converter.setCustomConversions(customConversions());
        converter.afterPropertiesSet();
        return new MongoTemplate(mongoDbFactory(), converter);
    }


}

но снова приводит к исключению

org.springframework.data.mapping.model.MappingException: No property null found on entity class java.time.OffsetDateTime to bind constructor parameter to!
    at org.springframework.data.mapping.model.PersistentEntityParameterValueProvider.getParameterValue(PersistentEntityParameterValueProvider.java:74) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
    at org.springframework.data.mapping.model.SpELExpressionParameterValueProvider.getParameterValue(SpELExpressionParameterValueProvider.java:63) ~[spring-data-commons-1.13.0.RELEASE.jar:na]
    at org.springframework.data.convert.ReflectionEntityInstantiator.createInstance(ReflectionEntityInstantiator.java:71) ~[spring-data-commons-1.13.0.RELEASE.jar:na]

Я пропустил или неправильно понял что-то. или делать что-л. неправильный?

ОБНОВЛЕНИЕ моих документов в коллекции

Вот отрывок из основных частей документов моей коллекции.

[
  {
    "_id": {
      "$oid": "58f8b107affb5f08e0a78a96"
    },
    "_class": "com.my.personal.app.document.ReportDocument",
    "version": 0,
    "checklistId": 2,
    "vehicleGuid": "some-vehicle-guid",
    "userGuid": "some-user-guid",
    "name": "Report 123",
    "start": {
      "dateTime": {
        "$date": "2017-04-20T12:00:55.930Z"
      },
      "offset": "+02:00"
    },
    "stations": [
      {
        "_id": 1,
        "name": "Front"
      }
    ]
  },
  {
    "_id": {
      "$oid": "58f8bf78affb5f2dec896acf"
    },
    "_class": "com.my.personal.app.document.ReportDocument",
    "version": 0,
    "checklistId": 2,
    "vehicleGuid": "some-vehicle-guid",
    "userGuid": "some-user-guid",
    "name": "Report 123",
    "start": {
      "dateTime": {
        "$date": "2017-04-20T10:02:32.930Z"
      },
      "offset": "+02:00"
    },
    "stations": [
      {
        "_id": 1,
        "name": "Front"
      }
    ]
  }
]

Это контроллер REST, который пытается вызвать документы

@RequestMapping(value = "/mongoreports")
public class MongoReportController {

    @Autowired
    private MongoReportRepository repository;

    @RequestMapping(
            method = RequestMethod.GET,
            produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
    public ResponseEntity<List<ReportDocument>> show(
            @RequestParam(name = "vehicleGuid") Optional<String> vehicleGuid,
            @RequestParam(name = "userGuid") Optional<String> userGuid) {
        if (vehicleGuid.isPresent() && !userGuid.isPresent()) {
            List<ReportDocument> reportDocuments = repository.findByVehicleGuidOrderByStartAsc(vehicleGuid.get());
            return ResponseEntity.ok(reportDocuments);
        }
        if (!vehicleGuid.isPresent() && userGuid.isPresent()) {
            List<ReportDocument> reportDocuments = repository.findByUserGuidOrderByStartAsc(userGuid.get());
            return ResponseEntity.ok(reportDocuments);
        }
        if (vehicleGuid.isPresent() && userGuid.isPresent()) {
            List<ReportDocument> reportDocuments = repository.findByUserGuidAndVehicleGuidOrderByStartAsc(vehicleGuid.get(), userGuid.get());
            return ResponseEntity.ok(reportDocuments);
        }
        return ResponseEntity.badRequest().build();
    }

и соответствующий MongoRepository

package com.my.personal.app.repository;

import com.my.personal.app.document.ReportDocument;
import org.springframework.data.mongodb.repository.MongoRepository;

import java.util.List;

public interface MongoReportRepository extends MongoRepository<ReportDocument, String> {

    List<ReportDocument> findByVehicleGuidOrderByStartAsc(String vehicleGuid);

    List<ReportDocument> findByUserGuidOrderByStartAsc(String userGuid);

    List<ReportDocument> findByUserGuidAndVehicleGuidOrderByStartAsc(String userGuid, String vehicleGuid);

}

person Bugra    schedule 21.04.2017    source источник
comment
напишите свой собственный конвертер и зарегистрируйтесь в MongoConfiguration   -  person Viet    schedule 21.04.2017
comment
Вы можете выполнить шаги, описанные здесь, чтобы создать пользовательское преобразование здесь. stackoverflow.com/questions/41127665/zoneddatetime-with-mongodb. Также прочитайте jira.spring.io/browse/DATACMNS-698.   -  person s7vr    schedule 21.04.2017
comment
@Veraam: я адаптировал конвертеры и зарегистрировал их. Но приложение не затрагивается, и возникает такое же исключение. Есть ли что-л. что я пропустил?   -  person Bugra    schedule 22.04.2017
comment
Могу ли я увидеть образец документа из вашей коллекции вместе с кодом вызова? Как поля сохраняются в базе данных?   -  person s7vr    schedule 22.04.2017
comment
Ваша конфигурация вообще подхватывается?   -  person s7vr    schedule 22.04.2017
comment
Я обновил свой пост выдержкой из своих документов и кодом вызова контроллера REST. Что вы имеете в виду, когда говорите, подхватывается ли конфигурация вообще? Что я могу сделать, чтобы проверить упомянутый вами пункт?   -  person Bugra    schedule 22.04.2017
comment
Извините, я просто спросил, работает ли вообще ваша конфигурация, но я думаю, что вижу проблему. Вы должны сохранить поля как поля типа Date, а не поля OffsetDateTime. Преобразователи Spring преобразуют их из даты в OffsetDateTime и наоборот.   -  person s7vr    schedule 22.04.2017
comment
Ах я вижу. Спасибо за подсказку. Но это также означает, что я не могу использовать какие-либо данные о смещении или даже данные о зональной дате. Метаинформация будет потеряна, и я смогу использовать только Date или LocalDateTime. Спасибо за важную подсказку. Иначе я бы искал бесконечно и с бородой сейчас. Я отмечу ваш ответ как полезный. Большое спасибо.   -  person Bugra    schedule 22.04.2017


Ответы (1)


Я боролся с этим в течение нескольких часов. Ошибка, которую я получал, была:

No converter found capable of converting from type [java.util.Date] to type [java.time.OffsetDateTime]

В конце концов я придумал следующий класс конфигурации, который преобразуется в java.util.Date, а не в строку:

import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import java.util.Date;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.mongodb.core.convert.MongoCustomConversions;

@Configuration
public class MongoConfig {

    @Bean
    public MongoCustomConversions mongoCustomConversions() {
        return new MongoCustomConversions(Arrays.asList(
            new OffsetDateTimeReadConverter(),
            new OffsetDateTimeWriteConverter()
        ));
    }

    static class OffsetDateTimeWriteConverter implements Converter<OffsetDateTime, Date> {

        @Override
        public Date convert(OffsetDateTime source) {
            return source == null ? null : Date.from(source.toInstant().atZone(ZoneOffset.UTC).toInstant());
        }
    }

    static class OffsetDateTimeReadConverter implements Converter<Date, OffsetDateTime> {

        @Override
        public OffsetDateTime convert(Date source) {
            return source == null ? null : source.toInstant().atOffset(ZoneOffset.UTC);
        }
    }
}

Это сработало для меня при построении против:

 <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.1.RELEASE</version>
    <relativePath/>
  </parent>
person theINtoy    schedule 05.08.2020