一次GSON時間問題的排查

語言: CN / TW / HK

問題描述

近期有一個老專案在測試環境中頻繁出現了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"

可以檢視當前配置的語言環境:

[[email protected] ~]# 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原始碼、架構、演算法和麵試。