Hoe LocalDateTime RequestParam gebruiken in het voorjaar? Ik krijg “Kan string niet converteren naar LocalDateTime”

Ik gebruik Spring Boot en heb jackson-datatype-jsr310meegeleverd met Maven:

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

Als ik een RequestParam probeer te gebruiken met een Java 8-datum/tijd-type,

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

en test het met deze URL:

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

Ik krijg de volgende foutmelding:

{
  "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"
}

Antwoord 1, autoriteit 100%

TL;DR– je kunt het vastleggen als een tekenreeks met slechts @RequestParam, of je kunt Spring de tekenreeks bovendien laten ontleden in een java-datum / tijdklasse via @DateTimeFormatook op de parameter.

de @RequestParamis voldoende om de datum te pakken die u invoert na het =-teken, maar het komt in de methode als een String. Daarom gooit het de cast-uitzondering.

Er zijn een paar manieren om dit te bereiken:

  1. ontleed de datum zelf en pak de waarde als een tekenreeks.
@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. Maak gebruik van de mogelijkheid van Spring om automatisch datumnotaties te ontleden en te verwachten:
@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).
}

Antwoord 2, autoriteit 90%

Je hebt alles goed gedaan 🙂 . Hieris een voorbeeld dat precies laat zien wat u zijn aan het doen. GewoonAnnoteer uw RequestParam met @DateTimeFormat. Er is geen speciale GenericConversionServiceof handmatige conversie in de controller nodig. Dezeblogpost schrijft erover.

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

Ik denk dat je een probleem had met het formaat. Op mijn setup werkt alles goed.


Antwoord 3, autoriteit 41%

Ik heb hiereen tijdelijke oplossing gevonden.

Spring/Spring Boot ondersteunt alleen de datum/datum-tijd-notatie in BODY-parameters.

De volgende configuratieklasse voegt ondersteuning toe voor datum/datum-tijd in QUERY STRING (verzoekparameters):

// 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);
    }
}

respectievelijk:

// 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);
    }
}

Het werkt zelfs als je meerdere verzoekparameters aan een klasse bindt (@DateTimeFormatannotatie hulpeloos in dit geval):

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) {
// ...

Antwoord 4, autoriteit 24%

Zoals ik in de opmerking plaatste, zou je deze oplossing ook kunnen gebruiken in de handtekeningmethode: @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start


Antwoord 5, autoriteit 11%

SpringBoot 2.X.X en nieuwer

Als je de afhankelijkheid gebruikt spring-boot-starter-webversie 2.0.0.RELEASEof hoger, het is niet langer nodig om expliciet jackson-datatype-jsr310-afhankelijkheid, die al wordt geleverd met spring-boot-starter-webvia spring-boot-starter-json.

Dit is opgelost als Spring Boot-probleem #9297en de antwoordis noggeldig en relevant:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.0.0.RELEASE</version>
</dependency>
@RequestMapping(value = "datetime", method = RequestMethod.POST)
public void foo(@RequestParam("dateTime") 
                @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime ldt) {
    // IMPLEMENTATION
}

Antwoord 6, autoriteit 4%

Ik kwam hetzelfde probleem tegen en vond mijn oplossing hier(zonder annotaties te gebruiken)

…je moet op zijn minst een string correct registreren bij [LocalDateTime] Converter in
uw context, zodat Spring deze kan gebruiken om dit automatisch te doen voor
u elke keer dat u een String als invoer geeft en een [LocalDateTime] verwacht. (Een grote
aantal converters is al geïmplementeerd door Spring en bevat
in het pakket core.convert.support, maar geen enkele heeft betrekking op een [LocalDateTime]
conversie)

Dus in jouw geval zou je dit doen:

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

en registreer dan gewoon je boon:

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

Met annotaties

voeg het toe aan uw ConversionService:

@Component
public class SomeAmazingConversionService extends GenericConversionService {
    public SomeAmazingConversionService() {
        addConverter(new StringToLocalDateTimeConverter());
    }
}

en tot slot zou u dan @Autowire in uw ConversionService:

@Autowired
private SomeAmazingConversionService someAmazingConversionService;

Je kunt meer lezen over conversies met spring (en opmaak) op deze website. Wees gewaarschuwd, het heeft een heleboel advertenties, maar ik vond het zeker een nuttige site en een goede introductie tot het onderwerp.


Antwoord 7, autoriteit 3%

Het volgende werkt goed met Spring Boot 2.1.6:

Controller

@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-lessen:

@Value
public class RequestParameter {
    @DateTimeFormat(iso = DATE_TIME)
    LocalDateTime created;
    @DateTimeFormat(iso = DATE)
    LocalDate createdDate;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class PostBody {
    LocalDate date;
}

Testles:

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

Antwoord 8

De bovenstaande antwoorden werkten niet voor mij, maar ik blunderde naar een die hier wel werkte: https://blog.codecentric.de/en/2017/08/parsing-of-localdate-query-parameters-in-spring-boot/ Het winnende fragment was de ControllerAdvice-annotatie, die het voordeel heeft dat deze oplossing op al je controllers wordt toegepast:

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

Antwoord 9

Je kunt toevoegen aan config, deze oplossing werkt zowel met optionele als met niet-optionele parameters.

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

Antwoord 10

Hier is nog een algemene oplossing met parameterconverter:

import org.springframework.core.convert.converter.Converter;
import org.springframework.stereotype.Component;
import ru.diasoft.micro.msamiddleoffice.ftcaa.customerprofile.config.JacksonConfig;
import java.time.DateTimeException;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
@Component
public class LocalDateTimeConverter implements Converter<String, LocalDateTime>{
    private static final List<String> SUPPORTED_FORMATS = Arrays.asList("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", "[another date time format ...]");
    private static final List<DateTimeFormatter> DATE_TIME_FORMATTERS = SUPPORTED_FORMATS
            .stream()
            .map(DateTimeFormatter::ofPattern)
            .collect(Collectors.toList());
    @Override
    public LocalDateTime convert(String s) {
        for (DateTimeFormatter dateTimeFormatter : DATE_TIME_FORMATTERS) {
            try {
                return LocalDateTime.parse(s, dateTimeFormatter);
            } catch (DateTimeParseException ex) {
                // deliberate empty block so that all parsers run
            }
        }
        throw new DateTimeException(String.format("unable to parse (%s) supported formats are %s",
                s, String.join(", ", SUPPORTED_FORMATS)));
    }
}

Antwoord 11

Voor globale configuratie:

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

En dan

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

Antwoord 12

U kunt de datum/tijd-indeling globaal configureren in application properties.
Vind ik leuk:

spring.mvc.format.date=jjjj-MM-dd

spring.mvc.format.date-time=jjjj-MM-dd UU:mm:ss

spring.mvc.format.time=UU:mm:ss

Check in mavern: org.springframework.boot:spring-boot-autoconfigure:2.5.3


Antwoord 13

Ik had een soortgelijk probleem in een gerelateerde context

Ik gebruik WebRequestDataBinder om de verzoekparameters dynamisch aan een model toe te wijzen.

Object domainObject = ModelManager.getEntity(entityName).newInstance();
WebRequestDataBinder binder = new WebRequestDataBinder(domainObject);
binder.bind(request);

Dit stuk code werkt voor primitieven, maar werkte niet voor kenmerken van het type LocalDateTime

Om het probleem op te lossen, heb ik, voordat ik binder.bind aanroep, een aangepaste editor geregistreerd voordat ik bind() aanroep

binder.registerCustomEditor(LocalDateTime.class, new PropertyEditorSupport()
                {
                    @Override
                    public void setAsText(String text) throws IllegalArgumentException
                    {
                        setValue(LocalDateTime.parse(text, DateTimeFormatter.ISO_DATE_TIME));
                    }
                    @Override
                    public String getAsText() {
                        return DateTimeFormatter.ISO_DATE_TIME.format((LocalDateTime) getValue());
                    }
                }
            );

Dit loste het probleem op.

Other episodes