對 JsonConvert 的認識太膚淺了,終於還是遇到了問題

一線碼農發表於2020-06-19

一:背景

1. 講故事

在開始本文之前,真的好想做個問卷調查,到底有多少人和我一樣,對 JsonConvert 的認識只侷限在 SerializeObjectDeserializeObject 這兩個方法上(┬_┬), 這樣我也好結伴同行,不再孤單落魄???,或許是這兩個方法基本上能夠解決工作中 80% 的場景,對於我來說確實是這樣,但隨著編碼的延續,終究還是會遇到那剩下的 20% ,所以呀。。。

我的場景是這樣的:前段時間寫業務程式碼的時候,我有一個自定義的客戶演算法型別的Model,這個Model中有這種演算法型別下的客戶群以及Report統計資訊,還用了 HashSet 記錄了該型別下的 CustomerID集合,為了方便講述,我把Model簡化如下:


    class CustomerAlgorithmModel
    {
        public string DisplayName { get; set; }

        public int CustomerType { get; set; }

        public ReprotModel Report { get; set; }

        public HashSet<int> CustomerIDHash { get; set; }
    }

    class ReprotModel
    {
        public int TotalCustomerCount { get; set; }

        public int TotalTradeCount { get; set; }
    }

那有意思的就來了,我個人是有記日誌的癖好,就想著以後不會出現死無對證的情況,然後就理所當然的使用 JsonConvert.SerializeObject, 這一下就出問題了,日誌送入到了 ElasticSearch ,然後通過 Kibana 查不出來,為啥呢? 看完上面的 Model 我想你也猜到了原因,json體太大了哈,好歹 CustomerIDHash 中也有幾十萬個撒,這一下全匯出成json了,這 size 還能小嗎? 要不我寫段程式碼看一看。


        static void Main(string[] args)
        {
            var algorithModel = new CustomerAlgorithmModel()
            {
                CustomerType = 1,
                DisplayName = "??",
                Report = new ReprotModel()
                {
                    TotalCustomerCount = 1000,
                    TotalTradeCount = 50
                },
                CustomerIDHash = new HashSet<int>(Enumerable.Range(1, 500000))
            };

            var json = JsonConvert.SerializeObject(algorithModel);

            File.WriteAllText("1.txt", json, Encoding.UTF8);

            Console.WriteLine("寫入完成!");
        }

可以看到,僅一個json就 3.3M,這樣的記錄多來幾打後,在 kibana 上一檢索,瀏覽器就卡的要死,其實 CustomerIDHash 這個欄位對我來說是可有可無的,就算存下來了也沒啥大用,所以需求就來了,如何遮蔽掉 CustomerIDHash

二:尋求解決方案

1. 使用 JsonIgnore

有問題就網上搜啊,這一搜馬上就有人告訴你可以使用 JsonIgnoreAttribute 忽略特性,加好這個特性後繼續跑一下程式。


    [Newtonsoft.Json.JsonIgnore]
    public HashSet<int> CustomerIDHash { get; set; }

太好了,終於搞定了,但是靜下心來想一想,總感覺心裡有那麼一點不舒服,為什麼這麼說,一旦你給這個 CustomerIDHash 套上了 JsonIgnore ,這就意味著它在 JsonConvet 的世界中從此消失,也不管是誰在使用這個Model, 但這並不是我的初衷,我的初衷僅僅是為了在記錄日誌的時候踢掉 CustomerIDHash,可千萬不要影響在其他場景下的使用哈,現在這種做法就會給自己,給別人挖坑,埋下了不可預知的bug,我想你應該明白我的意思,還得繼續尋找下一個方案。

2. 使用自定義的 JsonConverter

真的,Newtonsoft 太強大了,我都想寫一個專題好好彌補彌補我的知識盲區,其實在這個場景中不就是想把 HashSet<int> 給遮蔽掉嘛,Newtonsoft 中專門提供了一個針對特定型別的自定義處理類,接下來我就寫一段:


   /// <summary>
   /// 自定義一個 針對 HashSet<int> 的轉換類
   /// </summary>
   public class HashSetConverter : Newtonsoft.Json.JsonConverter<HashSet<int>>
   {
       public override HashSet<int> ReadJson(JsonReader reader, Type objectType, HashSet<int> existingValue, bool hasExistingValue, JsonSerializer serializer)
       {
           return existingValue;
       }

       public override void WriteJson(JsonWriter writer, HashSet<int> value, JsonSerializer serializer)
       {
           writer.WriteNull();
       }
   } 

就是這麼簡單,然後就可以在 SerializeObject 的時候指定下自定義的 HashSetConverter 即可,然後再將程式跑起來看一下。


 var json = JsonConvert.SerializeObject(algorithModel, Formatting.Indented, new HashSetConverter());

從圖中看,貌似也是解決了,但我突然發現自己要鑽牛角尖了,如果我的實體中又來了一個頂級優質客戶群的 TopNCustomerIDHash,但因為這個CustomerID 比較少,我希望在 Json 中能保留下來,然後就是踢掉的那個 CustomerIDHash 我要保留 CustomerIDHash.Length ,哈哈,搞事情哈,那接下來怎麼解決呢?

  • 修改 Model 實體

    class CustomerAlgorithmModel
    {
        public HashSet<int> CustomerIDHash { get; set; }

        // topN 優質客戶群
        public HashSet<int> TopNCustomerIDHash { get; set; }
    }

  • HashSetConverter 增加邏輯鑑別是否為保留欄位

        public override void WriteJson(JsonWriter writer, HashSet<int> value, JsonSerializer serializer)
        {
            if (writer.Path == "TopNCustomerIDHash")
            {
                writer.WriteStartArray();

                foreach (var item in value)
                {
                    writer.WriteValue(item);
                }

                writer.WriteEndArray();
            }
            else
            {
                writer.WriteValue(value.Count);
            }
        }

  • 最後給 TopNCustomerIDHash 賦值

            var algorithModel = new CustomerAlgorithmModel()
            {
                CustomerType = 1,
                DisplayName = "??",
                Report = new ReprotModel()
                {
                    TotalCustomerCount = 1000,
                    TotalTradeCount = 50
                },
                CustomerIDHash = new HashSet<int>(Enumerable.Range(1, 500000)),
                TopNCustomerIDHash = new HashSet<int>(Enumerable.Range(1, 10)),
            };

三塊都搞定後就可以把程式跑起來了,如下圖:

貌似鑽牛角尖的問題是解決了,既然鑽牛角尖肯定要各種鄙視,比如這裡的 ReportModel 我是不需要的,CustomerType 我也是不需要的,我僅僅需要看一下 DisplayNameTotalCustomerCount 這兩個欄位就可以了, 那這個要怎麼解決呢?

3. 使用 匿名型別

確實很多時候記日誌,就是為了跟蹤 Model 中你特別關心的那幾個欄位,所以摻雜了多餘的欄位確實也是沒必要的,這裡可以用匿名來解決,我就來寫一段程式碼:


    var json = JsonConvert.SerializeObject(new
    {
        algorithModel.DisplayName,
        algorithModel.Report.TotalCustomerCount
    }, Formatting.Indented);

三: 總結

雖然阻擊了幾個回合,但同時也發現了 Newtonsoft 中還有特別多的未挖掘功能,真的需要好好研究研究,原始碼已下好,接下來準備做個系列來解剖一下,值得一提的是 .Net中已自帶了 System.Text.Json.JsonSerializer 類,目前來看功能還不算太豐富,簡單用用還是可以的,本篇就說到這裡,希望對您有幫助。


如您有更多問題與我互動,掃描下方進來吧~

圖片名稱

相關文章