背景介紹
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)這一功能的鼠证?
框架選型峡竣、版本及主要功能
- spring boot 2.1.6.RELEASE
- spring cloud Greenwich.SR3
- 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)了兩處有價值的信息:
- 觸發(fā)消息類型轉(zhuǎn)換類是FastJsonHttpMessageConverter
- 真正完成類型映射是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ū)