1、问题描述
在某个功能中,后端使用@RequestBody接收前端的参数
@ApiOperation("/获取时间信息")
@PostMapping("/getDate")
public Object getDate(@RequestBody User user){
System.out.println("action method: " + user.getDate().getTime());
return user.getDate().getTime()+"";
}
在swwager中测试接口
{
"age": 36,
"date": "2022-10-26 10:21:04",
"name": "张三"
}
实体类
@Data
public class User {
private String name;
private Integer age;
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date date;
}
后端服务报错如下:Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2022-10-26 10:21:04": not a valid representation (error: Failed to parse Date value '2022-10-26 10:21:04': Cannot parse date "2022-10-26 10:21:04": 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-10-26 10:21:04": not a valid representation (error: Failed to parse Date value '2022-10-26 10:21:04': Cannot parse date "2022-10-26 10:21:04": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSX', parsing fails (leniency? null))<EOL> at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 3, column: 11] (through reference chain: com.prj.pojo.entity.User["date"])]
2、代码环境:
springboot 2.6.9,java8,idea 2020.3
3、代码跟踪:
大家都知道,所有的请求后端的逻辑都会进入DispatcherServlet的doDispatch方法
逐步断点调试如下图,发现invocableMethod就是我的controller,继续断点进入
ServletInvocableHandlerMethod的invokeAndHandle方法就是执行controller的逻辑
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 解析参数并执行controller逻辑
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
mavContainer.setRequestHandled(false);
Assert.state(this.returnValueHandlers != null, "No return value handlers");
// 处理返回值
try {
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
}catch (Exception ex) {
if (logger.isTraceEnabled()){
logger.trace(formatErrorForReturnValue(returnValue), ex);
}
throw ex;
}
}
进入invokeForRequest方法
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
//解析参数
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
//执行controller方法
return doInvoke(args);
}
因为是找寻解析参数失败的原因,所以进入getMethodArgumentValues方法,第179行是具体的解析逻辑,进入
继续断点进入,在第139行的validateIfApplicable(binder, parameter);是参数校验的逻辑,如果controller层的接口有@Validated的注解,会在这里进行判断。
在下图中的for循环中的genericConverter.canRead是HttpMessageConverter的方法,用来判断能否转为java对象
最后发现进入AbstractJackson2HttpMessageConverter类
该类也是HttpMessageConverter的子类
中间是一些判断字符集,继续断点进入
断点进入后发现ObjectMapper是com.fasterxml.jackson.databind包下的
一路断点到BeanDeserializer类,该方法较长,从386行到398行是一个dowhile循环
在p变量中找到了我们在swagger中传入的参数类型,dowhile中会挨个遍历,然后转换为对应的实体类属性类型
重点来了,当propName是date时,断点进入
下图中df其实是StdDateFormat
打开StdDateFormat类,发现仅支持这几种时间转换格式。
我在swagger传入的2022-10-26 10:21:04类型,因此直接报错
4、解决办法:
既然找到问题了,解决也就很容易了
@Configuration
public class LocalDateTimeSerializerConfig {
private static final String DATE_PATTERN = "yyyy-MM-dd HH:mm:ss";
@Bean
public Jackson2ObjectMapperBuilder mapperBuilder() {
Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder = new Jackson2ObjectMapperBuilder();
jackson2ObjectMapperBuilder.dateFormat(new SimpleDateFormat(DATE_PATTERN))
.serializerByType(LocalDateTime.class, localDateTimeSerializer())
.deserializerByType(LocalDateTime.class, localDateTimeDeserializer());
return jackson2ObjectMapperBuilder;
}
@Bean
public LocalDateTimeSerializer localDateTimeSerializer() {
return new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_PATTERN));
}
@Bean
public LocalDateTimeDeserializer localDateTimeDeserializer() {
return new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_PATTERN));
}
}
再看下图中df的值,已经变成SimpleDateFormat了,因为在上面的mapperBuilder()方法中,我指定了SimpleDateFormat和对应的解析格式。
下面第一张图是出现转换失败时的MappingJackson2HttpMessageConverter,
第二张图是添加了LocalDateTimeSerializerConfig的MappingJackson2HttpMessageConverter,很明显,第二张图已经替换为我添加的SimpleDateFormat转换器了。
springboot的思想是约定优于配置,也就是说,springboot默认帮我们配好了spring mvc的Converter,如果我们没有自定义Converter的话,那么框架就会帮我们创建一个,如果我们有自定义的话,那么springboot就直接使用我们所注册的bean进行绑定
以上摘自https://blog.csdn.net/qq906627950/article/details/79503801