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

.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.ComponentModel.DataAnnotations;
using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace DemoSourceGenerator
{
    public class Student
    {
        public 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()
            {
                Id = 1,
                StuName = "Bruce",
                Birthday = DateTime.Parse("1996-08-24"),
                Address = "上海市浦東新區"
            };

            var jsonOptions = new JsonSerializerOptions()
            {
                WriteIndented = true,
                PropertyNameCaseInsensitive = true
            };

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

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

我們直接跑這個程序看看

跑了幾次下來,時間差距都在兩倍左右,當然按照官方所說,內存等方面也會有大幅度優化。

應用場景

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

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章