某個ASP.NET MVC Action需要頻繁傳回大型數字陣列,數字大部分是整數,但部分帶有1-2位小數,故陣列採double[]或decimal[]。經Json.NET轉換後有個小問題: 即便是整數,轉換結果也會帶有".0"字尾,例如: double d = 2,Json.NET轉成"2.0",而decimal有個有趣特性,小數尾端的零會被原原本本保存,例如: decimal d = 1.200M,d.ToString()為"1.200",JSON結果也是"1.200"。
本來不是什麼大不了的事,但是當陣列元素一多,原本個位數字1被轉成"1.0",傳輸內容便多兩個Byte,乘上陣列元素個數,佔用頻寬也算可觀。即便IIS有GZip壓縮,但網站效能調校就是這些細節優化所累積出來的。
由於JavaScript不像C#採用強型別,JSON傳回"1"或"1.0"不影響處理結果,我想在Json.NET轉換過程動手腳,當decimal或double是整數時,去除JSON".0"字尾降低傳輸量;若deicmal產出"1.200"這種帶小數零字尾字串,也去除字尾零,輸出"1.2"就好。
準備一個測試ASP.NET MVC Action如下,傳回用亂數產生的1萬筆double陣列,約75%為整數,25%為1位小數: (關於JsonNetResult類別請參考舊文)
staticdouble[] numArray = null;
staticdouble[] GetNumArray()
{
if (numArray == null)
{
Random rnd = new Random(32767);
List<double> lst = new List<double>();
for (int i = 0; i < 10000; i++)
{
var n = rnd.NextDouble() * 10;
if (rnd.Next() % 4 != 0) n = Math.Floor(n);
else n = Math.Round(n, 1);
lst.Add(n);
}
numArray = lst.ToArray();
}
return numArray;
}
public ActionResult LotOfData()
{
returnnew JsonNetResult()
{
Data = GetNumArray()
};
}
執行結果可以看到5.0, 0.0, 8.0...,一大堆帶有".0"的整數:
Json.NET提供了很棒的擴充性,可自訂JsonConverter針對特殊型別執行指定序列化邏輯。於是我寫了一顆MinifiedNumArrayConveter,繼承JsonConverter,實做CanConvert(),遇到型別為double[]或decimal[]時傳回true,代表支援這兩種型別的轉換;之後Json.NET在遇到double[]或decimal[]時就會呼叫MinifiedNumArrayConveter.WriteJson()。要去除字尾零,我的做法是將double、decimal用.ToString()轉成字串,若出現".0"字尾表示為整數直接去除".0";若字串包含小數點時則用TrimEnd()去掉字尾"0";其餘狀況則直接輸出ToString()。為求效率,我直接呼叫JsonWriter.WriteRawValue輸出處理好的字串,省去讓Json.NET再做一次數字轉字串的程序。另外,去尾零修正只適用WirteJson(),CanRead()一律傳回false代表不處理JSON讀取,並補上一個空的ReadJson()。
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Web.Models
{
publicclass MinifiedNumArrayConverter : JsonConverter
{
privatevoid dumpNumArray<T>(JsonWriter writer, T[] array)
{
foreach (T n in array)
{
var s = n.ToString();
if (s.EndsWith(".0"))
writer.WriteRawValue(s.Substring(0, s.Length - 2));
elseif (s.Contains("."))
writer.WriteRawValue(s.TrimEnd('0'));
else
writer.WriteRawValue(s);
}
}
publicoverridevoid WriteJson(JsonWriter writer, objectvalue,
JsonSerializer serializer)
{
writer.WriteStartArray();
Type t = value.GetType();
if (t == dblArrayType)
dumpNumArray<double>(writer, (double[])value);
elseif (t == decArrayType)
dumpNumArray<decimal>(writer, (decimal[])value);
else
thrownew NotImplementedException();
writer.WriteEndArray();
}
private Type dblArrayType = typeof(double[]);
private Type decArrayType = typeof(decimal[]);
publicoverridebool CanConvert(Type objectType)
{
if (objectType == dblArrayType || objectType == decArrayType)
returntrue;
returnfalse;
}
publicoverridebool CanRead
{
get { returnfalse; }
}
publicoverrideobject ReadJson(JsonReader reader, Type objectType,
object existingValue, JsonSerializer serializer)
{
thrownew NotImplementedException();
}
}
}
使用時很簡單,JsonConvert.SerializeObject()時要多傳SerializerSettings參數,用SerializerSettings.Converters.Add()掛上MinifiedNumArrayConverter物件即可。
public ActionResult LotOfFixedData()
{
var res = new JsonNetResult()
{
Data = GetNumArray()
};
res.SerializerSettings.Converters.Add(new MinifiedNumArrayConverter());
return res;
}
如此,JSON裡囉嗦的".0"通通消失了!
實際比較,測試樣本(1萬個數字的陣列,1/4為1位小數,3/4為整數)經MinifiedNumArrayConverter處理,Response大小由40352降到24880,減少38%!
這個技術如果要應用在WebAPI上,要將MinifiedNumArrayConverter加進GlobalConfiguration.Configuration.Formatters.JsonFormatter.SerializerSettings.Converters,App_Start/WebApiConfig.cs可修改如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Http;
using Web.Models;
namespace Web
{
publicstaticclass WebApiConfig
{
publicstaticvoid Register(HttpConfiguration config)
{
// Web API configuration and services
// Web API routes
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
//強制GET時也傳回JSON,不要傳回XML
GlobalConfiguration.Configuration.Formatters
.XmlFormatter.SupportedMediaTypes.Clear();
//加入自訂序列化轉換邏輯
GlobalConfiguration.Configuration.Formatters
.JsonFormatter.SerializerSettings.Converters.Add(
new MinifiedNumArrayConverter());
}
}
}
如此,所有傳回decimal[]或double[]的WebAPI方法,都會套用MinifiedNumArrayConverter,達到省略小數字尾零的效果。
最後,還有很重要的一點: 加入自訂序列化邏輯是否會嚴重損耗效能呢? 我做了以下實測,比較加入MinifiedNumArrayConverter前後的差異。
public ActionResult Test()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("<pre>");
var array = GetNumArray();
int times = 200;
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new MinifiedNumArrayConverter());
Stopwatch sw = new Stopwatch();
for (int run = 0; run < 5; run++)
{
string res = null;
sw.Reset();
sw.Start();
for (int i = 0; i < times; i++)
{
res = JsonConvert.SerializeObject(array);
}
sw.Stop();
sb.AppendFormat("\nStd JSON: {0:n0}ms \n {1}",
sw.ElapsedMilliseconds, res.Substring(0, 64));
sw.Reset();
sw.Start();
for (int i = 0; i < times; i++)
{
res = JsonConvert.SerializeObject(array, settings);
}
sw.Stop();
sb.AppendFormat("\nMinified JSON: {0:n0}ms \n {1}",
sw.ElapsedMilliseconds, res.Substring(0, 64));
}
string test = "[1.0,2.5,3.0]";
double[] test1 = JsonConvert.DeserializeObject<double[]>(test, settings);
decimal[] test2 = JsonConvert.DeserializeObject<decimal[]>(test, settings);
sb.AppendFormat("\n Deserialization Test: double[] {0}, decimal[] {1}",
test == JsonConvert.SerializeObject(test1) ? "PASS" : "FAIL",
test == JsonConvert.SerializeObject(test2) ? "PASS" : "FAIL"
);
sb.AppendLine("</pre>");
return Content(sb.ToString());
}
public ActionResult Test()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("<pre>");
var array = GetNumArray();
int times = 200;
JsonSerializerSettings settings = new JsonSerializerSettings();
settings.Converters.Add(new MinifiedNumArrayConverter());
Stopwatch sw = new Stopwatch();
for (int run = 0; run < 5; run++)
{
string res = null;
sw.Reset();
sw.Start();
for (int i = 0; i < times; i++)
{
res = JsonConvert.SerializeObject(array);
}
sw.Stop();
sb.AppendFormat("\nStd JSON: {0:n0}ms \n {1}",
sw.ElapsedMilliseconds, res.Substring(0, 64));
sw.Reset();
sw.Start();
for (int i = 0; i < times; i++)
{
res = JsonConvert.SerializeObject(array, settings);
}
sw.Stop();
sb.AppendFormat("\nMinified JSON: {0:n0}ms \n {1}",
sw.ElapsedMilliseconds, res.Substring(0, 64));
}
string test = "[1.0,2.5,3.0]";
double[] test1 = JsonConvert.DeserializeObject<double[]>(test, settings);
decimal[] test2 = JsonConvert.DeserializeObject<decimal[]>(test, settings);
sb.AppendFormat("\n Deserialization Test: double[] {0}, decimal[] {1}",
test == JsonConvert.SerializeObject(test1) ? "PASS" : "FAIL",
test == JsonConvert.SerializeObject(test2) ? "PASS" : "FAIL"
);
sb.AppendLine("</pre>");
return Content(sb.ToString());
}
程式共跑5次,每次各執行200次1萬個double數字的陣列JSON轉換,比較套用MinifiedNumArrayConverter與否的執行時間,最後順便測試DesrializeObject()在套用MinifiedNumArrayConverter後是否正常。測試結果如下:
Std JSON: 2,385ms [0.4,5.0,0.0,3.8,8.0,7.0,3.0,9.0,7.7,6.0,5.0,7.8,0.5,8.0,2.0,0.0 Minified JSON: 1,974ms [0.4,5,0,3.8,8,7,3,9,7.7,6,5,7.8,0.5,8,2,0,6.9,1,5.5,7,5,6.5,8,3 Std JSON: 1,615ms [0.4,5.0,0.0,3.8,8.0,7.0,3.0,9.0,7.7,6.0,5.0,7.8,0.5,8.0,2.0,0.0 Minified JSON: 1,720ms [0.4,5,0,3.8,8,7,3,9,7.7,6,5,7.8,0.5,8,2,0,6.9,1,5.5,7,5,6.5,8,3 Std JSON: 1,316ms [0.4,5.0,0.0,3.8,8.0,7.0,3.0,9.0,7.7,6.0,5.0,7.8,0.5,8.0,2.0,0.0 Minified JSON: 2,107ms [0.4,5,0,3.8,8,7,3,9,7.7,6,5,7.8,0.5,8,2,0,6.9,1,5.5,7,5,6.5,8,3 Std JSON: 1,767ms [0.4,5.0,0.0,3.8,8.0,7.0,3.0,9.0,7.7,6.0,5.0,7.8,0.5,8.0,2.0,0.0 Minified JSON: 1,989ms [0.4,5,0,3.8,8,7,3,9,7.7,6,5,7.8,0.5,8,2,0,6.9,1,5.5,7,5,6.5,8,3 Std JSON: 1,591ms [0.4,5.0,0.0,3.8,8.0,7.0,3.0,9.0,7.7,6.0,5.0,7.8,0.5,8.0,2.0,0.0 Minified JSON: 1,786ms [0.4,5,0,3.8,8,7,3,9,7.7,6,5,7.8,0.5,8,2,0,6.9,1,5.5,7,5,6.5,8,3 Deserialization Test: double[] PASS, decimal[] PASS
第一次執行較耗時,依效能測試慣例略過不計,共取四次結果:
1,615ms vs 1,720ms
1,316ms vs 2,107ms
1,767ms vs 1,989ms
1,591ms vs 1,786ms
加入MinifiedNumArrayConverter後速度較慢,但執行兩百萬次的差異約在0.1到0.8秒之間,相較其所節省資料量,評估為划算的投資。