严查前后端日期格式传值问题

开心接活

一天,小码鸽接到一个需求,需求大概是这样的:要求统计某个时间段的人工成本,前端传入统计的开始时间和结束时间,还有成本类型等一些其他的查询条件,后端根据前端的查询条件,返回汇总的人工成本。哎哟,这个需求很简单啦。阿鸽内心窃喜,马上撸起了袖子,一顿操作…

问题浮现

阿鸽信心满满的的敲完了代码,觉得已经万无一失,准备和前端联调,可是接下来的一幕,瞬间让他上扬的嘴角僵持住了,他觉得不可能,他心态崩了。。。
控制台打印出了如下信息:

WARN 14676 --- [nio-8763-exec-9] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2022-07-01 00:00:00": not a valid representation (error: Failed to parse Date value '2022-07-01 00:00:00': Cannot parse date "2022-07-01 00:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSX', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2022-07-01 00:00:00": not a valid representation (error: Failed to parse Date value '2022-07-01 00:00:00': Cannot parse date "2022-07-01 00:00:00": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSX', parsing fails (leniency? null))
 at [Source: (PushbackInputStream); line: 2, column: 14] (through reference chain: com.example.nacosconsumer.demo.dto.LaborCostQueryCondition["endDate"])]

原因

前端传递yy-MM-dd HH:mm:ss格式的时间字符串,后端只使用Date无法直接类型转换。这究竟是为啥呢,让我们开始定位问题。首先从DispathcherServlet类开始,我们来看看请求的处理过程。在DispathcherServlet类的doService方法中调用doDispatch方法,这个方法首先确定处理请求的handler。
file
返回的mappedHandler变量是一个HandlerExecutionChain类型的对象,大概瞄一眼是啥东西。
file
嗯?注释不是说确定handler的吗,怎么还夹带私货呢,不讲武德。那好吧,返回了handler加上拦截器信息,继续往下走。
file
哎,看注释这是要调用handler真正去处理请求了,点进去看下是不是真的。
filefile
这是个接口,有五个实现了该接口的类,有四个重写了handle方法,有这么多劳动力在这,那先看下的具体打工人吧,勤勤恳恳的"打工人"RequestMappingHandlerAdapter登场了。
file
哎呦,不错哟,RequestMappingHandlerAdapter一看就是妥妥的富二代了。
file
file
虽然从AbstractHandlerMethodAdapter那继承来了handle方法,但是RequestMappingHandlerAdapter需要自己去实现handleInternal方法才能干活RequestMappingHandlerAdapter说这不是坑娃嘛,不过没事,一代人有一代人的责任,这次我来上吧,经过它的不屑努力,终于实现了属于自己的一套绝技,而这套绝技的秘诀就是invokeHandlerMethod。
file
我们直奔主题,来揭开这神秘的面纱
file
凭直觉和语义推测,这invokeHandlerMethod方法里的invokeAndHandle调用的后面,应该就是真相了。这个invokeAndHandle方法在ServletInvocableHandlerMethod对象里,然后它又调用了InvocableHandlerMethod类的invokeForRequest,原来最后的boss是这个InvocableHandlerMethod
file
file
file
找到了最后boss,开始实施最后的逮捕。这个invokeForRequest方法里的getMethodArgumentValues方法调用很可疑,看它的名字就知道它有问题,锁定一号目标。
file
进一步调查发现它会委托参数解析器对参数进行解析,这次他请来的帮手是RequestResponseBodyMethodProcessor,由他来负责具体的解析任务。
file
完成这个解析任务依靠readWithMessageConverters方法调用。
file
这个方法还有个双胞胎兄弟,我们来认识认识

@Nullable
protected <T> Object readWithMessageConverters(HttpInputMessage inputMessage, MethodParameter parameter,Type targetType) throws IOException, HttpMediaTypeNotSupportedException, HttpMessageNotReadableException {
		MediaType contentType;
		boolean noContentType = false;
		try {
			contentType = inputMessage.getHeaders().getContentType();
		}
		catch (InvalidMediaTypeException ex) {
			throw new HttpMediaTypeNotSupportedException(ex.getMessage());
		}
		if (contentType == null) {
			noContentType = true;
			contentType = MediaType.APPLICATION_OCTET_STREAM;
		}

		Class<?> contextClass = parameter.getContainingClass();
		Class<T> targetClass = (targetType instanceof Class ? (Class<T>) targetType : null);
		if (targetClass == null) {
			ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter);
			targetClass = (Class<T>) resolvableType.resolve();
		}

		HttpMethod httpMethod = (inputMessage instanceof HttpRequest ? ((HttpRequest) inputMessage).getMethod() : null);
		Object body = NO_VALUE;

		EmptyBodyCheckingHttpInputMessage message;
		try {
			message = new EmptyBodyCheckingHttpInputMessage(inputMessage);

			for (HttpMessageConverter<?> converter : this.messageConverters) {
				Class<HttpMessageConverter<?>> converterType = (Class<HttpMessageConverter<?>>) converter.getClass();
				GenericHttpMessageConverter<?> genericConverter =
						(converter instanceof GenericHttpMessageConverter ? (GenericHttpMessageConverter<?>) converter : null);
				if (genericConverter != null ? genericConverter.canRead(targetType, contextClass, contentType) :
						(targetClass != null && converter.canRead(targetClass, contentType))) {
					if (message.hasBody()) {
						HttpInputMessage msgToUse =
								getAdvice().beforeBodyRead(message, parameter, targetType, converterType);
						body = (genericConverter != null ? genericConverter.read(targetType, contextClass, msgToUse) :
								((HttpMessageConverter<T>) converter).read(targetClass, msgToUse));
						body = getAdvice().afterBodyRead(body, msgToUse, parameter, targetType, converterType);
					}
					else {
						body = getAdvice().handleEmptyBody(null, message, parameter, targetType, converterType);
					}
					break;
				}
			}
		}
		catch (IOException ex) {
			throw new HttpMessageNotReadableException("I/O error while reading input message", ex, inputMessage);
		}

		if (body == NO_VALUE) {
			if (httpMethod == null || !SUPPORTED_METHODS.contains(httpMethod) ||
					(noContentType && !message.hasBody())) {
				return null;
			}
			throw new HttpMediaTypeNotSupportedException(contentType, this.allSupportedMediaTypes);
		}

		MediaType selectedContentType = contentType;
		Object theBody = body;
		LogFormatUtils.traceDebug(logger, traceOn -> {
			String formatted = LogFormatUtils.formatValue(theBody, !traceOn);
			return "Read \"" + selectedContentType + "\" to [" + formatted + "]";
		});

		return body;
	}

可惜这次作案没有成功,因为在解析请求体时抛出了最开始的小码鸽遇到的异常,导致了这次请求处理任务失败了,抛出异常的位置是这里

file
表面上看是springmvc无法将日期类型的字符串,转换为Date类型,实质上是readWithMessageConverters这里解析请求体出错了。

解决方法

  1. 前端传递数据时使用yy-MM-dd格式的时间字符串,后端用Date可以直接接收
  2. 在pojo实体的日期属性上加上@JsonFormat注解
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值