https://4aiur.github.io/2018/03/26/gson-dateformat-pattern/

 

问题描述:

线上的日志里报了一个JsonSyntaxException的异常:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Exception in thread "main" com.google.gson.JsonSyntaxException: 2018-03-26 22:22:35
at com.google.gson.internal.bind.DateTypeAdapter.deserializeToDate(DateTypeAdapter.java:74)
at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:59)
at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:41)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:129)
at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:220)
at com.google.gson.Gson.fromJson(Gson.java:888)
at com.google.gson.Gson.fromJson(Gson.java:853)
at com.google.gson.Gson.fromJson(Gson.java:802)
at com.google.gson.Gson.fromJson(Gson.java:774)
at web.service.JsonTest.main(JsonTest.java:25)
Caused by: java.text.ParseException: Failed to parse date ["2018-03-26 22:22:35']: Invalid time zone indicator ' '
at com.google.gson.internal.bind.util.ISO8601Utils.parse(ISO8601Utils.java:274)
at com.google.gson.internal.bind.DateTypeAdapter.deserializeToDate(DateTypeAdapter.java:72)
... 9 more
Caused by: java.lang.IndexOutOfBoundsException: Invalid time zone indicator ' '
at com.google.gson.internal.bind.util.ISO8601Utils.parse(ISO8601Utils.java:245)
... 10 more

 

生产场景:

RPC框架是hessian, 这个错误发生在调用hessian接口时, 本地一切正常, 放到线上就报错.

解决办法:

最后发现调用时反序列化时Gson是这样实例化的

1
Gson gson = new Gson()

 

改为工厂模式构建出的Gson就好了

1
Gson builderTime = (new GsonBuilder()).setDateFormat("yyyy-MM-dd HH:mm:ss").create();

 

复现问题:

实体类里只有java.util.Date类型, 按理说序列化之后应该是Mon Mar 26 21:17:43 CST 2018才对, 这个时间格式明显是被格式化了,
于是发现在hessian的服务端, GsonBuilder格式化了这个时间,

1
2
Gson gson = (new GsonBuilder()).setDateFormat("yyyy-MM-dd HH:mm:ss").create();
gson.toJson(new Foo());

把实体类格式化成了这种样子

{"name":"foo","startTime":"2018-03-26 21:17:43"}

而在另一边, 在hessian的客户端, 是这样反序列的:

1
2
Gson gson = new Gson();
Foo foo = gson.fromJson(barJson, Foo.class);

 

在win10简体中文版环境下一切正常, 结果到了线上CentOS环境下,
这样Gson gson = new Gson()得到的gson不能正常的将yyyy-MM-dd HH:mm:ss格式的时间转换为GMT格式.

分析源码:

根据报错信息打开出错的源头com.google.gson.internal.bind.DateTypeAdapter.java类中的deserializeToDate()方法, 这个方法是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private synchronized Date deserializeToDate(String json) {
try {
return localFormat.parse(json);
} catch (ParseException ignored) {
}
try {
return enUsFormat.parse(json);
} catch (ParseException ignored) {
}
try {
return ISO8601Utils.parse(json, new ParsePosition(0));
} catch (ParseException e) {
throw new JsonSyntaxException(json, e);
}
}

 

这是一个很暴力的适配器模式, localFormat, enUsFormat, ISO8601Utils挨个尝试转换,那么这三个值具体是什么呢, 打个断点来看看:

ISO8601:

首先是ISO8601,Google的大佬直接hard code 成Locale.US.

ISO8601


这是个UTC时间, T标识是UTC时间,Z标识时区, 北京时间比UTC快的8个小时, 会被记作UTC+8, 这就是东八区的由来, 如下:

UTC例子

 

EnUsFormat:

再看enUsFormat,

enUsFormat


美国的时间会标记出上午和下午, 时间格式是MMM dd, yyyy hh:mm:ss a, 例如Sep 16, 2015 10:34:23 AM.

 

LocalFormat:

最后是localFormat,

cn-localFormat


注意到这个java.text.DateFormat#getDateTimeInstance(int, int)这个方法,
不同地区规定的常用日期格式是不一样的, 查询WIKI百科各地日期和时间表示法,得知这个日期格式是台湾的常用日期格式,
问题它怎么知道我是在美国还是在中国台湾, 看getDateTimeInstance方法的源码

getDateTimeInstance()


发现这个方法在Java7之后, 根据操作系统的语言, 判断用户所在的时区,
那现在将WIN10控制面板=>时钟、语言和区域=>添加语言=>更改windows显示语言为英语-美国, 再来看localFormat变成了什么:

us-localFormat

 

坑爹啊Gson, 使用locale命令查一查线上CentOS7的语言环境

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[root@VM_0_9_centos ~]# locale
LANG=en_US.utf8
LC_CTYPE="en_US.utf8"
LC_NUMERIC="en_US.utf8"
LC_TIME="en_US.utf8"
LC_COLLATE="en_US.utf8"
LC_MONETARY="en_US.utf8"
LC_MESSAGES="en_US.utf8"
LC_PAPER="en_US.utf8"
LC_NAME="en_US.utf8"
LC_ADDRESS="en_US.utf8"
LC_TELEPHONE="en_US.utf8"
LC_MEASUREMENT="en_US.utf8"
LC_IDENTIFICATION="en_US.utf8"
LC_ALL=

 

果然是英文环境, 这样的话yyyy-MM-dd HH:mm:ss,
既无法被UTC时间yyyy-MM-dd'T'HH:mm:ss'Z'适配,
无法被美国默认时间MMM dd, yyyy hh:mm:ss a适配, 最后抛出了JsonSyntaxException.

根治:

改成中文, 并重启系统

1
2
// 查看系统拥有语言包
locale -a

 

zh_CN.UTF-8是简体中文,如果没有zh_CN.UTF-8,就安装语言包,如果存在可以直接设置

1
2
// 安装简中语言包
yum install kde-l10n-Chinese

 

永久修改系统语言

1
localectl  set-locale LANG=zh_CN.UTF8