从@DateTimeFormat不生效出发认识DataBinder

bingo 2020年11月01日 37次浏览

环境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);
	}
}