一天,小码鸽接到一个需求,需求大概是这样的:要求统计某个时间段的人工成本,前端传入统计的开始时间和结束时间,还有成本类型等一些其他的查询条件,后端根据前端的查询条件,返回汇总的人工成本。哎哟,这个需求很简单啦。阿鸽内心窃喜,马上撸起了袖子,一顿操作…
问题浮现阿鸽信心满满的的敲完了代码,觉得已经万无一失,准备和前端联调,可是接下来的一幕,瞬间让他上扬的嘴角僵持住了,他觉得不可能,他心态崩了。。。
控制台打印出了如下信息:
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 protectedObject 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 targetClass = (targetType instanceof Class ? (Class ) targetType : null); if (targetClass == null) { ResolvableType resolvableType = ResolvableType.forMethodParameter(parameter); targetClass = (Class ) 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 > converterType = (Class >) 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 ) 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注解



