关于java:一次GSON时间问题的排查

54次阅读

共计 4215 个字符,预计需要花费 11 分钟才能阅读完成。

问题形容

近期有一个老我的项目在测试环境中频繁呈现了 GSON 反序列化工夫问题,谬误堆栈如下所示:

Exception in thread "main" com.google.gson.JsonSyntaxException: 2021-05-14 14:59:37
    at com.google.gson.internal.bind.DateTypeAdapter.deserializeToDate(DateTypeAdapter.java:81)
    at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:66)
    at com.google.gson.internal.bind.DateTypeAdapter.read(DateTypeAdapter.java:41)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$1.read(ReflectiveTypeAdapterFactory.java:93)
    at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:172)
    at com.google.gson.Gson.fromJson(Gson.java:795)
    at com.google.gson.Gson.fromJson(Gson.java:761)
    at com.google.gson.Gson.fromJson(Gson.java:710)
    at com.google.gson.Gson.fromJson(Gson.java:682)
    at com.gson.GsonDate.main(GsonDate.java:17)
Caused by: java.text.ParseException: Unparseable date: "2021-05-14 14:59:37"
    at java.text.DateFormat.parse(DateFormat.java:366)
    at com.google.gson.internal.bind.DateTypeAdapter.deserializeToDate(DateTypeAdapter.java:79)
    ... 9 more

谬误形容也很具体,就是 GSON 在反序列化一段 Json 串的时候,因为某个工夫字符串无奈反序列化,导致最终整个 Json 反序列化失败;

背景阐明

因为此零碎数据量比拟大,所有会将半年前的数据归档到 Hbase 中,归档的时候会将数据库中的数据序列化为 json 格局,而后保留到 Hbase 中;如果是近期半年的数据会间接查询数据库,如果是很早的数据才会查问 Hbase,所以呈现的概率比拟低;

问题剖析

本地重现

为了不便剖析,间接把 Json 串拷贝到本地,而后再本地进行重现,再进行问题剖析,Json 串比拟长,这里应用如下 Json 串代替:

{"date":"2021-05-14 14:59:37"}

筹备相干代码如下所示:

public class GsonDate {public static void main(String[] args) {String json = "{\"date\":\"2021-05-14 14:59:37\"}";
        GsonDateBean date = new Gson().fromJson(json, GsonDateBean.class);
        System.out.println(date);
    }
}

@Data
class GsonDateBean {private Date date;}

执行的后果是能够反序列胜利,并没有呈现下面的谬误,为了找出起因,这里须要剖析一下 Gson 工夫转换的相干源码;

源码剖析

Gson 工夫转换的源码还是比较简单的,DateTypeAdapter局部代码如下所示:

  private final DateFormat enUsFormat
      = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT, Locale.US);
  private final DateFormat localFormat
      = DateFormat.getDateTimeInstance(DateFormat.DEFAULT, DateFormat.DEFAULT);
  private final DateFormat iso8601Format = buildIso8601Format();

  private static DateFormat buildIso8601Format() {DateFormat iso8601Format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US);
    iso8601Format.setTimeZone(TimeZone.getTimeZone("UTC"));
    return iso8601Format;
  }

  private synchronized Date deserializeToDate(String json) {
    try {return localFormat.parse(json);
    } catch (ParseException ignored) { }
    try {return enUsFormat.parse(json);
    } catch (ParseException ignored) { }
    try {return iso8601Format.parse(json);
    } catch (ParseException e) {throw new JsonSyntaxException(json, e);
    }
  }

Gson 筹备了三个 DateFormat,别离是:localFormat,enUsFormat,iso8601Format;转换的时候也是依照这个程序进行转换,哪个能转换胜利就间接返回,以上呈现问题阐明三种DateFormat 都没有转换胜利;本地调试能够间接 Debug 进来,能够发现间接应用 localFormat 就转换胜利了,并且能够别离查看每个的pattern

  • localFormat:yyyy-M-d H:mm:ss
  • enUsFormat:MMM d, yyyy h:mm:ss a
  • iso8601Format:yyyy-MM-dd’T’HH:mm:ss’Z’

以上的日期格局完全符合 yyyy-M-d H:mm:ss 格局,所以能够间接转换胜利;能够发现 localFormat 其实是和本地零碎的语言环境无关,所以会呈现本地运行后果和服务器运行后果不统一;

再次重现

能够间接通过代码设置语言环境,把环境设置为Locale.US

public class GsonDate {public static void main(String[] args) {System.out.println("默认:"+Locale.getDefault());
        System.out.println("重置语言环境:Locale.US");
        Locale.setDefault(Locale.US);
        String json = "{\"date\":\"2021-05-14 14:59:37\"}";
        GsonDateBean date = new Gson().fromJson(json, GsonDateBean.class);
        System.out.println(date);
    }
}

运行以上代码,呈现了和服务器一样的反序列工夫问题:

默认:zh_CN
重置语言环境:Locale.US
Exception in thread "main" com.google.gson.JsonSyntaxException: 2021-05-14 14:59:37
    at com.google.gson.internal.bind.DateTypeAdapter.deserializeToDate(DateTypeAdapter.java:81)

能够发现咱们本地的环境个别都是zh_CN,对应Locale.CHINA

问题解决

系统配置

能够间接扭转零碎语言环境,liunx 能够间接在 /etc/sysconfig/i18n 中配置:

英文版零碎:LANG="en_US.UTF-8"
中文版零碎:LANG="zh_CN.UTF-8"

能够查看以后配置的语言环境:

[root@Centos ~]# echo $LANG
en_US.UTF-8

代码实现

能够给 Gson 设置默认的日志转换格局:

Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
GsonDateBean date = gson.fromJson(json, GsonDateBean.class);

扩大

同样的如果应用其余 Json 序列化工具,比方 fastjson 是否也有这样的问题那,能够简略做一个测试:

Locale.setDefault(Locale.US);
String json = "{\"date\":\"2021-05-14 14:59:37\"}";
String json2 = "{\"date\":\"2021 年 05 月 14 日 14:59:37\"}";
JacksonDateBean date = JSON.parseObject(json, JacksonDateBean.class);

后果是不仅 yyyy-MM-dd HH:mm:ss 格局能被解析,蕴含中文的 年月日 都能够被解析;如果查看相干源码能够发现,fastjson并没有间接应用 DateFormat 去做日期格局转换,而是实现了 ISO 8601 规范,并且提供了中国常见日期格局的反对;具体能够间接查看源码 JSONScanner 中的 scanISO8601DateIfMatch 办法;
另外一点须要阐明的是以上 GSON 应用的是 2.2.2 版本,最新版本 2.8.6 版本中同样提供了对 ISO 8601 规范 的反对,具体能够查看 ISO8601Utils 类。

感激关注

能够关注微信公众号「回滚吧代码」,第一工夫浏览,文章继续更新;专一 Java 源码、架构、算法和面试。

正文完
 0