.net6&7中如何優雅且高效能的使用Json序列化

2022-12-02 18:01:18

.net中的SourceGenerator讓開發者編可以寫分析器,在專案程式碼編譯時,分析器分析專案既有的靜態程式碼,允許新增原始碼到GeneratorExecutionContext中,一同與既有的程式碼參與編譯。這種技術其實是把一些執行時才能去獲取程式集相關資源的方式提前到編譯前了。
.net6開始,微軟為我們提供了System.Text.Json的SourceGenerator版本,接下來我們一起基於一個.net6的控制檯專案學習瞭解System.Text.Json.SourceGenerator.
(SourceGenerator以下簡稱源生成)

反射 vs 源生成

目前基本所有的序列化和反序列化都是基於反射,反射是執行時的一些操作,一直以來效能差而被詬病。System.Text.Json中的JsonSerializer物件中的序列化操作也是基於反射的,我們常用的方法如下:
序列化:

JsonSerializer.Serialize(student, new JsonSerializerOptions()
{
    WriteIndented = true, 
    PropertyNameCaseInsensitive = true //不敏感大小寫
});

反序列化:

JsonSerializer.Deserialize<Student>("xxxx");

本身微軟就宣稱System.Text.Json.JsonSerializer效能是強於一個Newtonsoft,所以這兩年一直使用微軟自帶的。
當然話題扯遠了,只是帶大家稍微瞭解回顧下。
我們來看看微軟官網提供的反射和源生成兩種方式在Json序列化中的優劣:

1.可以看到反射的易用性和開放程度是高於源生成的。
2.效能方面則是源生成完全碾壓。

源生成注意點

1.源生成有兩種模式:後設資料收集和序列化優化,兩者的區別會在下面的實踐中給出自己的理解,官網並沒有得到較為明確的兩種的解釋,兩種生成模式可以同時存在。預設同時啟用。
2.源生成不能夠像反射一樣可以使用JsonInclude標籤將包含私有存取器的公共屬性包含進來,會拋NotSupportedException異常

後設資料收集&序列化優化

後設資料收集

可以使用源生成將後設資料收集程序從執行時移到編譯時。 在編譯期間,系統將收集後設資料並生成原始碼檔案。 生成的原始碼檔案會自動編譯為應用程式的一個整型部分。 使用此方法便無需進行執行時後設資料集合,這可提高序列化和反序列化的效能.

序列化優化:

這個就比較好理解一點了,無非就是對於序列化的一些設定選項和特性做出一些優化,當然目前不是所有設定和特性都支援,官網也列出了受支援的設定和特性。

設定選項:

特性:

好了說了這麼多,大家對一些概念都有了基本瞭解,我也很討厭這麼多文字的概念往上貼,那麼現在就進入實戰!

實戰

建立專案

一個.net6的控制檯專案,可以觀察到它的分析器裡有一個System.Text.Json.SourceGenerator這個解析器

建立一個序列化上下文

建立SourceGenerationContext派生自JsonSerializerContext

指定要序列化或反序列化的型別

通過向上下文類應用 JsonSerializableAttribute 來指定要序列化或反序列化的型別。
不需要為型別的欄位型別做特殊處理,但是如果型別包含object型別的物件,並且你知道,在執行時,它可能有 boolean 和 int 物件
則需要新增

[JsonSerializable(typeof(bool))]
[JsonSerializable(typeof(int))]

以增加對於這些型別的支援,便於源生成提前生成相關型別程式碼。

序列化設定

JsonSourceGenerationOptions可以新增一些序列化的設定設定。

序列化上下文最後程式碼:

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Student))] 
[JsonSerializable(typeof(Teacher))]
internal partial class SourceGenerationContext : JsonSerializerContext
{

}

分析器下會出現一些自動生成的程式碼:

序列化/反序列化

序列化:

JsonSerializer.Serialize(student, SourceGenerationContext.Default.Student);

反序列化:

var obj = JsonSerializer.Deserialize<Student>(
    jsonString, SourceGenerationContext.Default.Student);
指定源生成方式
後設資料收集模式

全部型別設定後設資料收集模式

[JsonSourceGenerationOptions(WriteIndented = true,GenerationMode =JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(Student))] 
[JsonSerializable(typeof(Teacher))]
internal partial class SourceGenerationContext : JsonSerializerContext
{

}

單個型別設定後設資料收集模式,只設定學生型別使用特定的後設資料收集模式

[JsonSourceGenerationOptions(WriteIndented = true,GenerationMode =JsonSourceGenerationMode.Metadata)]
[JsonSerializable(typeof(Student,GenerationMode =JsonSourceGenerationMode.Metadata))]
[JsonSerializable(typeof(Teacher))]
internal partial class SourceGenerationContext : JsonSerializerContext
{

}
序列化優化模式

全部型別設定序列化優化模式

[JsonSourceGenerationOptions(WriteIndented = true,GenerationMode =JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(Student))]
[JsonSerializable(typeof(Teacher))]
internal partial class SourceGenerationContext : JsonSerializerContext
{

}

單個型別設定序列化優化模式,只設定學生型別使用特定的序列化優化模式

[JsonSourceGenerationOptions(WriteIndented = true)]
[JsonSerializable(typeof(Student), GenerationMode = JsonSourceGenerationMode.Serialization)]
[JsonSerializable(typeof(Teacher))]
internal partial class SourceGenerationContext : JsonSerializerContext
{

}

注意點:如果不顯示設定源生成模式,那麼會同時應用後設資料收集和序列化優化兩種方式。

效果對比

說了這麼多,你憑啥說服我們使用這玩意兒??
我們試試使用JsonSerializer和源生成的方式來跑10000次序列化試試,說試就試,完整程式碼如下:

using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace DemoSourceGenerator
{
    public class Student
    {
        private int Id { get; set; }
        public string StuName { get; set; }
        public DateTime Birthday { get; set; }
        public string Address { get; set; }
    }

    public class Teacher 
    {
        public int Id { get; set; }
        public string TeacherName { get; set; }
        public DateTime Birthday { get; set; }
        public string Address { get; set; }
    }

    [JsonSourceGenerationOptions(WriteIndented = true)]
    [JsonSerializable(typeof(Student))]
    [JsonSerializable(typeof(Teacher))]
    internal partial class SourceGenerationContext : JsonSerializerContext
    {

    }

    public class Program 
    {
        public static void Main(string[] args) 
        {
            Student student = new Student()
            {
                StuName = "Bruce",
                Birthday = DateTime.Parse("1996-08-24"),
                Address = "上海市浦東新區"
            };

            Stopwatch stopwatch1 = new Stopwatch();
            stopwatch1.Start();
            foreach (var index in Enumerable.Range(0, 10000))
            {
                JsonSerializer.Serialize(student, new JsonSerializerOptions()
                {
                    WriteIndented = true,
                    PropertyNameCaseInsensitive = true
                });
            }
            stopwatch1.Stop();
            Console.WriteLine($"原始的序列化時間:{stopwatch1.ElapsedMilliseconds}");

            Stopwatch stopwatch2 = new Stopwatch();
            stopwatch2.Start();
            foreach (var index in Enumerable.Range(0, 10000))
            {
                JsonSerializer.Serialize(student, SourceGenerationContext.Default.Student);
            }
            stopwatch2.Stop();
            Console.WriteLine($"原始碼生成器的序列化時間:{stopwatch2.ElapsedMilliseconds}");
        }
    }
}

我們直接跑這個程式看看

跑了幾次下來,時間上差距了接近300倍!!!

應用場景

1.首先肯定是.net 6及其之後的版本,因為我們公司在升級一些服務到.net6,所以可以使用微軟提供的這個功能。
2.大量的使用到了序列化和反序列化,可以為建立一個上下文,將這這些型別通過JsonSerializable註冊到上下文中,當然也可以根據領域劃分多個上下文。

參考檔案

https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json/source-generation-modes?pivots=dotnet-7-0

https://learn.microsoft.com/zh-cn/dotnet/standard/serialization/system-text-json/source-generation-modes?pivots=dotnet-7-0

本文是本人按照官方檔案和自己的一些實際使用作出,如存在誤區,希望不吝賜教。