SpringMVC日期格式屬性自動轉(zhuǎn)成時間戳實現(xiàn)源碼分析

背景介紹

SpringMVC搭建的微服務(wù)系統(tǒng)炼幔,后端數(shù)據(jù)庫對時間類型的存儲使用的是Long類型宇植,而前端框架傾向于使用yyyy-MM-dd HH:mm:ss這種標準顯示格式宴猾,前端JSON格式的請求報文與后臺的接口交互都需要進行格式轉(zhuǎn)換,這部分轉(zhuǎn)換功能由后臺實現(xiàn)徐鹤。

使用時我們發(fā)現(xiàn)听绳,前端定義的JSON請求颂碘,時間格式為yyyy-MM-dd HH:mm:ss,如果后臺定義的POJO相應(yīng)的屬性為Long類型椅挣,可以自動轉(zhuǎn)換為時間戳头岔,對此非常好奇,框架是如何實現(xiàn)這一功能的鼠证?

框架選型峡竣、版本及主要功能

  1. spring boot 2.1.6.RELEASE
  2. spring cloud Greenwich.SR3
  3. alibaba fastjson 1.2.60

注意json框架使用的是fastjson

代碼演示

為了方便演示,定義一個特別簡單的POJO類:

public class DateReq {

    private String dateFormat;
    private Long timestamp;

    // 省略getter/setter/toString方法
}

再定義一個簡單的Controller方法:

@RestController
public class DemoController {

    @PostMapping(value = "/json/demo/info")
    public ApiResponse<?> dateJson(@RequestBody DateReq request) {
        System.out.println(request);
    }
}

請求報文如下:

{
    "dateFormat": "2020-08-07 18:50:00",
    "timestamp": "2020-08-07 18:50:00"
}

響應(yīng)的結(jié)果:DateReq{dateFormat='2020-08-07 18:50:00', timestamp=1596797400000}

從結(jié)果可以發(fā)現(xiàn)量九,dateFormat字段我們定義的是String類型适掰,timestamp定義的是Long類型,請求報文兩個字段使用相同的值荠列,但是到了Controller方法里类浪,timestamp自動變成Long類型的時間戳了,并且是按東8區(qū)轉(zhuǎn)換的肌似。

在這里我們可以得到一個使用經(jīng)驗:POJO的時間格式是可以自動轉(zhuǎn)換成Long類型時間戳的费就,默認時區(qū)取操作系統(tǒng)的時區(qū),或者通過jvm參數(shù)-Duser.timezone=GMT+08設(shè)置川队。

源碼閱讀

既然看到了自動轉(zhuǎn)換的效果力细,非常好奇框架是怎么實現(xiàn)的,我們通過斷點查找堆棧:

deserialze:79, LongCodec (com.alibaba.fastjson.serializer)
parseField:85, DefaultFieldDeserializer (com.alibaba.fastjson.parser.deserializer)
parseField:1224, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:850, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseRest:1538, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
deserialze:-1, FastjsonASMDeserializer_3_DateReq (com.alibaba.fastjson.parser.deserializer)
deserialze:284, JavaBeanDeserializer (com.alibaba.fastjson.parser.deserializer)
parseObject:692, DefaultJSONParser (com.alibaba.fastjson.parser)
parseObject:383, JSON (com.alibaba.fastjson)
parseObject:448, JSON (com.alibaba.fastjson)
parseObject:556, JSON (com.alibaba.fastjson)
readType:263, FastJsonHttpMessageConverter (com.alibaba.fastjson.support.spring)
read:237, FastJsonHttpMessageConverter (com.alibaba.fastjson.support.spring)
readWithMessageConverters:204, AbstractMessageConverterMethodArgumentResolver (org.springframework.web.servlet.mvc.method.annotation)
readWithMessageConverters:157, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:130, RequestResponseBodyMethodProcessor (org.springframework.web.servlet.mvc.method.annotation)
resolveArgument:124, HandlerMethodArgumentResolverComposite (org.springframework.web.method.support)

發(fā)現(xiàn)了兩處有價值的信息:

  1. 觸發(fā)消息類型轉(zhuǎn)換類是FastJsonHttpMessageConverter
  2. 真正完成類型映射是fastjson框架

有這個思路固额,閱讀源碼時可以把重點放在fastjson上眠蚂,從JSON反序列化為POJO,Long類型字段處理对雪,找到這段代碼:

public <T> T deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) {
        JSONLexer lexer = parser.lexer;

        Long longObject;
        try {
            int token = lexer.token();
            if (token == 2) {
                long longValue = lexer.longValue();
                lexer.nextToken(16);
                longObject = longValue;
            } else if (token == 3) {
                BigDecimal number = lexer.decimalValue();
                longObject = TypeUtils.longValue(number);
                lexer.nextToken(16);
            } else {
                if (token == 12) {
                    JSONObject jsonObject = new JSONObject(true);
                    parser.parseObject(jsonObject);
                    longObject = TypeUtils.castToLong(jsonObject);
                } else {
                    Object value = parser.parse();
                    // 關(guān)注這一行河狐,yyyy-MM-dd HH:mm:ss會執(zhí)行這一行代碼
                    longObject = TypeUtils.castToLong(value);
                }

                if (longObject == null) {
                    return null;
                }
            }
        } catch (Exception var9) {
            throw new JSONException("parseLong error, field : " + fieldName, var9);
        }

        return clazz == AtomicLong.class ? new AtomicLong(longObject) : longObject;
    }

重點關(guān)注longObject = TypeUtils.castToLong(value);yyyy-MM-dd HH:mm:ss格式的數(shù)據(jù)會執(zhí)行這一行代碼,跟進去查看源碼:

public static Long castToLong(Object value) {
        if (value == null) {
            return null;
        } else if (value instanceof BigDecimal) {
            return longValue((BigDecimal)value);
        } else if (value instanceof Number) {
            return ((Number)value).longValue();
        } else {
            if (value instanceof String) {
                String strVal = (String)value;
                if (strVal.length() == 0 || "null".equals(strVal) || "NULL".equals(strVal)) {
                    return null;
                }

                if (strVal.indexOf(44) != 0) {
                    strVal = strVal.replaceAll(",", "");
                }

                try {
                    return Long.parseLong(strVal);
                } catch (NumberFormatException var4) {
                    // 在異常里做最后的掙扎馋艺,今天的案例是執(zhí)行到這里的
                    JSONScanner dateParser = new JSONScanner(strVal);
                    Calendar calendar = null;
                    if (dateParser.scanISO8601DateIfMatch(false)) {
                        calendar = dateParser.getCalendar();
                    }

                    dateParser.close();
                    if (calendar != null) {
                        return calendar.getTimeInMillis();
                    }
                }
            }

            if (value instanceof Map) {
                Map map = (Map)value;
                if (map.size() == 2 && map.containsKey("andIncrement") && map.containsKey("andDecrement")) {
                    Iterator iter = map.values().iterator();
                    iter.next();
                    Object value2 = iter.next();
                    return castToLong(value2);
                }
            }

            throw new JSONException("can not cast to long, value : " + value);
        }
    }

可以看到在castToLong方法里栅干,對假想的數(shù)據(jù)類型做各種假設(shè)處理,很不幸的是我們試驗的數(shù)據(jù)格式捐祠,是在NumberFormatException異常里完成的最后掙扎碱鳞,使用JSONScanner類接收的請求數(shù)據(jù)。

可以看到在這里通過調(diào)用dateParser.scanISO8601DateIfMatch對數(shù)據(jù)進行解析踱蛀,得到calendar對象實例窿给,最終通過calendar獲取時間戳,scanISO8601DateIfMatch方法邏輯很復(fù)雜率拒,總共有450多行崩泡,這里截取了其中一部分展現(xiàn)一下:

private boolean scanISO8601DateIfMatch(boolean strict, int rest) {
        if (rest < 8) {
            return false;
        }

        char c0 = charAt(bp);
        char c1 = charAt(bp + 1);
        char c2 = charAt(bp + 2);
        char c3 = charAt(bp + 3);
        char c4 = charAt(bp + 4);
        char c5 = charAt(bp + 5);
        char c6 = charAt(bp + 6);
        char c7 = charAt(bp + 7);

        if ((!strict) && rest > 13) {
            char c_r0 = charAt(bp + rest - 1);
            char c_r1 = charAt(bp + rest - 2);
        }

        char c10;
       

        if (rest < 9) {
            return false;
        }

        char c8 = charAt(bp + 8);
        char c9 = charAt(bp + 9);

        int date_len = 10;
        char y0, y1, y2, y3, M0, M1, d0, d1;
        if ((c4 == '-' && c7 == '-') // cn
                ||  (c4 == '/' && c7 == '/') // tw yyyy/mm/dd
        ) {
            y0 = c0;
            y1 = c1;
            y2 = c2;
            y3 = c3;
            M0 = c5;
            M1 = c6;
            d0 = c8;
            d1 = c9;
        } else if ((c4 == '-' && c6 == '-') // cn yyyy-m-dd
        ) {
            y0 = c0;
            y1 = c1;
            y2 = c2;
            y3 = c3;
            M0 = '0';
            M1 = c5;

            if (c8 == ' ') {
                d0 = '0';
                d1 = c7;
                date_len = 8;
            } else {
                d0 = c7;
                d1 = c8;
                date_len = 9;
            }
        } else if ((c2 == '.' && c5 == '.') // de dd.mm.yyyy
                || (c2 == '-' && c5 == '-') // in dd-mm-yyyy
        ) {
            d0 = c0;
            d1 = c1;
            M0 = c3;
            M1 = c4;
            y0 = c6;
            y1 = c7;
            y2 = c8;
            y3 = c9;
        } else if (c8 == 'T') {
            y0 = c0;
            y1 = c1;
            y2 = c2;
            y3 = c3;
            M0 = c4;
            M1 = c5;
            d0 = c6;
            d1 = c7;
            date_len = 8;
        } else {
            if (c4 == '年' || c4 == '?') {
                y0 = c0;
                y1 = c1;
                y2 = c2;
                y3 = c3;

                if (c7 == '月' || c7 == '?') {
                    M0 = c5;
                    M1 = c6;
                    if (c9 == '日' || c9 == '?') {
                        d0 = '0';
                        d1 = c8;
                    } else if (charAt(bp + 10) == '日' || charAt(bp + 10) == '?'){
                        d0 = c8;
                        d1 = c9;
                        date_len = 11;
                    } else {
                        return false;
                    }
                } else if (c6 == '月' || c6 == '?') {
                    M0 = '0';
                    M1 = c5;
                    if (c8 == '日' || c8 == '?') {
                        d0 = '0';
                        d1 = c7;
                    } else if (c9 == '日' || c9 == '?'){
                        d0 = c7;
                        d1 = c8;
                    } else {
                        return false;
                    }
                } else {
                    return false;
                }
            } else {
                return false;
            }
        }

        if (!checkDate(y0, y1, y2, y3, M0, M1, d0, d1)) {
            return false;
        }

        setCalendar(y0, y1, y2, y3, M0, M1, d0, d1);

        char t = charAt(bp + date_len);
       

        if (charAt(bp + date_len + 3) != ':') {
            return false;
        }
        if (charAt(bp + date_len + 6) != ':') {
            return false;
        }

        char h0 = charAt(bp + date_len + 1);
        char h1 = charAt(bp + date_len + 2);
        char m0 = charAt(bp + date_len + 4);
        char m1 = charAt(bp + date_len + 5);
        char s0 = charAt(bp + date_len + 7);
        char s1 = charAt(bp + date_len + 8);

        if (!checkTime(h0, h1, m0, m1, s0, s1)) {
            return false;
        }

        setTime(h0, h1, m0, m1, s0, s1);

        char dot = charAt(bp + date_len + 9);
        int millisLen = -1; // 有可能沒有毫秒?yún)^(qū)域,沒有毫秒?yún)^(qū)域的時候下一個字符位置有可能是'Z'猬膨、'+'角撞、'-'
        int millis = 0;
       
        calendar.set(Calendar.MILLISECOND, millis);

        int timzeZoneLength = 0;
        char timeZoneFlag = charAt(bp + date_len + 10 + millisLen);

        if (timeZoneFlag == ' ') {
            millisLen++;
            timeZoneFlag = charAt(bp + date_len + 10 + millisLen);
        }

        char end = charAt(bp + (date_len + 10 + millisLen + timzeZoneLength));
        if (end != EOI && end != '"') {
            return false;
        }
        ch = charAt(bp += (date_len + 10 + millisLen + timzeZoneLength));

        token = JSONToken.LITERAL_ISO8601_DATE;
        return true;
    }

支持的格式還是挺多,不過基本上符合國內(nèi)的日期使用習(xí)慣勃痴,像2020-08-08和2020/08/08谒所,甚至2020年08月08日都行,解析的思路是按位截取判斷沛申,然后作為Calendar的參數(shù)劣领,上述節(jié)選的代碼有刪節(jié),有興趣可以查看原代碼铁材。

小結(jié)

簡單做個小結(jié)尖淘,fastjson在SpringMVC中注冊了FastJsonHttpMessageConverter轉(zhuǎn)換器,并且由該轉(zhuǎn)換器驅(qū)動fastjson的反序列化能力衫贬,對一些常用格式的數(shù)據(jù)進行自動轉(zhuǎn)換德澈,加快了研發(fā)效率。本篇內(nèi)容從一個好奇心開始固惯,到查閱源碼,了解框架內(nèi)組件的協(xié)同缴守,并在源碼中證實自己的想法葬毫,學(xué)習(xí)框架內(nèi)解決問題的思路,希望這份好奇心屡穗,能夠驅(qū)動對框架源碼的閱讀贴捡。

專注Java高并發(fā)、分布式架構(gòu)村砂,更多技術(shù)干貨分享與心得烂斋,請關(guān)注公眾號:Java架構(gòu)社區(qū)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子汛骂,更是在濱河造成了極大的恐慌罕模,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件帘瞭,死亡現(xiàn)場離奇詭異淑掌,居然都是意外死亡,警方通過查閱死者的電腦和手機蝶念,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門抛腕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人媒殉,你說我怎么就攤上這事担敌。” “怎么了廷蓉?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵全封,是天一觀的道長。 經(jīng)常有香客問我苦酱,道長售貌,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任疫萤,我火速辦了婚禮颂跨,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘扯饶。我一直安慰自己恒削,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布尾序。 她就那樣靜靜地躺著钓丰,像睡著了一般。 火紅的嫁衣襯著肌膚如雪每币。 梳的紋絲不亂的頭發(fā)上携丁,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天,我揣著相機與錄音兰怠,去河邊找鬼梦鉴。 笑死,一個胖子當(dāng)著我的面吹牛揭保,可吹牛的內(nèi)容都是我干的肥橙。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼秸侣,長吁一口氣:“原來是場噩夢啊……” “哼存筏!你這毒婦竟也來了宠互?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤椭坚,失蹤者是張志新(化名)和其女友劉穎予跌,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體藕溅,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡匕得,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了巾表。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汁掠。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖集币,靈堂內(nèi)的尸體忽然破棺而出考阱,到底是詐尸還是另有隱情,我是刑警寧澤鞠苟,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布乞榨,位于F島的核電站,受9級特大地震影響当娱,放射性物質(zhì)發(fā)生泄漏吃既。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一跨细、第九天 我趴在偏房一處隱蔽的房頂上張望鹦倚。 院中可真熱鬧,春花似錦冀惭、人聲如沸震叙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽媒楼。三九已至,卻和暖如春戚丸,著一層夾襖步出監(jiān)牢的瞬間划址,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工限府, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留猴鲫,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓谣殊,卻偏偏與公主長得像,于是被迫代替她去往敵國和親牺弄。 傳聞我的和親對象是個殘疾皇子姻几,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,627評論 2 350