开心接活
一天,小码鸽接到一个需求,需求大概是这样的:要求统计某个时间段的人工成本,前端传入统计的开始时间和结束时间,还有成本类型等一些其他的查询条件,后端根据前端的查询条件,返回汇总的人工成本。哎哟,这个需求很简单啦。阿鸽内心窃喜,马上撸起了袖子,一顿操作…
问题浮现
阿鸽信心满满的的敲完了代码,觉得已经万无一失,准备和前端联调,可是接下来的一幕,瞬间让他上扬的嘴角僵持住了,他觉得不可能,他心态崩了。。。
控制台打印出了如下信息:
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。
返回的mappedHandler变量是一个HandlerExecutionChain类型的对象,大概瞄一眼是啥东西。
嗯?注释不是说确定handler的吗,怎么还夹带私货呢,不讲武德。那好吧,返回了handler加上拦截器信息,继续往下走。
哎,看注释这是要调用handler真正去处理请求了,点进去看下是不是真的。
这是个接口,有五个实现了该接口的类,有四个重写了handle方法,有这么多劳动力在这,那先看下的具体打工人吧,勤勤恳恳的"打工人"RequestMappingHandlerAdapter登场了。
哎呦,不错哟,RequestMappingHandlerAdapter一看就是妥妥的富二代了。
虽然从AbstractHandlerMethodAdapter那继承来了handle方法,但是RequestMappingHandlerAdapter需要自己去实现handleInternal方法才能干活RequestMappingHandlerAdapter说这不是坑娃嘛,不过没事,一代人有一代人的责任,这次我来上吧,经过它的不屑努力,终于实现了属于自己的一套绝技,而这套绝技的秘诀就是invokeHandlerMethod。
我们直奔主题,来揭开这神秘的面纱
凭直觉和语义推测,这invokeHandlerMethod方法里的invokeAndHandle调用的后面,应该就是真相了。这个invokeAndHandle方法在ServletInvocableHandlerMethod对象里,然后它又调用了InvocableHandlerMethod类的invokeForRequest,原来最后的boss是这个InvocableHandlerMethod
找到了最后boss,开始实施最后的逮捕。这个invokeForRequest方法里的getMethodArgumentValues方法调用很可疑,看它的名字就知道它有问题,锁定一号目标。
进一步调查发现它会委托参数解析器对参数进行解析,这次他请来的帮手是RequestResponseBodyMethodProcessor,由他来负责具体的解析任务。
完成这个解析任务依靠readWithMessageConverters方法调用。
这个方法还有个双胞胎兄弟,我们来认识认识
@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;
}
可惜这次作案没有成功,因为在解析请求体时抛出了最开始的小码鸽遇到的异常,导致了这次请求处理任务失败了,抛出异常的位置是这里
表面上看是springmvc无法将日期类型的字符串,转换为Date类型,实质上是readWithMessageConverters这里解析请求体出错了。
解决方法
- 前端传递数据时使用yy-MM-dd格式的时间字符串,后端用Date可以直接接收
- 在pojo实体的日期属性上加上@JsonFormat注解