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日期的序列化与反序列化的程序。
- 序列化:默认使用ISO-8601的格式(会将Date类型格式成“yyyy-MM-dd'T'HH:MM:ss.SSSZ”字符串类型)。
- 反序列化:可以解析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
方法中,会发现不存在对应类型。直接抛出异常。