Як використовувати LocalDateTime RequestParam навесні? Я отримую повідомлення "Не вдалося перетворити рядок у LocalDateTime"


80

Я використовую Spring Boot і входить jackson-datatype-jsr310до складу Maven:

<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.7.3</version>
</dependency>

Коли я намагаюся використовувати RequestParam із типом дати / часу Java 8,

@GetMapping("/test")
public Page<User> get(
    @RequestParam(value = "start", required = false)
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start) {
//...
}

і протестуйте його за цією URL-адресою:

/test?start=2016-10-8T00:00

Я отримую таку помилку:

{
  "timestamp": 1477528408379,
  "status": 400,
  "error": "Bad Request",
  "exception": "org.springframework.web.method.annotation.MethodArgumentTypeMismatchException",
  "message": "Failed to convert value of type [java.lang.String] to required type [java.time.LocalDateTime]; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam @org.springframework.format.annotation.DateTimeFormat java.time.LocalDateTime] for value '2016-10-8T00:00'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2016-10-8T00:00]",
  "path": "/test"
}

Відповіді:


59

TL; DR - ви можете захопити його як рядок за допомогою just @RequestParam, або ви можете мати Spring додатково проаналізувати рядок у класі дати / часу Java за @DateTimeFormatдопомогою параметра.

@RequestParamдосить , щоб захопити дату поставки після знака =, проте, він приходить в метод як String. Ось чому він кидає виняток акторів.

Є кілька способів досягти цього:

  1. проаналізуйте дату самостійно, захопивши значення як рядок.
@GetMapping("/test")
public Page<User> get(@RequestParam(value="start", required = false) String start){

    //Create a DateTimeFormatter with your required format:
    DateTimeFormatter dateTimeFormat = 
            new DateTimeFormatter(DateTimeFormatter.BASIC_ISO_DATE);

    //Next parse the date from the @RequestParam, specifying the TO type as 
a TemporalQuery:
   LocalDateTime date = dateTimeFormat.parse(start, LocalDateTime::from);

    //Do the rest of your code...
}
  1. Використовуйте можливість Spring для автоматичного аналізу та очікування форматів дат:
@GetMapping("/test")
public void processDateTime(@RequestParam("start") 
                            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) 
                            LocalDateTime date) {
        // The rest of your code (Spring already parsed the date).
}

Звичайно, але є одна основна проблема - навіщо використовувати користувацький контролер, якщо для більшості цих запитів ви могли б використовувати Spring JPA Repositories? І саме тут фактично починається проблема з цією помилкою; /
thorinkor

26
Ви також можете використати це рішення у способі підпису:@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start
Анна

1
@Anna, будь ласка, опублікуй свій коментар як відповідь, оскільки він повинен бути прийнятим imo
Joaquín L. Robles

Дякую, підхід 2 працює для мене, оскільки іноді я проходжу менуети, а інколи мені теж не потрібен. це просто піклується про все це :)
ASH

70

Ви все правильно зробили :). Ось приклад, який показує, що саме ви робите. Просто анотуйте ваш RequestParam за допомогою @DateTimeFormat. У GenericConversionServiceконтролері немає необхідності в спеціальному або ручному перетворенні. Про це пише цей допис у блозі.

@RestController
@RequestMapping("/api/datetime/")
final class DateTimeController {

    @RequestMapping(value = "datetime", method = RequestMethod.POST)
    public void processDateTime(@RequestParam("datetime") 
                                @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime dateAndTime) {
        //Do stuff
    }
}

Думаю, у вас виникла проблема з форматом. На моїй установці все працює добре.


Я погодився з цією порадою, і вона спрацювала, але потім я подумав, чи можна анотацію застосувати до всього методу контролера ... і виявляється, що може. Це не може бути застосоване до всього регулятору, хоча: @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface DateTimeFormat {.
АбуНассар,

Незважаючи на мій коментар вище, переміщення анотації з параметра запиту (насправді два з них: startDateі endDate) до методу запиту, здавалося, порушило поведінку методу на гірше.
АбуНассар,

Це добре працює для шаблонів дат, які не мають позначок часу, але якщо ви включите позначку часу в шаблон, він не зможе перетворити рядок на дату (або інший відповідний тип).
MattWeiler

Я помилився, це добре працює з мітками часу, але якщо скопіювати і вставити приклад із JavaDoc для org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME, він не вдасться. Приклад, який вони надають, повинен мати X замість Z для його шаблону, оскільки вони включали -05: 00 на відміну від -0500.
MattWeiler,

Я спробував це рішення, і воно працює, якщо ви передаєте дату або DateTime, але коли значення ПУСТІ, це не вдається.
darshgohel

29

Я знайшов тут обхідний шлях .

Spring / Spring Boot підтримує лише формат дати / дати-часу в параметрах BODY.

Наступний клас конфігурації додає підтримку дати / дати-часу в QUERY STRING (параметри запиту):

// Since Spring Framwork 5.0 & Java 8+
@Configuration
public class DateTimeFormatConfiguration implements WebMvcConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}

відповідно:

// Until Spring Framwork 4.+
@Configuration
public class DateTimeFormatConfiguration extends WebMvcConfigurerAdapter {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}

Це працює, навіть якщо ви прив'язуєте кілька параметрів запиту до якогось класу ( @DateTimeFormatу цьому випадку анотація безпомічна):

public class ReportRequest {
    private LocalDate from;
    private LocalDate to;

    public LocalDate getFrom() {
        return from;
    }

    public void setFrom(LocalDate from) {
        this.from = from;
    }

    public LocalDate getTo() {
        return to;
    }

    public void setTo(LocalDate to) {
        this.to = to;
    }
}

// ...

@GetMapping("/api/report")
public void getReport(ReportRequest request) {
// ...

як зловити виняток конвертації тут?
Terran,

Це найкраща відповідь. Це працює, навіть якщо поле Дата є вкладеним полем. Це також краще, оскільки таким чином вам просто потрібно додати цю конфігурацію один раз.
bluelurker

20

Як я вже писав у коментарі, ви також можете використовувати це рішення в методі підпису: @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start


4

Я зіткнувся з тією ж проблемою і знайшов тут своє рішення (без використання анотацій)

... ви повинні принаймні належним чином зареєструвати рядок у конвертері [LocalDateTime] у вашому контексті, щоб Spring міг використовувати його, щоб автоматично робити це для вас кожного разу, коли ви даєте рядок як вхід і очікуєте [LocalDateTime]. (Велика кількість перетворювачів вже реалізована Spring і міститься в пакеті core.convert.support, але жоден не передбачає перетворення [LocalDateTime])

Отже, у вашому випадку ви зробите це:

public class StringToLocalDateTimeConverter implements Converter<String, LocalDateTime> {
    public LocalDateTime convert(String source) {
        DateTimeFormatter formatter = DateTimeFormatter.BASIC_ISO_DATE;
        return LocalDateTime.parse(source, formatter);
    }
}

а потім просто зареєструйте свій боб:

<bean class="com.mycompany.mypackage.StringToLocalDateTimeConverter"/>

З анотаціями

додайте його у свою службу ConversionService:

@Component
public class SomeAmazingConversionService extends GenericConversionService {

    public SomeAmazingConversionService() {
        addConverter(new StringToLocalDateTimeConverter());
    }

}

і нарешті, ви б потім @Autowire у своєму ConversionService:

@Autowired
private SomeAmazingConversionService someAmazingConversionService;

Детальніше про перетворення з spring (та форматування) ви можете прочитати на цьому веб-сайті . Попереджаємо, у ньому є маса реклами, але я точно визнав, що це корисний сайт та гарне вступ до теми.


3

Оновлення SpringBoot 2.XX

Якщо ви використовуєте spring-boot-starter-webверсію залежності 2.0.0.RELEASEабо новішу, то більше не потрібно явно включати jackson-datatype-jsr310залежність, яка вже надана spring-boot-starter-webчерез spring-boot-starter-json.

Це було вирішено як випуск Spring Boot № 9297, і відповідь все ще є актуальною та актуальною:

@RequestMapping(value = "datetime", method = RequestMethod.POST)
public void foo(@RequestParam("dateTime") 
                @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime ldt) {
    // IMPLEMENTATION
}

2

Наступне добре працює з Spring Boot 2.1.6:

Контролер

@Slf4j
@RestController
public class RequestController {

    @GetMapping
    public String test(RequestParameter param) {
        log.info("Called services with parameter: " + param);
        LocalDateTime dateTime = param.getCreated().plus(10, ChronoUnit.YEARS);
        LocalDate date = param.getCreatedDate().plus(10, ChronoUnit.YEARS);

        String result = "DATE_TIME: " + dateTime + "<br /> DATE: " + date;
        return result;
    }

    @PostMapping
    public LocalDate post(@RequestBody PostBody body) {
        log.info("Posted body: " + body);
        return body.getDate().plus(10, ChronoUnit.YEARS);
    }
}

Dto класи:

@Value
public class RequestParameter {
    @DateTimeFormat(iso = DATE_TIME)
    LocalDateTime created;

    @DateTimeFormat(iso = DATE)
    LocalDate createdDate;
}

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostBody {
    LocalDate date;
}

Тестовий клас:

@RunWith(SpringRunner.class)
@WebMvcTest(RequestController.class)
public class RequestControllerTest {

    @Autowired MockMvc mvc;
    @Autowired ObjectMapper mapper;

    @Test
    public void testWsCall() throws Exception {
        String pDate        = "2019-05-01";
        String pDateTime    = pDate + "T23:10:01";
        String eDateTime = "2029-05-01T23:10:01"; 

        MvcResult result = mvc.perform(MockMvcRequestBuilders.get("")
            .param("created", pDateTime)
            .param("createdDate", pDate))
          .andExpect(status().isOk())
          .andReturn();

        String payload = result.getResponse().getContentAsString();
        assertThat(payload).contains(eDateTime);
    }

    @Test
    public void testMapper() throws Exception {
        String pDate        = "2019-05-01";
        String eDate        = "2029-05-01";
        String pDateTime    = pDate + "T23:10:01";
        String eDateTime    = eDate + "T23:10:01"; 

        MvcResult result = mvc.perform(MockMvcRequestBuilders.get("")
            .param("created", pDateTime)
            .param("createdDate", pDate)
        )
        .andExpect(status().isOk())
        .andReturn();

        String payload = result.getResponse().getContentAsString();
        assertThat(payload).contains(eDate).contains(eDateTime);
    }


    @Test
    public void testPost() throws Exception {
        LocalDate testDate = LocalDate.of(2015, Month.JANUARY, 1);

        PostBody body = PostBody.builder().date(testDate).build();
        String request = mapper.writeValueAsString(body);

        MvcResult result = mvc.perform(MockMvcRequestBuilders.post("")
            .content(request).contentType(APPLICATION_JSON_VALUE)
        )
        .andExpect(status().isOk())
        .andReturn();

        ObjectReader reader = mapper.reader().forType(LocalDate.class);
        LocalDate payload = reader.readValue(result.getResponse().getContentAsString());
        assertThat(payload).isEqualTo(testDate.plus(10, ChronoUnit.YEARS));
    }

}

1

Наведені вище відповіді для мене не спрацювали, але я помилився з тим, що тут: https://blog.codecentric.de/en/2017/08/parsing-of-localdate-query-parameters-in-spring- boot / Виграшним фрагментом стала анотація ControllerAdvice, яка має перевагу, застосовуючи це виправлення до всіх ваших контролерів:

@ControllerAdvice
public class LocalDateTimeControllerAdvice
{

    @InitBinder
    public void initBinder( WebDataBinder binder )
    {
        binder.registerCustomEditor( LocalDateTime.class, new PropertyEditorSupport()
        {
            @Override
            public void setAsText( String text ) throws IllegalArgumentException
            {
                LocalDateTime.parse( text, DateTimeFormatter.ISO_DATE_TIME );
            }
        } );
    }
}

1

Ви можете додати до config, це рішення працює з додатковими, а також з необов'язковими параметрами.

@Bean
    public Formatter<LocalDate> localDateFormatter() {
        return new Formatter<>() {
            @Override
            public LocalDate parse(String text, Locale locale) {
                return LocalDate.parse(text, DateTimeFormatter.ISO_DATE);
            }

            @Override
            public String print(LocalDate object, Locale locale) {
                return DateTimeFormatter.ISO_DATE.format(object);
            }
        };
    }


    @Bean
    public Formatter<LocalDateTime> localDateTimeFormatter() {
        return new Formatter<>() {
            @Override
            public LocalDateTime parse(String text, Locale locale) {
                return LocalDateTime.parse(text, DateTimeFormatter.ISO_DATE_TIME);
            }

            @Override
            public String print(LocalDateTime object, Locale locale) {
                return DateTimeFormatter.ISO_DATE_TIME.format(object);
            }
        };
    }


Ось декілька вказівок щодо того, як написати хорошу відповідь? . Ця надана відповідь може бути правильною, але вона може отримати користь від пояснення. Відповіді лише за кодом не вважаються "хорошими" відповідями.
Trenton McKinney

0

Для глобальної конфігурації:

public class LocalDateTimePropertyEditor extends PropertyEditorSupport {

    @Override
    public void setAsText(String text) throws IllegalArgumentException {
        setValue(LocalDateTime.parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME));
    }

}

І потім

@ControllerAdvice
public class InitBinderHandler {

    @InitBinder
    public void initBinder(WebDataBinder binder) { 
        binder.registerCustomEditor(OffsetDateTime.class, new OffsetDateTimePropertyEditor());
    }

}

Це LocalDateTimePropertyEditorмає бути OffsetDateTimePropertyEditor, або навпаки?
tom_mai78101
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.