[Jackson序列化(4)— Jackson“默认的”时间格式化类—StdDateFormat解析

Jackson序列化(1)— [SpringBoot2.x]-Jackson在HttpMessageConverter(消息转换器)中的使用
Jackson序列化(2)— [SpringBoot2.x]-Spring容器中ObjectMapper配置
Jackson序列化(3)— Jackson中ObjectMapper配置详解
Jackson序列化(4)— Jackson“默认的”时间格式化类—StdDateFormat解析
Jackson序列化(5) — Jackson的ObjectMapper.DefaultTyping.NON_FINAL属性
Jackson序列化(6)— Java使用Jackson进行序列化

1. SpringBoot2.x环境下默认的日期格式

在SpringBoot2.x环境下,默认情况下使用com.fasterxml.jackson.databind.util.StdDateFormat类对Date时间类进行格式化和反序列化,效果如下图所示:

{"id":1001,"userName":"李白","password":"123456","age":12,"date":"2020-02-13T06:44:27.417+0000"}
UserT{id=1001, userName='李白', password='123456', age=12, date=Thu Feb 13 14:44:27 CST 2020}

此处需要注意:Json中date与Object对象中的date相差8个小时的时差。

测试代码如下:

@RestController
@Slf4j
public class ObjectMapperController {
    @Autowired //获取Spring容器中的ObjectMapper对象
    private ObjectMapper objectMapper;
    public static UserT userT;

    static {
        userT = new UserT();
        userT.setId(1001);
        userT.setUserName("李白");
        userT.setAge(12);
        userT.setPassword("123456");
        userT.setDate(new Date());
    }

    @RequestMapping("/testJackson")
    public String insertUser() {
        String result = null;
        try {
            result = objectMapper.writeValueAsString(userT);
            UserT userT = objectMapper.readValue(result, UserT.class);
            System.out.println(result);
            System.out.println(userT.toString());
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        //JSON串再次解析为Object对象
        return result;
    }
}

ObjectMapper提供了DateFormat参数来处理Date类型的格式化,默认情况下,Jackson使用com.fasterxml.jackson.databind.util.StdDateFormat类进行格式化。

时间格式为:yyyy-MM-dd'T'HH:mm:ss.SSSZ,为ISO-8601数据类型。

若是请求上送的Date格式为:yyyy-MM-dd HH:mm:ss而SpringBoot使用默认的Jackson配置会出现异常:

Cannot deserialize value of type `java.util.Date` from String \"2020-04-20 20:30:00\": 
not a valid representation (error: Failed to parse Date value '2020-04-20 20:30:00': Cannot parse date \"2020-04-20 20:30:00\": while it seems to fit format 'yyyy-MM-dd'T'HH:mm:ss.SSSZ', 
parsing fails (leniency? null))\n at [Source: (PushbackInputStream); line: 1, column: 477] 
(through reference chain: ....}

解决方案:

方案1. 在yml配置中进行配置:

spring:
  jackson:
    date-format: yyyy-MM-dd HH:mm:ss # 默认是StdDateFormat
    time-zone: GMT+8

方案二.在实体类参数使用注解

@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss",timezone = "GMT+8")

2. StdDateFormat类解析

该类实现了DateFormat接口,提供了对Date日期的序列化与反序列化的程序。

  1. 序列化:默认使用ISO-8601的格式(会将Date类型格式成“yyyy-MM-dd'T'HH:MM:ss.SSSZ”字符串类型)。
  2. 反序列化:可以解析ISO-8601(yyyy-MM-dd'T'HH:mm:ss.SSSZ)和RFC-1123(yyyy-MM-dd)类型的字符串。
public class StdDateFormat
    extends DateFormat{
    //ISO-8601时间格式的字符串
    public final static String DATE_FORMAT_STR_ISO8601 = "yyyy-MM-dd'T'HH:mm:ss.SSSZ";
    /**
     * ISO-8601,只有日期部分,没有时间:错误消息需要
     */
    protected final static String DATE_FORMAT_STR_PLAIN = "yyyy-MM-dd";

    /**
     * 定义的是符合 RFC 1123 / RFC 822的日期格式。
     */
    protected final static String DATE_FORMAT_STR_RFC1123 = "EEE, dd MMM yyyy HH:mm:ss zzz";
    /**
     * 默认时区 UTC,在Jackson2.6之前使用的是GMT。这就是为什么我们格式化后的时间时差相差8小时的原因,因为北京时间与美国时间,相差8小时!
     */
    protected final static TimeZone DEFAULT_TIMEZONE;
    static {
        DEFAULT_TIMEZONE = TimeZone.getTimeZone("UTC"); // since 2.7
    }
    //默认的地区:美国
    protected final static Locale DEFAULT_LOCALE = Locale.US;
    //程序中会将Date日期转换为Calendar格式,以便处理日期类型。由于线程安全的问题,不能直接使用CALENDAR去格式化数据。
    //一般通过clone获取Calendar 实例,他会比new Calendar更有效率。
    protected static final Calendar CALENDAR = new GregorianCalendar(DEFAULT_TIMEZONE, DEFAULT_LOCALE);
    //设置时区
    protected transient TimeZone _timezone;
    //设置地区
    protected final Locale _locale;
    //是否允许宽松的时间格式
    protected Boolean _lenient;
    //2.9.1版本:TZ偏移量是否必须在小时和分钟之间用冒号格式化({@code HH:mm}格式)
    private boolean _tzSerializedWithColon = false;
    //构造方法,创建StdDateFormat对象
    public StdDateFormat() {
        _locale = DEFAULT_LOCALE;
    }
    /**
     * 2.9.1版本后才存在的构造方法 lenient:是否允许宽松的时间格式;
     *  formatTzOffsetWithColon 格式化时是否允许Tz的偏移
     */
    protected StdDateFormat(TimeZone tz, Locale loc, Boolean lenient,
            boolean formatTzOffsetWithColon) {
        _timezone = tz;
        _locale = loc;
        _lenient = lenient;
        _tzSerializedWithColon = formatTzOffsetWithColon;
    }    
    //该方法用于创建带有特殊时区的StdDateFormat实例对象,若时区不是特殊的,返回默认的StdDateFormat对象(UTC时区)
    // withLenient(Boolean b) 方法和withLocale(Locale loc)方法withColonInTimeZone(boolean b) 同理
    public StdDateFormat withTimeZone(TimeZone tz) {
        if (tz == null) {
            tz = DEFAULT_TIMEZONE;
        }
        if ((tz == _timezone) || tz.equals(_timezone)) {
            return this;
        }
        return new StdDateFormat(tz, _locale, _lenient, _tzSerializedWithColon);
    }

上述格式化后时间相差8个小时可以通过修改TimeZone为北京时区去解决。

    @Test
    public void configureDateFormat() {
        // GMT+08:00  北京时间
        TimeZone timeZone = TimeZone.getTimeZone("GMT+08:00");
        StdDateFormat stdDateFormat = new StdDateFormat();
        //获取到北京时间的时区
        stdDateFormat.setTimeZone(timeZone);
        //获取到Locale为中国大陆地区
        stdDateFormat = stdDateFormat.withLocale(Locale.SIMPLIFIED_CHINESE);
        String format2 = stdDateFormat.format(new Date());
        System.out.println(format2);
   }

2.1 Date转换为String源码分析

    
    //将Date格式化为yyyy-MM-dd'T'HH:mm:ss.SSSZ类型的数据,需要注意的是,若使用StdDateFormat那么返回的是例如:2020-02-13T06:44:27.417+0000格式的数据。
    //不能手动的填入'yyyy-MM-dd HH:mm:ss'等模板类型。
    @Override
    public StringBuffer format(Date date, StringBuffer toAppendTo,
            FieldPosition fieldPosition)
    {
        TimeZone tz = _timezone;
        if (tz == null) {
            tz = DEFAULT_TIMEZONE;
        }
        _format(tz, _locale, date, toAppendTo);
        return toAppendTo;
    }

    protected void _format(TimeZone tz, Locale loc, Date date,
            StringBuffer buffer)
    {
        //通过clone()方法获取到到Calendar实例。
        Calendar cal = _getCalendar(tz);
        cal.setTime(date);
        // [databind#2167]: handle range beyond [1, 9999]
        //处理的范围是[1,9999]年
        final int year = cal.get(Calendar.YEAR);

        //判断年份是否是公元前(BC(BCE)-公元前;AD-公元)
        if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
            _formatBCEYear(buffer, year);
        } else {
            if (year > 9999) {
              //超过4位数的处理没有明确规定,但是加前缀是强制性的。
                buffer.append('+');
            }
          //该方法时填充4位数,例如年份是100,最终得到的为0100;若是2001,最终得到是2001
            pad4(buffer, year);
        }
        buffer.append('-');
        //填充两位数,即若是1,最终得到01;
        pad2(buffer, cal.get(Calendar.MONTH) + 1);
        buffer.append('-');
        pad2(buffer, cal.get(Calendar.DAY_OF_MONTH));
        buffer.append('T');
        pad2(buffer, cal.get(Calendar.HOUR_OF_DAY));
        buffer.append(':');
        pad2(buffer, cal.get(Calendar.MINUTE));
        buffer.append(':');
        pad2(buffer, cal.get(Calendar.SECOND));
        buffer.append('.');
        pad3(buffer, cal.get(Calendar.MILLISECOND));
        //判读Calendar时间戳的偏移和类中TimeZone的偏移是否相同
        int offset = tz.getOffset(cal.getTimeInMillis());
        if (offset != 0) {
            int hours = Math.abs((offset / (60 * 1000)) / 60);
            int minutes = Math.abs((offset / (60 * 1000)) % 60);
            buffer.append(offset < 0 ? '-' : '+');
            pad2(buffer, hours);
            if( _tzSerializedWithColon ) {
                    buffer.append(':');
            }
            pad2(buffer, minutes);
        } else {
            //根据_tzSerializedWithColon属性值,来决定最终填充的是+00:00类型还是+0000类型
//            formatted.append('Z');
                if( _tzSerializedWithColon ) {
                buffer.append("+00:00");
                }
                else {
                    buffer.append("+0000");
                }
        }
    }

}

填充数据的公共方法(实际项目中可以利用!)。

    private static void pad2(StringBuffer buffer, int value) {
        int tens = value / 10;
        if (tens == 0) {
            buffer.append('0');
        } else {
            buffer.append((char) ('0' + tens));
            value -= 10 * tens;
        }
        buffer.append((char) ('0' + value));
    }

    private static void pad3(StringBuffer buffer, int value) {
        int h = value / 100;
        if (h == 0) {
            buffer.append('0');
        } else {
            buffer.append((char) ('0' + h));
            value -= (h * 100);
        }
        pad2(buffer, value);
    }

    private static void pad4(StringBuffer buffer, int value) {
        //h=99/100,那么h==0,会填充00,value=99-100*0
        //使用pad2()方法继续填充99这个数据。
        int h = value / 100;
        if (h == 0) {
            buffer.append('0').append('0');
        } else {
            if (h > 99) { // [databind#2167]: handle above 9999 correctly
                buffer.append(h);
            } else {
                pad2(buffer, h);
            }
            value -= (100 * h);
        }
        pad2(buffer, value);
    }

2.2 String转换为Date源码分析

将String解析为Date类型的方法(可根据实际需求进行扩展)

  • 根据解析是DATE_FORMAT_STR_ISO8601(yyyy-MM-dd'T'HH:mm:ss.SSSZ)或者DATE_FORMAT_STR_PLAIN(yyyy-MM-dd)格式。
  • 解析普通时间戳类型格式(可以允许存在负数,可以参考代码,判断字符串是否是时间戳即:若上送的是时间戳,也可以解析为Date格式
  • 解析RFC1123格式的数据;
    @Override
    public Date parse(String dateStr) throws ParseException
    {
        //去除参数空格
        dateStr = dateStr.trim();
        ParsePosition pos = new ParsePosition(0);
        Date dt = _parseDate(dateStr, pos);
        if (dt != null) {
            return dt;
        }
        StringBuilder sb = new StringBuilder();
        for (String f : ALL_FORMATS) {
            if (sb.length() > 0) {
                sb.append("\", \"");
            } else {
                sb.append('"');
            }
            sb.append(f);
        }
        sb.append('"');
        throw new ParseException
            (String.format("Cannot parse date \"%s\": not compatible with any of standard forms (%s)",
                           dateStr, sb.toString()), pos.getErrorIndex());
    }

    // 24-Jun-2017, tatu: I don't think this ever gets called. So could... just not implement?
    @Override
    public Date parse(String dateStr, ParsePosition pos)
    {
        try {
            return _parseDate(dateStr, pos);
        } catch (ParseException e) {
            // 可能看起来很奇怪,但这是“DateFormat”建议的做法
        }
        return null;
    }

    protected Date _parseDate(String dateStr, ParsePosition pos) throws ParseException
    {
      //判断传入的str是否是DATE_FORMAT_STR_ISO8601 格式或者DATE_FORMAT_STR_PLAIN 格式(判断特定位置和特定格式)
        if (looksLikeISO8601(dateStr)) { 
            //解析字符串
            return parseAsISO8601(dateStr, pos);
        }
        // 也可以考虑“简化”的简单时间戳(将时间戳解析为Date类型)

        int i = dateStr.length();
        while (--i >= 0) {
            char ch = dateStr.charAt(i);
            if (ch < '0' || ch > '9') {
                // 07-Aug-2013, tatu: And [databind#267] points out that negative numbers should also work
                if (i > 0 || ch != '-') {
                    break;
                }
            }
        }
        //(1)解析dateStr字符串,字符串每个类型均为0-9之内的数字或ch等于`-`号时,继续解析,直到break或遍历完毕。若是break结束,那么i<0为false,若是遍历完毕,i<0为true。
       //(2)判断`-`号是否是第一位或者dateStr是否是Long类型
        if ((i < 0)
            // 假设负数是可以的(无论如何都不能是RFC-1123);
                && (dateStr.charAt(0) == '-' || NumberInput.inLongRange(dateStr, false))) {
            return _parseDateFromLong(dateStr, pos);
        }
        // 否则,回到使用RFC 1123。注意:调用不会抛出异常,只返回'null`。
        return parseAsRFC1123(dateStr, pos);
    }

根据字符串特定位置的字符,判断是否是ISO8601类型的格式。

    protected boolean looksLikeISO8601(String dateStr)
    {
        //字符串长度大于7,且第0位时数字,第三位是数字,第四位时-号,第5位时数字,即返回true
        if (dateStr.length() >= 7 // really need 10, but...
            && Character.isDigit(dateStr.charAt(0))
            && Character.isDigit(dateStr.charAt(3))
            && dateStr.charAt(4) == '-'
            && Character.isDigit(dateStr.charAt(5))
            ) {
            return true;
        }
        return false;
    }
    //将字符串解析为Date类型
    private Date _parseDateFromLong(String longStr, ParsePosition pos) throws ParseException
    {
        long ts;
        try {
            ts = NumberInput.parseLong(longStr);
        } catch (NumberFormatException e) {
            throw new ParseException(String.format(
                    "Timestamp value %s out of 64-bit value range", longStr),
                    pos.getErrorIndex());
        }
        return new Date(ts);
    }

解析ISO8601格式的字符串为Date类型。

    protected Date _parseAsISO8601(String dateStr, ParsePosition bogus)
        throws IllegalArgumentException, ParseException
    {
      //获取传入的字符串的长度;
        final int totalLen = dateStr.length();
        //事实上,若以Z结尾,一定是UTC时区
        TimeZone tz = DEFAULT_TIMEZONE;
        if ((_timezone != null) && ('Z' != dateStr.charAt(totalLen-1))) {
            tz = _timezone;
        }
        //通过clone的方式获取到Calendar对象
        Calendar cal = _getCalendar(tz);
        cal.clear();
        String formatStr;
       //判断传入字符的长度,若小于10,那么使用DATE_FORMAT_STR_PLAIN去解析
        if (totalLen <= 10) {
            //使用正则表达式判断类型
            Matcher m = PATTERN_PLAIN.matcher(dateStr);
            if (m.matches()) {
                int year = _parse4D(dateStr, 0);
                int month = _parse2D(dateStr, 5)-1;
                int day = _parse2D(dateStr, 8);

                cal.set(year, month, day, 0, 0, 0);
                cal.set(Calendar.MILLISECOND, 0);
                //解析为Date类型,且返回
                return cal.getTime();
            }
            formatStr = DATE_FORMAT_STR_PLAIN;
        } else {
            //使用正则表达式,判断DATE_FORMAT_STR_ISO8601 格式
            Matcher m = PATTERN_ISO8601.matcher(dateStr);
            if (m.matches()) {
                // Important! START with optional time zone; otherwise Calendar will explode
                
                int start = m.start(2);
                int end = m.end(2);
                int len = end-start;
                if (len > 1) { // 0 -> none, 1 -> 'Z'
                    // NOTE: first char is sign; then 2 digits, then optional colon, optional 2 digits
                    int offsetSecs = _parse2D(dateStr, start+1) * 3600; // hours
                    if (len >= 5) {
                        offsetSecs += _parse2D(dateStr, end-2) * 60; // minutes
                    }
                    if (dateStr.charAt(start) == '-') {
                        offsetSecs *= -1000;
                    } else {
                        offsetSecs *= 1000;
                    }
                    cal.set(Calendar.ZONE_OFFSET, offsetSecs);
                    // 23-Jun-2017, tatu: Not sure why, but this appears to be needed too:
                    cal.set(Calendar.DST_OFFSET, 0);
                }
                
                int year = _parse4D(dateStr, 0);
                int month = _parse2D(dateStr, 5)-1;
                int day = _parse2D(dateStr, 8);

                // So: 10 chars for date, then `T`, so starts at 11
                int hour = _parse2D(dateStr, 11);
                int minute = _parse2D(dateStr, 14);

                // Seconds are actually optional... so
                int seconds;
                if ((totalLen > 16) && dateStr.charAt(16) == ':') {
                    seconds = _parse2D(dateStr, 17);
                } else {
                    seconds = 0;
                }
                cal.set(year, month, day, hour, minute, seconds);

                // Optional milliseconds
                start = m.start(1) + 1;
                end = m.end(1);
                int msecs = 0;
                if (start >= end) { // no fractional
                    cal.set(Calendar.MILLISECOND, 0);
                } else {
                    // first char is '.', but rest....
                    msecs = 0;
                    final int fractLen = end-start;
                    switch (fractLen) {
                    default: // [databind#1745] Allow longer fractions... for now, cap at nanoseconds tho

                        if (fractLen > 9) { // only allow up to nanos
                            throw new ParseException(String.format(
"Cannot parse date \"%s\": invalid fractional seconds '%s'; can use at most 9 digits",
                                       dateStr, m.group(1).substring(1)
                                       ), start);
                        }
                        // fall through
                    case 3:
                        msecs += (dateStr.charAt(start+2) - '0');
                    case 2:
                        msecs += 10 * (dateStr.charAt(start+1) - '0');
                    case 1:
                        msecs += 100 * (dateStr.charAt(start) - '0');
                        break;
                    case 0:
                        break;
                    }
                    cal.set(Calendar.MILLISECOND, msecs);
                }
                return cal.getTime();
            }
            formatStr = DATE_FORMAT_STR_ISO8601;
        }
       //若不满足上述条件,则之间抛出异常。
        throw new ParseException
        (String.format("Cannot parse date \"%s\": while it seems to fit format '%s', parsing fails (leniency? %s)",
                       dateStr, formatStr, _lenient),
                // [databind#1742]: Might be able to give actual location, some day, but for now
                //  we can't give anything more indicative
                0);
    }

实际上StdDateFormat不会解析yyyy-MM-dd HH:mm:ss格式的字符串。若我们上述这种类型的数据,它通过looksLikeISO8601方法,判断其为DATE_FORMAT_STR_ISO8601或者DATE_FORMAT_STR_PLAIN类型的数据。

但在parseAsISO8601方法中,会发现不存在对应类型。直接抛出异常。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343