SpringMVC日期格式属性自动转成时间戳实现源码分析

编程

背景介绍

SpringMVC搭建的微服务系统,后端数据库对时间类型的存储使用的是Long类型,而前端框架倾向于使用yyyy-MM-dd HH:mm:ss这种标准显示格式,前端JSON格式的请求报文与后台的接口交互都需要进行格式转换,这部分转换功能由后台实现。

使用时我们发现,前端定义的JSON请求,时间格式为yyyy-MM-dd HH:mm:ss,如果后台定义的POJO相应的属性为Long类型,可以自动转换为时间戳,对此非常好奇,框架是如何实现这一功能的?

框架选型、版本及主要功能

  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"

}

响应的结果:DateReq{dateFormat="2020-08-07 18:50:00", timestamp=1596797400000}

从结果可以发现,dateFormat字段我们定义的是String类型,timestamp定义的是Long类型,请求报文两个字段使用相同的值,但是到了Controller方法里,timestamp自动变成Long类型的时间戳了,并且是按东8区转换的。

在这里我们可以得到一个使用经验:POJO的时间格式是可以自动转换成Long类型时间戳的,默认时区取操作系统的时区,或者通过jvm参数-Duser.timezone=GMT+08设置。

源码阅读

既然看到了自动转换的效果,非常好奇框架是怎么实现的,我们通过断点查找堆栈:

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)

发现了两处有价值的信息:

  1. 触发消息类型转换类是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();

// 关注这一行,yyyy-MM-dd HH:mm:ss会执行这一行代码

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;

}

重点关注longObject = TypeUtils.castToLong(value);yyyy-MM-dd HH:mm:ss格式的数据会执行这一行代码,跟进去查看源码:

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) {

// 在异常里做最后的挣扎,今天的案例是执行到这里的

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方法里,对假想的数据类型做各种假设处理,很不幸的是我们试验的数据格式,是在NumberFormatException异常里完成的最后挣扎,使用JSONScanner类接收的请求数据。

可以看到在这里通过调用dateParser.scanISO8601DateIfMatch对数据进行解析,得到calendar对象实例,最终通过calendar获取时间戳,scanISO8601DateIfMatch方法逻辑很复杂,总共有450多行,这里截取了其中一部分展现一下:

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; // 有可能没有毫秒区域,没有毫秒区域的时候下一个字符位置有可能是"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;

}

支持的格式还是挺多,不过基本上符合国内的日期使用习惯,像2020-08-08和2020/08/08,甚至2020年08月08日都行,解析的思路是按位截取判断,然后作为Calendar的参数,上述节选的代码有删节,有兴趣可以查看原代码。

小结

简单做个小结,fastjson在SpringMVC中注册了FastJsonHttpMessageConverter转换器,并且由该转换器驱动fastjson的反序列化能力,对一些常用格式的数据进行自动转换,加快了研发效率。本篇内容从一个好奇心开始,到查阅源码,了解框架内组件的协同,并在源码中证实自己的想法,学习框架内解决问题的思路,希望这份好奇心,能够驱动对框架源码的阅读。

专注Java高并发、分布式架构,更多技术干货分享与心得,请关注公众号:Java架构社区

可以扫左边二维码添加好友,邀请你加入Java架构社区微信群共同探讨技术

以上是 SpringMVC日期格式属性自动转成时间戳实现源码分析 的全部内容, 来源链接: utcz.com/z/519269.html

回到顶部