Làm thế nào để sử dụng LocalDateTime RequestParam trong Spring? Tôi nhận được thông báo "Không chuyển đổi được chuỗi thành LocalDateTime"


80

Tôi sử dụng Spring Boot và đi kèm jackson-datatype-jsr310với Maven:

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

Khi tôi cố gắng sử dụng RequestParam với loại Ngày / Giờ trong Java 8,

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

và kiểm tra nó với URL này:

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

Tôi nhận được lỗi sau đây:

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

Câu trả lời:


59

TL; DR - bạn có thể nắm bắt nó dưới dạng một chuỗi chỉ với @RequestParamhoặc bạn có thể có Spring bổ sung phân tích cú pháp chuỗi thành một lớp ngày / giờ java thông qua @DateTimeFormattham số.

những @RequestParamlà đủ để lấy ngày bạn cung cấp sau dấu =, tuy nhiên, nó đi vào phương pháp như một String. Đó là lý do tại sao nó ném ngoại lệ diễn viên.

Có một số cách để đạt được điều này:

  1. tự phân tích cú pháp ngày, lấy giá trị dưới dạng một chuỗi.
@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. Tận dụng khả năng tự động phân tích cú pháp và định dạng ngày tháng của 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).
}

Chắc chắn, nhưng có một vấn đề lớn - tại sao lại sử dụng bộ điều khiển tùy chỉnh, nếu đối với hầu hết các yêu cầu đó, Bạn có thể sử dụng Kho lưu trữ Spring JPA? Và đây là nơi mà vấn đề thực sự với lỗi này bắt đầu; /
thorinkor

26
Bạn cũng có thể sử dụng giải pháp này trong phương pháp chữ ký:@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start
Anna

1
@Anna xin vui lòng gửi nhận xét của bạn như một câu trả lời như nó phải là người chấp nhận imo
Joaquín L. Robles

Cảm ơn bạn, phương pháp tiếp cận 2 phù hợp với tôi vì đôi khi tôi vượt qua minuets, và những lần khác tôi không cần quá. điều này chỉ giải quyết tất cả những điều đó :)
ASH

70

Bạn đã làm mọi thứ chính xác :). Đây là một ví dụ cho thấy chính xác những gì bạn đang làm. Chỉ cần chú thích RequestParam của bạn với @DateTimeFormat. Không cần GenericConversionServicechuyển đổi thủ công hoặc đặc biệt trong bộ điều khiển. Bài đăng trên blog này viết về nó.

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

Tôi đoán bạn đã gặp sự cố với định dạng. Trên thiết lập của tôi, mọi thứ hoạt động tốt.


Tôi đã làm theo lời khuyên này, và nó đã hoạt động, nhưng sau đó tôi tự hỏi liệu chú thích có thể được áp dụng cho toàn bộ phương thức bộ điều khiển hay không ... và hóa ra nó có thể. Nó không thể được áp dụng cho toàn bộ điều khiển, mặc dù: @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.ANNOTATION_TYPE}) public @interface DateTimeFormat {.
AbuNassar

Bất chấp nhận xét của tôi ở trên, việc di chuyển chú thích từ một tham số yêu cầu (thực tế là hai trong số đó: startDateendDate) sang phương thức yêu cầu dường như đã phá vỡ hành vi của phương thức đó.
AbuNassar

Điều này hoạt động tốt đối với các mẫu ngày không có dấu thời gian, nhưng nếu bạn bao gồm dấu thời gian trong mẫu, nó không thể chuyển đổi Chuỗi thành Ngày (hoặc loại áp dụng khác).
MattWeiler

Tôi đã nhầm, điều này chỉ hoạt động tốt với dấu thời gian, nhưng nếu bạn sao chép-dán ví dụ trong JavaDoc cho org.springframework.format.annotation.DateTimeFormat.ISO.DATE_TIME, nó sẽ không thành công. Ví dụ mà họ cung cấp phải có X thay vì Z cho mẫu của nó vì chúng bao gồm -05: 00 thay vì -0500.
MattWeiler

Tôi đã thử giải pháp này và nó hoạt động nếu bạn chuyển ngày hoặc DateTime, nhưng khi các giá trị là EMPTY, điều này không thành công.
darshgohel

29

Tôi đã tìm thấy cách giải quyết ở đây .

Spring / Spring Boot chỉ hỗ trợ định dạng ngày / tháng-giờ trong tham số BODY.

Lớp cấu hình sau đây bổ sung hỗ trợ cho ngày / giờ trong QUERY STRING (tham số yêu cầu):

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

tương ứng:

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

Nó hoạt động ngay cả khi bạn liên kết nhiều tham số yêu cầu với một số lớp ( @DateTimeFormatchú thích không có tác dụng trong trường hợp này):

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

Làm thế nào để bắt ngoại lệ chuyển đổi ở đây?
Terran

Đây là câu trả lời tốt nhất. Nó hoạt động ngay cả khi trường Ngày là một trường lồng nhau. Nó cũng tốt hơn vì theo cách này, bạn chỉ phải thêm cấu hình này một lần.
bluelurker

20

Giống như tôi đã đưa ra trong nhận xét, bạn cũng có thể sử dụng giải pháp này trong phương pháp chữ ký: @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime start


4

Tôi gặp phải vấn đề tương tự và tìm thấy giải pháp của mình ở đây (mà không sử dụng Chú thích)

... bạn ít nhất phải đăng ký đúng một chuỗi vào Trình chuyển đổi [LocalDateTime] trong ngữ cảnh của bạn, để Spring có thể sử dụng nó để tự động thực hiện việc này cho bạn mỗi khi bạn cung cấp một Chuỗi làm đầu vào và mong đợi một [LocalDateTime]. (Một số lượng lớn trình chuyển đổi đã được Spring triển khai và chứa trong gói core.convert.support, nhưng không có chuyển đổi nào liên quan đến chuyển đổi [LocalDateTime])

Vì vậy, trong trường hợp của bạn, bạn sẽ làm điều này:

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

và sau đó chỉ cần đăng ký bean của bạn:

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

Có chú thích

thêm nó vào ConversionService của bạn:

@Component
public class SomeAmazingConversionService extends GenericConversionService {

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

}

và cuối cùng bạn sẽ @Autowire trong ConversionService của mình:

@Autowired
private SomeAmazingConversionService someAmazingConversionService;

Bạn có thể đọc thêm về chuyển đổi với mùa xuân (và định dạng) trên trang web này . Hãy cảnh báo trước rằng nó có rất nhiều quảng cáo, nhưng tôi chắc chắn thấy nó là một trang web hữu ích và một phần giới thiệu tốt về chủ đề.


3

Cập nhật SpringBoot 2.XX

Nếu bạn sử dụng spring-boot-starter-webphiên bản phụ thuộc 2.0.0.RELEASEhoặc cao hơn, thì không cần bao gồm jackson-datatype-jsr310phụ thuộc một cách rõ ràng , đã được cung cấp spring-boot-starter-webthông qua spring-boot-starter-json.

Điều này đã được giải quyết dưới dạng vấn đề Spring Boot # 9297câu trả lời vẫn hợp lệ và có liên quan:

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

2

Sau đây hoạt động tốt với Spring Boot 2.1.6:

Bộ điều khiển

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

Các lớp 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;
}

Lớp kiểm tra:

@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

Các câu trả lời ở trên không phù hợp với tôi, nhưng tôi đã nhầm lẫn với một câu trả lời ở đây: https://blog.codecentric.de/en/2017/08/parsing-of-localdate-query-parameters-in-spring- boot / Đoạn mã chiến thắng là chú thích ControllerAdvice, có lợi thế khi áp dụng bản sửa lỗi này trên tất cả các bộ điều khiển của bạn:

@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

Bạn có thể thêm vào cấu hình, giải pháp này hoạt động với các thông số tùy chọn cũng như không tùy chọn.

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


Dưới đây là một số hướng dẫn về Làm cách nào để viết một câu trả lời tốt? . Câu trả lời được cung cấp này có thể đúng, nhưng nó có thể có lợi từ một lời giải thích. Câu trả lời chỉ có mã không được coi là câu trả lời "tốt".
Trenton McKinney

0

Đối với cấu hình toàn cầu:

public class LocalDateTimePropertyEditor extends PropertyEditorSupport {

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

}

Và sau đó

@ControllerAdvice
public class InitBinderHandler {

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

}

LocalDateTimePropertyEditorđược cho là OffsetDateTimePropertyEditor, hoặc ngược lại?
tom_mai78101
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.