1. 程式人生 > >springcloud採坑-jason序列化中的Date物件

springcloud採坑-jason序列化中的Date物件

提綱:

    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