JSON(反)序列化之唯快不破新選擇-Jil
ASP.NET MVC的ActionResult一直有個讓人疑惑的地方,就是它的JsonResult,JsonResult預設使用了.NET Framework的JavaScriptSerializer類別,並不是說JavaScriptSerializer不好,每一個技術都有它的時空背景,.NET Framework發展過程中JSON格式也是慢慢才成為標準。在JSON格式流行與大量使用下,JavaScriptSerializer已經有點上氣不接下氣。而微軟在ASP.NET Web API第1版中就非常明確的以JSON.NET取而代之。而ASP.NET MVC到第5版為止雖然一直無異動這部分的程式碼,原因也很簡單,ASP.NET MVC設計了優良的擴充點,讓開發者想要擴充原有的ActionResult也不是太難的問題。
擴充為JsonNetResult
如果讀者想使用Json.NET來擴充原有的ActionResult,可以參考Json.NET的作者James所寫的JsonNetResult或是參考黑大所寫的JsonNetController版本。這都是能直接上線使用的程式碼,我就不再多說明。
我就是速度(I am speed)
最近在進行專案效能調教,在MVC專案中使用了大量的JsonResult,在黑大JSON轉換效能評比-Json.NET,就決定是你了!得到JavaScriptSerializer早就應該退休了的結論,取代ASP.NET MVC預設JsonResult是個快又有效的一步。不過在這個唯快不破的時代,我想找找是否還有更好的選擇。
我發現一個JSON(反)序列化新選擇-Jil。
以下效能圖表參考原專案:
從圖表可以發現,我們最常使用的JSON.NET表現可以說是中規中矩,但Jil速度上更勝一籌。(另一套由Google發展的Protobuf,速度可以說是第一名,但我沒有考慮是因為它在nuget上停在2013年就無更新了,而Jil在我撰文期間都還收到更新的通知。)
不過那是別人的圖表,我還是半信半疑,我決定用黑大的效能評比程式做一個Jil的評比:
Json.net與Jil評比程式
class Program { static void Main(string[] args) { //CreateSerializedData(); TestJsonNet(); TestJil(); } private static void TestJil() { //隨機假造20萬筆User資料 List<User> bigList = GenSimData(); string fileName = "serialized.data"; int indexToTest = 1024; //用來比對測試的筆數 //序列化前取出第indexToTest筆資料的顯示內容 string beforeSer = bigList[indexToTest].Display, afterDeser = null; Stopwatch sw = new Stopwatch(); sw.Start(); //將List<User> JSON化 string json1 = JSON.Serialize(bigList); //string json1 = JSON.Serialize<List<User>>(bigList); //string json1 = JSON.SerializeDynamic(bigList); sw.Stop(); Console.WriteLine("Serialization: {0:N0}ms", sw.ElapsedMilliseconds); sw.Reset(); sw.Start(); //由檔案字串反序列化還原回List<User> using (FileStream stm = new FileStream(fileName, FileMode.Open)) { //還原後一樣取出第indexToTest筆的User顯示內容 afterDeser = (JSON.Deserialize<List<User>>(json1)) [indexToTest].Display; } sw.Stop(); Console.WriteLine("Deserialization: {0:N0}ms", sw.ElapsedMilliseconds); //比對還原後的資料是否相同 Console.WriteLine("Before: {0}", beforeSer); Console.WriteLine("After: {0}", afterDeser); Console.WriteLine("Pass Test: {0}", beforeSer.Equals(afterDeser)); Console.Read(); } private static void TestJsonNet() { //隨機假造20萬筆User資料 List<User> bigList = GenSimData(); string fileName = "serialized.data"; int indexToTest = 1024; //用來比對測試的筆數 //序列化前取出第indexToTest筆資料的顯示內容 string beforeSer = bigList[indexToTest].Display, afterDeser = null; Stopwatch sw = new Stopwatch(); sw.Start(); //將List<User> JSON化 string json1 = JsonConvert.SerializeObject(bigList); sw.Stop(); Console.WriteLine("Serialization: {0:N0}ms", sw.ElapsedMilliseconds); sw.Reset(); sw.Start(); //由檔案字串反序列化還原回List<User> using (FileStream stm = new FileStream(fileName, FileMode.Open)) { //還原後一樣取出第indexToTest筆的User顯示內容 afterDeser = (JsonConvert.DeserializeObject<List<User>>(json1)) [indexToTest].Display; } sw.Stop(); Console.WriteLine("Deserialization: {0:N0}ms", sw.ElapsedMilliseconds); //比對還原後的資料是否相同 Console.WriteLine("Before: {0}", beforeSer); Console.WriteLine("After: {0}", afterDeser); Console.WriteLine("Pass Test: {0}", beforeSer.Equals(afterDeser)); Console.Read(); } private static void CreateSerializedData() { //隨機假造20萬筆User資料 List<User> bigList = GenSimData(); string fileName = "serialized.data"; int indexToTest = 1024; //用來比對測試的筆數 //序列化前取出第indexToTest筆資料的顯示內容 string beforeSer = bigList[indexToTest].Display, afterDeser = null; DataContractSerializer dcs = new DataContractSerializer(bigList.GetType()); Stopwatch sw = new Stopwatch(); sw.Start(); //將List<User>序列化後寫入檔案 using (FileStream stm = new FileStream(fileName, FileMode.Create)) { dcs.WriteObject(stm, bigList); } //using (FileStream stm = new FileStream(fileName, FileMode.Create)) //{ // //用GZipStream把FileStream包起來 // using (GZipStream zip = new GZipStream(stm, CompressionMode.Compress)) // { // //序列化結果改寫入GZipStream // dcs.WriteObject(zip, bigList); // } //} sw.Stop(); Console.WriteLine("Serialization: {0:N0}ms", sw.ElapsedMilliseconds); sw.Reset(); sw.Start(); //由檔案反序列化還原回List<User> using (FileStream stm = new FileStream(fileName, FileMode.Open)) { //還原後一樣取出第indexToTest筆的User顯示內容 afterDeser = (dcs.ReadObject(stm) as List<User>)[indexToTest].Display; } //using (FileStream stm = new FileStream(fileName, FileMode.Open)) //{ // //一樣用GZipStream把FileStream包起來 // using (GZipStream zip = new GZipStream(stm, CompressionMode.Decompress)) // { // //還原的二進位資料來源改為GZipStream // afterDeser = (dcs.ReadObject(zip) as List<User>)[indexToTest].Display; // } //} sw.Stop(); Console.WriteLine("Deserialization: {0:N0}ms", sw.ElapsedMilliseconds); //比對還原後的資料是否相同 Console.WriteLine("Before: {0}", beforeSer); Console.WriteLine("After: {0}", afterDeser); Console.WriteLine("Pass Test: {0}", beforeSer.Equals(afterDeser)); Console.Read(); } private static List<User> GenSimData() { List<User> lst = new List<User>(); Random rnd = new Random(); for (int i = 0; i < 200000; i++) { lst.Add(new User() { Id = Guid.NewGuid(), RegDate = DateTime.Today.AddDays(-rnd.Next(5000)), Name = "User" + i, Score = rnd.Next(65535) }); } return lst; } [Serializable] private class User { public Guid Id { get; set; } public DateTime RegDate { get; set; } public string Name { get; set; } public decimal Score { get; set; } public string Display { get { return string.Format("{0} / {1:yyyy-MM-dd} / {2:N0}", Name, RegDate, Score); } } } }
Json.Net-20萬筆測試結果:
Serialization: 1,016ms
Deserialization: 1,051ms
Before: User1024 / 2013-12-26 / 61,284
After: User1024 / 2013-12-26 / 61,284
Pass Test: True
Jil-20萬筆測試結果:
Serialization: 901ms
Deserialization: 376ms
Before: User1024 / 2012-01-22 / 39,551
After: User1024 / 2012-01-21 / 39,551
Pass Test: False
Jil在序列化部分小勝JSON.NET,但在反序列化部分,那376ms是用倍級的差距獲勝,速度快到嚇人。自己測試取得的數據,無非為自己打了一針強心針。令人動心。
不過我們也最注意Jil的在最後反序列化的日期有些問題,造成測試失敗的結果。
Jil的日期
Jil在序列化時採用MicrosoftStyleMillisecondsSinceUnixEpoch,也就是和JavaScriptSerializer一樣的"\/Date(628318530718)\/"格式,這部分可以指定Options選項來修正:
private static void JilDateTime() { var dt = DateTime.Now; Console.WriteLine(JSON.Serialize(dt)); // 二種指定 Options 選擇方式 Console.WriteLine(JSON.Serialize(dt, new Options(dateFormat: Jil.DateTimeFormat.ISO8601))); Console.WriteLine(JSON.Serialize(dt, Options.ISO8601)); Console.WriteLine(JSON.Deserialize<DateTime>(JSON.Serialize(dt)).ToLocalTime()); Console.Read(); }
"\/Date(1455118355525)\/"
"2016-02-10T15:32:35.5258567Z"
"2016-02-10T15:32:35.5258567Z"
2016/2/10 下午 11:32:35
在反序列化時我加上了.ToLocalTime()以修改時區造成的時間不正確的問題。原因在官網的組態段落有說明:
ISO8601, a string
- for DateTimes & DateTimeOffsets, ie. "2011-07-14T19:43:37Z"
- DateTimes are always serialized in UTC (timezone offset = 00:00), because Local DateTimes cannot reliably roundtrip
- DateTimeOffsets include their timezone offset when serialized
- for TimeSpans, ie. "P40DT11H10M9.4S"
修正我們的Jil測試程式,除了在(反)序列化加上Options選擇外,在Display屬性加上.ToLocalTime()來修正時區問題:
public string Display { get { return string.Format( "{0} / {1:yyyy-MM-dd} / {2:N0}", Name, RegDate.ToLocalTime(), Score); } }
重新跑一次測試程式:
Serialization: 991ms
Deserialization: 402ms
Before: User1024 / 2007-03-16 / 55,103
After: User1024 / 2007-03-16 / 55,103
Pass Test: True
不只速度,反序列化後的日期也正確無誤了。
小結
Jil(反)序列化就是以效能為主軸,速度的表現也非常可圈可點。時間格式是要注意的小地方,Options.ISO8601是我們比較習慣使用的格式。由時間問題我們也學習到,Jil在序列化如果碰到時間是會進行UTC時區處理,所以在反序列化時要記得還原成當地時間。
Jil效能通過了驗證,那麼下一集:「取代JsonResult新選擇-招喚JilResult」。
Github Sample Source code:KK.JilTest
沒有留言:
張貼留言
感謝您的留言,如果我的文章你喜歡或對你有幫助,按個「讚」或「分享」它,我會很高興的。