环境SpringBoot-2.2.5
情景:
前端发送请求,通过将请求参数直接映射成实体的方式,参数
createTime对应的实体类属性类型为java.time.LocalDateTime,createTime传过来的值为'2020-10-01'的形式。 字段配置如下(把iso去掉也是一样会报错):
@DateTimeFormat(pattern = "yyyy-MM-dd",iso=DateTimeFormat.ISO.DATE)
private LocalDateTime createTime;
报错信息如下:
java Failed to convert property value of type 'java.lang.String' to required type 'java.time.LocalDateTime' for property 'createTime'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@com.fasterxml.jackson.annotation.JsonIgnore @org.springframework.format.annotation.DateTimeFormat java.time.LocalDateTime] for value '2020-10-01'; nested exception is java.lang.IllegalArgumentException: Parse attempt failed for value [2020-10-01]
网上的解决方法主要集中在讨论@org.springframework.format.annotation.DateTimeFormat和@com.fasterxml.jackson.annotation.JsonFormat的如何使用。
亲测发现对情景出现的情况无任何改观。
排查
对于需要将请求内容转化为指定对象的请求,会经过DispatcherServlet的层层调用,来到org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor#bindRequestParameters(WebDataBinder, NativeWebRequest)
protected void bindRequestParameters(WebDataBinder binder, NativeWebRequest request) {
ServletRequest servletRequest = request.getNativeRequest(ServletRequest.class);
Assert.state(servletRequest != null, "No ServletRequest");
ServletRequestDataBinder servletBinder = (ServletRequestDataBinder) binder;
servletBinder.bind(servletRequest);
}
接着
org.springframework.validation.DataBinder#doBind(MutablePropertyValues )
protected void doBind(MutablePropertyValues mpvs) {
checkAllowedFields(mpvs);
checkRequiredFields(mpvs);
applyPropertyValues(mpvs);
}
对于LocalDate这个字段,最终会执行到这里
@Override
org.springframework.format.datetime.standard.TemporalAccessorParser#parse()
public TemporalAccessor parse(String text, Locale locale) throws ParseException {
DateTimeFormatter formatterToUse = DateTimeContextHolder.getFormatter(this.formatter, locale);
if (LocalDate.class == this.temporalAccessorType) {
return LocalDate.parse(text, formatterToUse);
}
else if (LocalTime.class == this.temporalAccessorType) {
return LocalTime.parse(text, formatterToUse);
}
else if (LocalDateTime.class == this.temporalAccessorType) {
return LocalDateTime.parse(text, formatterToUse);
}
else if (ZonedDateTime.class == this.temporalAccessorType) {
return ZonedDateTime.parse(text, formatterToUse);
}
else if (OffsetDateTime.class == this.temporalAccessorType) {
return OffsetDateTime.parse(text, formatterToUse);
}
else if (OffsetTime.class == this.temporalAccessorType) {
return OffsetTime.parse(text, formatterToUse);
}
else {
throw new IllegalStateException("Unsupported TemporalAccessor type: " + this.temporalAccessorType);
}
}
@Override
public void registerFormatters(FormatterRegistry registry) {
DateTimeConverters.registerConverters(registry);
DateTimeFormatter df = getFormatter(Type.DATE);
DateTimeFormatter tf = getFormatter(Type.TIME);
DateTimeFormatter dtf = getFormatter(Type.DATE_TIME);
// Efficient ISO_LOCAL_* variants for printing since they are twice as fast...
registry.addFormatterForFieldType(LocalDate.class,
new TemporalAccessorPrinter(
df == DateTimeFormatter.ISO_DATE ? DateTimeFormatter.ISO_LOCAL_DATE : df),
new TemporalAccessorParser(LocalDate.class, df));
registry.addFormatterForFieldType(LocalTime.class,
new TemporalAccessorPrinter(
tf == DateTimeFormatter.ISO_TIME ? DateTimeFormatter.ISO_LOCAL_TIME : tf),
new TemporalAccessorParser(LocalTime.class, tf));
registry.addFormatterForFieldType(LocalDateTime.class,
new TemporalAccessorPrinter(
dtf == DateTimeFormatter.ISO_DATE_TIME ? DateTimeFormatter.ISO_LOCAL_DATE_TIME : dtf),
new TemporalAccessorParser(LocalDateTime.class, dtf));
...
}
public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) {
Assert.notNull(targetType, "Target type to convert to cannot be null");
if (sourceType == null) {
Assert.isTrue(source == null, "Source must be [null] if source type == [null]");
return handleResult(null, targetType, convertNullSource(null, targetType));
}
if (source != null && !sourceType.getObjectType().isInstance(source)) {
throw new IllegalArgumentException("Source to convert from must be an instance of [" +
sourceType + "]; instead it was a [" + source.getClass().getName() + "]");
}
GenericConverter converter = getConverter(sourceType, targetType);
// 当加上DateTimeFormat注解后这里边得到了一个FormattingConversionService$AnnotationParserConverter
// 方法进去会执行ConversionService.converterCache.get(key)
if (converter != null) {
Object result = ConversionUtils.invokeConverter(converter, source, sourceType, targetType);
return handleResult(sourceType, targetType, result);
}
return handleConverterNotFound(source, sourceType, targetType);
}
在FormattingConversionService#convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType)方法中
public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
Annotation ann = targetType.getAnnotation(this.annotationType);
if (ann == null) {
throw new IllegalStateException(
"Expected [" + this.annotationType.getName() + "] to be present on " + targetType);
}
AnnotationConverterKey converterKey = new AnnotationConverterKey(ann, targetType.getObjectType());
GenericConverter converter = cachedParsers.get(converterKey);
// 首次进来为空
if (converter == null) {
Parser<?> parser = this.annotationFormatterFactory.getParser(
converterKey.getAnnotation(), converterKey.getFieldType());
//这里开始创建covnerter
converter = new ParserConverter(this.fieldType, parser, FormattingConversionService.this);
cachedParsers.put(converterKey, converter);
}
return converter.convert(source, sourceType, targetType);
}
org.springframework.format.datetime.standard.TemporalAccessorParser#parse()
@Override
public TemporalAccessor parse(String text, Locale locale) throws ParseException {
DateTimeFormatter formatterToUse = DateTimeContextHolder.getFormatter(this.formatter, locale);
if (LocalDate.class == this.temporalAccessorType) {
return LocalDate.parse(text, formatterToUse);
}
else if (LocalTime.class == this.temporalAccessorType) {
return LocalTime.parse(text, formatterToUse);
}
else if (LocalDateTime.class == this.temporalAccessorType) {
return LocalDateTime.parse(text, formatterToUse);
}
else if (ZonedDateTime.class == this.temporalAccessorType) {
return ZonedDateTime.parse(text, formatterToUse);
}
else if (OffsetDateTime.class == this.temporalAccessorType) {
return OffsetDateTime.parse(text, formatterToUse);
}
else if (OffsetTime.class == this.temporalAccessorType) {
return OffsetTime.parse(text, formatterToUse);
}
else {
throw new IllegalStateException("Unsupported TemporalAccessor type: " + this.temporalAccessorType);
}
}
按照情景中情况,执行的是这句
else if (LocalDateTime.class == this.temporalAccessorType) {
return LocalDateTime.parse(text, formatterToUse);
}
即LocalDateTime.parse("2020-10-01", formatterToUse);
你会发现执行失败,因为默认状态下,formatterToUse使用的是DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT)(这个有待进一步验证),所以如果我反过来用这个style去format我的LocateDate是一直失败的。
DateTimeFormatter dtf =
DateTimeFormatter.ofLocalizedDateTime(FormatStyle.SHORT);
LocalDateTime date = LocalDateTime.now();
System.out.println(dtf.format(date));
//输出 20-11-14 下午7:25
//而且只能用LocalDateTime
总结
如果字段是LocalDate 那么pattern 需要指定为日期的格式 如 'yyyy-MM-dd',对应的字符串可以是yyyy-MM-dd 也可以是 yyyy-MM-dd HH:mm:ss
如果字段是LocalDatetime 那么patter需要指定为日期时间格式 如 yyyy-MM-dd HH:mm:ss 并严格对应字符串的内容,如果出现yyyy-MM-dd的字符串去format则会报错
延伸
//org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration$WebMvcAutoConfigurationAdapter#addFormatters()
@Override
public void addFormatters(FormatterRegistry registry) {
for (Converter<?, ?> converter : getBeansOfType(Converter.class)) {
registry.addConverter(converter);
}
for (GenericConverter converter : getBeansOfType(GenericConverter.class)) {
registry.addConverter(converter);
}
for (Formatter<?> formatter : getBeansOfType(Formatter.class)) {
registry.addFormatter(formatter);
}
}