踩坑一:调用 Feign 接口 Date 转换出错
原创2019/1/14大约 4 分钟
踩坑一:调用 Feign 接口 Date 转换出错
1、业务背景
使用 Spring Cloud 框架,项目 A(Feign 调用方,Eureka 调用端),项目 B(Feign 实现方,Eureka 客户端)
2、问题现象
项目 A 页面传递时间类型的参数到 A 的 Controller,然后 A 的 Controller 方法使用 Feign 调用 B 的 Controller 接口时,B 端报时间转换异常:
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2018-12-27 20:20:58": not a valid representation (error: Failed to parse Date value '2018-12-27 20:20:58': Cannot parse date "2018-12-27 20:20:58": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2018-12-27 20:20:58": not a valid representation (error: Failed to parse Date value '2018-12-27 20:20:58': Cannot parse date "2018-12-27 20:20:58": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null))
at [Source: (PushbackInputStream); line: 4, column: 15] (through reference chain: com.marry.common.dto.cms.BlockItemDto["createTime"])
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:245)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:227)
at org.springframework.web.servlet.mvc.method.annotation.AbstractMessageConverterMethodArgumentResolver.readWithMessageConverters(AbstractMessageConverterMethodArgumentResolver.java:204)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.readWithMessageConverters(RequestResponseBodyMethodProcessor.java:157)
at org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor.resolveArgument(RequestResponseBodyMethodProcessor.java:130)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:126)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:166)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:134)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:102)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:895)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:800)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1038)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:942)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1005)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:908)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:665)
at org.springframework.web.servlet.FrameworkServlet.service(FrameworkServlet.java:882)
at javax.servlet.http.HttpServlet.service(HttpServlet.java:750)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:231)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:53)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.RequestContextFilter.doFilterInternal(RequestContextFilter.java:99)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.FormContentFilter.doFilterInternal(FormContentFilter.java:92)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:93)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:200)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:193)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:166)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:199)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:96)
at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:490)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:139)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:92)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:74)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:343)
at org.apache.coyote.http11.Http11Processor.service(Http11Processor.java:408)
at org.apache.coyote.AbstractProcessorLight.process(AbstractProcessorLight.java:66)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:791)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1417)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2018-12-27 20:20:58": not a valid representation (error: Failed to parse Date value '2018-12-27 20:20:58': Cannot parse date "2018-12-27 20:20:58": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null))
at [Source: (PushbackInputStream); line: 4, column: 15] (through reference chain: com.marry.common.dto.cms.BlockItemDto["createTime"])
at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:67)
at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1549)
at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:911)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:524)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:467)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateBasedDeserializer._parseDate(DateDeserializers.java:195)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:285)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:268)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:127)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:369)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:159)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4013)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3084)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:239)
... 55 common frames omitted
2019-01-14 11:53:01.956 [marry] [http-nio-8888-exec-3] WARN [org.springframework.web.servlet.mvc.method.annotation.ExceptionHandlerExceptionResolver.logException:194] - Resolved [org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.util.Date` from String "2018-12-27 20:20:58": not a valid representation (error: Failed to parse Date value '2018-12-27 20:20:58': Cannot parse date "2018-12-27 20:20:58": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null)); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Cannot deserialize value of type `java.util.Date` from String "2018-12-27 20:20:58": not a valid representation (error: Failed to parse Date value '2018-12-27 20:20:58': Cannot parse date "2018-12-27 20:20:58": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', parsing fails (leniency? null))
at [Source: (PushbackInputStream); line: 4, column: 15] (through reference chain: com.marry.common.dto.cms.BlockItemDto["createTime"])]3、定位原因
Jason 仅支持以下几种格式的时间:
"yyyy-MM-dd'T'HH:mm:ss.SSSZ"
"yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
"EEE, dd MMM yyyy HH:mm:ss zzz"
"yyyy-MM-dd"4、解决方案
4.1、简单属性配置
在 application.properties 中添加如下配置(亲测可用):
# mvc date convert config
spring.mvc.date-format=yyyy-MM-dd HH:mm:ss
spring.jackson.time-zone=GMT+8
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss4.2、使用配置类
使用如下配置类(亲测可用):
package com.marry.common.conf;
import java.text.DateFormat;
import java.text.FieldPosition;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.util.Date;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* 时间类型转换配置
*
* @author Jastar Wang
* @date 2019-01-14
* @version 1.0
*/
@Configuration
public class JsonDateConfig {
@Autowired
private Jackson2ObjectMapperBuilder jackson2ObjectMapperBuilder;
@Bean
public MappingJackson2HttpMessageConverter MappingJsonpHttpMessageConverter() {
ObjectMapper mapper = jackson2ObjectMapperBuilder.build();
// ObjectMapper为了保障线程安全性,里面的配置类都是一个不可变的对象
// 所以这里的setDateFormat的内部原理其实是创建了一个新的配置类
DateFormat dateFormat = mapper.getDateFormat();
mapper.setDateFormat(new MyDateFormat(dateFormat));
MappingJackson2HttpMessageConverter mappingJsonpHttpMessageConverter = new MappingJackson2HttpMessageConverter(
mapper);
return mappingJsonpHttpMessageConverter;
}
}
/**
*
* 自定义时间格式转换
*
* @author Jastar Wang
* @date 2019-01-14
* @version 1.0
*/
class MyDateFormat extends DateFormat {
private static final long serialVersionUID = -1873228667541623675L;
private DateFormat dateFormat;
private SimpleDateFormat format = new SimpleDateFormat("yyy-MM-dd HH:mm:ss");
public MyDateFormat(DateFormat dateFormat) {
this.dateFormat = dateFormat;
}
@Override
public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
return dateFormat.format(date, toAppendTo, fieldPosition);
}
@Override
public Date parse(String source, ParsePosition pos) {
Date date = null;
try {
date = format.parse(source, pos);
} catch (Exception e) {
date = dateFormat.parse(source, pos);
}
return date;
}
/*
* 主要还是装饰这个方法
*/
@Override
public Date parse(String source) throws ParseException {
Date date = null;
try {
// 先按我的规则来
date = format.parse(source);
} catch (Exception e) {
// 不行,那就按原先的规则吧
date = dateFormat.parse(source);
}
return date;
}
/*
* 这里装饰clone方法的原因是因为clone方法在jackson中也有用到
*/
@Override
public Object clone() {
Object format = dateFormat.clone();
return new MyDateFormat((DateFormat) format);
}
}4.3、在 Date 类型字段上加注解
在字段的 setter 上对 Date 对象增加注解,这种方法比较麻烦。
public class DateJsonDeserializer extends JsonDeserializer<Date> {
/**
* @see JsonDeserializer#deserialize(JsonParser,
* DeserializationContext)
*/
@Override
public Date deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
try {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
return sdf.parse(parser.getValueAsString());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}然后在 DTO 的字段上增加注解:
@JsonDeserialize(using = DateJsonDeserializer.class)
private Date applyRefundTime;
