springcloud採坑-jason序列化中的Date物件
阿新 • • 發佈:2019-01-11
提綱:
1.出現的場景。
2.報錯內容和程式碼追蹤。
3.原因。
4.三種解決方案。
1. 出現的場景:
- 服務端提供一個springcloud介面。
- 客戶端通過feign呼叫該介面,返回值為一個列表的CompanyDTO,DTO中有一個Date物件。
- 調用出錯。
2. 報錯如下:
Caused by: org.springframework.http.converter.HttpMessageNotReadableException: Could not read document: Can not deserialize value of type java.util.Date from String "2017-12-08 09:20:53": not a valid representation (error: Failed to parse Date value '2017-12-08 09:20:53': Can not parse date "2017-12-08 09:20:53Z": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSS'Z'', parsing fails (leniency? null))
at [Source: [email protected]; line: 1, column: 554] (through reference chain: com.qts.base.result.ResponseData["data"]->java.util.ArrayList[0]->com.qts.company.api.dto.CompanyDTO["applyRefundTime"]); nested exception is com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not deserialize value of type java.util.Date from String "2017-12-08 09:20:53": not a valid representation (error: Failed to parse Date value '2017-12-08 09:20:53': Can not parse date "2017-12-08 09:20:53Z": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSS'Z'', parsing fails (leniency? null))
at [Source: [email protected]; line: 1, column: 554] (through reference chain: com.qts.base.result.ResponseData["data"]->java.util.ArrayList[0]->com.qts.company.api.dto.CompanyDTO["applyRefundTime"])
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:228)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.read(AbstractJackson2HttpMessageConverter.java:213)
at org.springframework.web.client.HttpMessageConverterExtractor.extractData(HttpMessageConverterExtractor.java:95)
at org.springframework.cloud.netflix.feign.support.SpringDecoder.decode(SpringDecoder.java:59)
at org.springframework.cloud.netflix.feign.support.ResponseEntityDecoder.decode(ResponseEntityDecoder.java:47)
at feign.SynchronousMethodHandler.decode(SynchronousMethodHandler.java:165)
... 75 more
Caused by: com.fasterxml.jackson.databind.exc.InvalidFormatException: Can not deserialize value of type java.util.Date from String "2017-12-08 09:20:53": not a valid representation (error: Failed to parse Date value '2017-12-08 09:20:53': Can not parse date "2017-12-08 09:20:53Z": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSS'Z'', parsing fails (leniency? null))
at [Source: [email protected]; line: 1, column: 554] (through reference chain: com.qts.base.result.ResponseData["data"]->java.util.ArrayList[0]->com.qts.company.api.dto.CompanyDTO["applyRefundTime"])
at com.fasterxml.jackson.databind.exc.InvalidFormatException.from(InvalidFormatException.java:74)
at com.fasterxml.jackson.databind.DeserializationContext.weirdStringException(DeserializationContext.java:1410)
at com.fasterxml.jackson.databind.DeserializationContext.handleWeirdStringValue(DeserializationContext.java:926)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:822)
at com.fasterxml.jackson.databind.deser.std.StdDeserializer._parseDate(StdDeserializer.java:791)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateBasedDeserializer._parseDate(DateDeserializers.java:172)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:259)
at com.fasterxml.jackson.databind.deser.std.DateDeserializers$DateDeserializer.deserialize(DateDeserializers.java:242)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:499)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:101)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:357)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:148)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:277)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:249)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:26)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:499)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:101)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:357)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:148)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:3789)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2913)
at org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter.readJavaType(AbstractJackson2HttpMessageConverter.java:225)
... 80 more
- 從AbstractJackson2HttpMessageConverter.java:225的readJavaType方法開始。
- DateDeserializers.java的deserialize中,有一段核心邏輯:this._customFormat.parse(str)。問題來了:_customFormat是從哪裡來的?
protected Date _parseDate(JsonParser p, DeserializationContext ctxt) throws IOException {
if (this._customFormat != null) {
JsonToken t = p.getCurrentToken();
if (t == JsonToken.VALUE_STRING) {
String str = p.getText().trim();
if (str.length() == 0) {
return (Date)this.getEmptyValue(ctxt);
}
DateFormat var5 = this._customFormat;
synchronized(this._customFormat) {
Date var10000;
try {
var10000 = this._customFormat.parse(str);//重點
} catch (ParseException var8) {
return (Date)ctxt.handleWeirdStringValue(this.handledType(), str, "expected format \"%s\"", new Object[]{this._formatString});
}
return var10000;
}
}
}
return super._parseDate(p, ctxt);
}
- DateBasedDeserializer中的_customFormat,通過ctxt.getConfig()獲取,DeserializationContext是配置的上下文。
protected abstract static class DateBasedDeserializer<T> extends StdScalarDeserializer<T> implements ContextualDeserializer { protected final DateFormat _customFormat; protected final String _formatString; protected abstract DateDeserializers.DateBasedDeserializer<T> withDateFormat(DateFormat var1, String var2); public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) throws JsonMappingException { if (property != null) { Value format = this.findFormatOverrides(ctxt, property, this.handledType()); if (format != null) { TimeZone tz = format.getTimeZone(); Locale loc; if (format.hasPattern()) { String pattern = format.getPattern(); loc = format.hasLocale() ? format.getLocale() : ctxt.getLocale(); SimpleDateFormat df = new SimpleDateFormat(pattern, loc); if (tz == null) { tz = ctxt.getTimeZone(); } df.setTimeZone(tz); return this.withDateFormat(df, pattern); } if (tz != null) { DateFormat df = ctxt.getConfig().getDateFormat();//重點 Object df; if (df.getClass() == StdDateFormat.class) { loc = format.hasLocale() ? format.getLocale() : ctxt.getLocale(); StdDateFormat std = (StdDateFormat)df; std = std.withTimeZone(tz); std = std.withLocale(loc); df = std; } else { df = (DateFormat)df.clone(); ((DateFormat)df).setTimeZone(tz); } return this.withDateFormat((DateFormat)df, this._formatString); } } } return this; } }
- 我們看下getDateFormat的具體實現,最終是通過MapperConfig中的BaseSettings獲取。
public final DateFormat getDateFormat() {
return this._base.getDateFormat();
}
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設定客戶端的DateFormat。
public class MyDateFormat extends DateFormat {
private DateFormat dateFormat;
private SimpleDateFormat format1 = 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 = format1.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 = format1.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);
}
}
並在啟動的時候加入。
@Configuration
public class WebConfig {
@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;
}
}
4.2解決方案2可以通過程式碼,那就可以通過配置。。。
#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:ss
#spring.jackson.deserialization.accept_empty_string_as_null_object=true
4.3解決方案3
在欄位的setter上加上註解,這種方法比較麻煩。
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;
參考文章:
https://blog.csdn.net/qq906627950/article/details/79503801
http://wujiu.iteye.com/blog/2244537