Spring将@RequestBody中的LocalDate反序列化与@RequestParam中的LocalDate不同-为什么,它们可以相同吗?

dre*_*rew 8 java serialization spring json java-8

问题:Spring似乎使用不同的反序列化方法,LocalDate具体取决于它出现在一个@RequestBody请求中还是一个请求中@ReqestParam-这是否正确?如果正确,是否可以将它们配置为在整个应用程序中相同?

背景:在我的中@RestController,我有两种方法-一种GET和一种POST。GET期望类型为LocalDate; 的请求参数(“日期”);POST需要一个JSON对象,其中一个键(“日期”)的类型为LocalDate。它们的签名类似于以下内容:

@RequestMapping(value = "/entity", method = RequestMethod.GET)
public EntityResponse get(
       Principal principal,
       @RequestParam(name = "date", required = false) LocalDate date) 

@RequestMapping(value = "/entity", method = RequestMethod.POST)
public EntityResponse post(
       Principal principal,
       @RequestBody EntityPost entityPost)

public class EntityPost {
       public LocalDate date;
}
Run Code Online (Sandbox Code Playgroud)

我已经将我的ObjectMapper配置如下:

@Bean
public ObjectMapper objectMapper() {

   ObjectMapper objectMapper = new ObjectMapper();
   objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
   objectMapper.registerModule(new JavaTimeModule());
   objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

   return objectMapper;
}
Run Code Online (Sandbox Code Playgroud)

这样可以确保系统接受LocalDateyyyy-MM-dd格式,并按预期的方式反序列化它-至少在它是a的一部分时@RequestBody。因此,如果以下是POST的请求正文

{
"date": 2017-01-01
}
Run Code Online (Sandbox Code Playgroud)

系统将请求主体反序列EntityPost化为预期的内容。

但是,该配置不适用于的反序列化@RequestParam。结果,这失败了:

// fail!
/entity?date=2017-01-01
Run Code Online (Sandbox Code Playgroud)

而是,系统似乎期望格式为MM / dd / yy。结果,此操作成功完成:

// success!
/entity?date=01/01/17
Run Code Online (Sandbox Code Playgroud)

我知道我可以使用@DateTimeFormat批注在每个参数的基础上进行更改。我知道,如果我按如下方式更改GET方法的签名,它将接受第一种格式:

@RequestMapping(value = "/entity", method = RequestMethod.GET)
public EntityResponse get(
       Principal principal,
       @RequestParam(name = "date", required = false) @DateTimeFormat(iso=DateTimeFormat.ISO.DATE) LocalDate date) 
Run Code Online (Sandbox Code Playgroud)

但是,如果我不必为的每次使用都包含注释,则希望使用LocalDate。有什么方法可以全局设置此值,以便系统以相同的方式反序列化每个@RequestParam类型LocalDate

以供参考:

我正在使用Spring 4.3.2.RELEASE

我正在使用Jackson 2.6.5

Dor*_*use 6

为LocalDate创建格式化程序:

public class LocalDateFormatter implements Formatter<LocalDate> {

    @Override
    public LocalDate parse(String text, Locale locale) throws ParseException {
        return LocalDate.parse(text, DateTimeFormatter.ISO_DATE);
    }

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

Spring 5+: 注册格式化程序:WebMvcConfigurer在中实现@Configuration并覆盖addFormatters

@Override
public void addFormatters(FormatterRegistry registry) {
    registry.addFormatter(new LocalDateFormatter());
}
Run Code Online (Sandbox Code Playgroud)

Spring Boot:定义一个@Primary @Bean以覆盖默认格式化程序:

@Bean
@Primary
public Formatter<LocalDate> localDateFormatter() {
    return new LocalDateFormatter();
}
Run Code Online (Sandbox Code Playgroud)


dre*_*rew 5

根据评论中的@Andreas,Spring 框架使用 Jackson 反序列化,@RequestBody但 Spring 本身反序列化@RequestParam. 这就是两者差异的根源。

答案显示了如何使用@ControllerAdvice@InitBinder自定义@RequestParam. 我最终使用的代码如下:

import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;

import java.beans.PropertyEditorSupport;
import java.text.Format;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.function.Function;

@ControllerAdvice
public class ControllerAdviceInitBinder {

    private static class Editor<T> extends PropertyEditorSupport {

        private final Function<String, T> parser;
        private final Format format;

        public Editor(Function<String, T> parser, Format format) {

            this.parser = parser;
            this.format = format;
        }

        public void setAsText(String text) {

            setValue(this.parser.apply(text));
        }

        public String getAsText() {

            return format.format((T) getValue());
        }
    }

    @InitBinder
    public void initBinder(WebDataBinder webDataBinder) {

        webDataBinder.registerCustomEditor(
                Instant.class,
                new Editor<>(
                        Instant::parse,
                        DateTimeFormatter.ISO_INSTANT.toFormat()));

        webDataBinder.registerCustomEditor(
                LocalDate.class,
                new Editor<>(
                        text -> LocalDate.parse(text, DateTimeFormatter.ISO_LOCAL_DATE),
                        DateTimeFormatter.ISO_LOCAL_DATE.toFormat()));

        webDataBinder.registerCustomEditor(
                LocalDateTime.class,
                new Editor<>(
                        text -> LocalDateTime.parse(text, DateTimeFormatter.ISO_LOCAL_DATE_TIME),
                        DateTimeFormatter.ISO_LOCAL_DATE_TIME.toFormat()));

        webDataBinder.registerCustomEditor(
                LocalTime.class,
                new Editor<>(
                        text -> LocalTime.parse(text, DateTimeFormatter.ISO_LOCAL_TIME),
                        DateTimeFormatter.ISO_LOCAL_TIME.toFormat()));

        webDataBinder.registerCustomEditor(
                OffsetDateTime.class,
                new Editor<>(
                        text -> OffsetDateTime.parse(text, DateTimeFormatter.ISO_OFFSET_DATE_TIME),
                        DateTimeFormatter.ISO_OFFSET_DATE_TIME.toFormat()));

        webDataBinder.registerCustomEditor(
                OffsetTime.class,
                new Editor<>(
                        text -> OffsetTime.parse(text, DateTimeFormatter.ISO_OFFSET_TIME),
                        DateTimeFormatter.ISO_OFFSET_TIME.toFormat()));

        webDataBinder.registerCustomEditor(
                ZonedDateTime.class,
                new Editor<>(
                        text -> ZonedDateTime.parse(text, DateTimeFormatter.ISO_ZONED_DATE_TIME),
                        DateTimeFormatter.ISO_ZONED_DATE_TIME.toFormat()));
    }
}
Run Code Online (Sandbox Code Playgroud)