SpringCloud采坑——Jason序列化中的Date对象

北蔡
提纲:

1.出现的场景。

2.报错内容和代码追踪。

3.原因。

4.三种解决方案。
  1. 出现的场景:
a.服务端提供一个springcloud接口。
b.客户端通过feign调用该接口,返回值为一个列表的CompanyDTO,DTO中有一个Date对象。
c.调用出错。
  1. 报错如下:

    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: java.io.PushbackInputStream@2041c1b0; 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: java.io.PushbackInputStream@2041c1b0; 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: java.io.PushbackInputStream@2041c1b0; 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
    

a.从AbstractJackson2HttpMessageConverter.java:225的readJavaType方法开始。
b.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);
    }

c.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;
    }
}

d.我们看下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

4.3解决方案3

在字段的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;

参考文章:

https://blog.csdn.net/qq906627950/article/details/79503801
http://wujiu.iteye.com/blog/2244537

    原文作者:qtshe
    原文地址: https://www.jianshu.com/p/770e1615bf25
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞