KoobooJson - 更小更快的C# JSON序列化工具(基于表达式树构建)
在C#领域,有很多成熟的开源JSON框架,其中最著名且使用最多的是 Newtonsoft.Json ,然而因为版本迭代,其代码要兼容从net2.0到现在的最新的net框架,并且要支持.net平台下的其它语言,所以最新发布版本的Newtonsoft.Json其dll大小接近700k,另一方面,因为其复杂的迭代历史导致它的代码为了维护向下扩展性和向上兼容性而舍弃一些性能。
如果你不太在乎体积和性能的话,那么 Newtonsoft.Json 无疑是一款很好的选择。但是如果你在意性能的话,在github上仍然有一些出名的以速度为称的c# JSON框架,其中最为人知的应该是 JIL , JIL有着出色的性能是因为它采用了大量的加速技术,但这也带来了一些局限性,它不够灵活,对object类型的解析必须得调用它的另一个API,并且因为出于性能考虑其采用的是Emit技术,不易维护,在我的测试中有很多类型它不支持。但是JIL的地位是显而易见的,因为它的出现,github上有着很多相仿思路的以速度为称的JSON框架,几乎每个都称自己是最快的,但实际上很少有超越JIL的,并且它们中的大部分没有一个良好的文档,这导致我在做性能测试时,我想改个配置都得对源码全局搜索花费一定时间。
在说回程序集大小,JIL的最新发布版本是500k,并且其依赖一个库,加起来是800k大小。
那么,我讲这些,大家应该知道我想要表达什么!
是的,考虑到前面种种,这些都不是在某种场景最理想化的那种JSON库,所以我写了一款以体积更小,速度更快,类型覆盖更广的开源C# JSON框架,它叫:KoobooJson
在我正式介绍KoobooJson之前,我要介绍一下什么是Kooboo!
Kooboo是我们老板用C#编写的一个开源的非常神奇的网站开发工具,它是一个类CMS生成器,但其从数据库,前端引擎,到各种网络协议服务器都是用c#自主创造的,几乎很少使用到第三方库,它编译后的发布版本仅有几M,而正是因为仅仅只有几M,为了Json框架不要太影响主程序的大小,这才有了KoobooJson此次的诞生!
Kooboo是开源的:https://github.com/Kooboo/Kooboo
KoobooJson自然也是开源的:https://github.com/Kooboo/Json
在NuGet包中可以直接搜索 KoobooJson 下载使用即可
什么是KoobooJson?
KoobooJson是一款C#的JSON工具,其主要通过表达式技术构建,最低支持.NET4.5(可以支持.NET4.0,但考虑到一些因素,最终没有支持,有需要支持的可以自行源码分支更改。另外,几乎每个以性能号称的JSON框架都最低支持.NET4.5),最低支持.NET Core 2.0,体积小巧,性能出色,类型覆盖广是KoobooJson的优点!
- KoobooJson的优点
- 小巧
- 快速
- 覆盖类型广
- KoobooJson的实现 (后续我将出一篇新的文章详细讲解实现)
- 序列化
- 反序列化
- 功能介绍
- 忽略注释
- 忽略互引用所导致的堆栈循环
- 忽略Null值
- 排序特性
- Dictionary的Key格式
- JObject和JArray
- 忽略默认值元素
- 忽略序列化元素
- 序列化时仅包含该元素
- 时间格式
- 首字母大小写
- 别名特性
- 反序列化时指定构造函数
- 值格式化特性
- 全局Key格式化
- 其它
KoobooJson的优点
1. 小巧
目前KoobooJson只有130k, 并且没有任何额外依赖项, KoobooJson当前支持框架版本.NET4.5 .NET Core2 .NET Standard 2
2. 快速
KoobooJson 遵循JSON RFC8259规范, 是一款适用于C#的快速的Json文本序列化器
它基于表达式树构建, 在运行时会动态的为每个类型生成高效的解析代码, 这过程包括: 利用静态泛型模板进行缓存, 避免字典查询开销, 避免装箱拆箱消耗, 缓冲池复用, 加速字节复制...
KoobooJson生成代码的方式并没有采用Emit, 而是采用ExpressionTree. ExpressionTree相比Emit而言, 它不能像Emit直接写出最优的IL代码, 它依赖于下层的编译器, 在某些时候它会多生成一些不必要的IL代码路径, 故而性能上有所逊色. 但相较于几乎没有类型检查的Emit而言, ExpressionTree不会出现各种莫名其妙的错误, 它更加安全, 也更加容易扩展维护.
虽然ExpressionTree与Emit相比在性能方面可能会有所差异, 但是KoobooJson的表现却相当亮眼!
上图是使用BenchmarkDotNet在Net Core2.1上做的Json序列化和反序列化的性能测试,随机生成大量的测试数据,迭代100次后产生的结果,基准报告在这里
BenchmarkDotNet=v0.11.4, OS=Windows 10.0.17763.316 (1809/October2018Update/Redstone5) Intel Core i7-8550U CPU 1.80GHz (Kaby Lake R), 1 CPU, 8 logical and 4 physical cores .NET Core SDK=2.1.505 Host : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT Job-XEQPPS : .NET Core 2.1.9 (CoreCLR 4.6.27414.06, CoreFX 4.6.27415.01), 64bit RyuJIT IterationCount=100 LaunchCount=1 WarmupCount=1
3. 覆盖类型广
在类型定义上, KoobooJson并没有单独实现每个集合或键值对类型, 而是对这些FCL类型进行划分成不同的模板
a. KoobooJson将序列化分为5种类型:
- 原始类型 它包括 Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, and Single.
- 所有拥有键值对行为的类型 任何能够实现IDictionary<>或能够实现IDictionary且能够通过构造函数注入键值对的类型, 都将以键值对方式进行解析
- 所有拥有集合行为的类型 任何能够实现IEnumable并且满足IColloction的Add行为或拥有自己独特的集合行为且能够通过构造函数注入集合的类型, 都将以集合方式进行解析
- 特殊类型 如Nullable<>, Lazy<>, Guid, Datatable, DateTime, Type, Task, Thread, Timespan...等等这些特定的类型实现
- 常规Model的键值对类型 在KoobooJson中, 如果当类型不满足上述4种时, 将会以键值对的形式来对其解析, KoobooJson会对Model中公开的所有元素进行序列化, 在这个环节, 几乎配置器中所有的配置都是有关Model的. 诸如别名, 忽略特性, 指定构造函数, 忽略堆栈循环引用, 首字母大小写, 格式化器... 值得一提的是, 在对接口类型进行反序列化时, KoobooJson默认会自动创建并返回一个实现于该接口的对象.
b. 在对类型的解析上, 其中浮点型,日期时间类型, GUID的解析是参照了JIL的代码, 在此表示感谢.
作为一款活跃的Json库, KoobooJson会不断支持更多的类型, 这其中, 因为对FCL中的键值对和集合的行为进行归纳, 所以对于这两种类型, KoobooJson并不像其它框架一样去特定的为每种类型单独实现, 实际上, 第2和3所定义的规则可以容纳FCL中的大多数键值对或集合类型.
目前KoobooJson所覆盖的类型包括 : Hashtable, SortedList, ArrayList, IDictionary, Dictionary<,>, IList,List<>, IEnumerable<>, IEnumerable, ICollection, ICollection<>, Stack<>, Queue<>, ConcurrentBag<>, ConcurrentQueue<>, ConcurrentStack<>, SortedDictionary<,>, ConcurrentDictionary<,>, SortedList<,>, IReadOnlyDictionary<,>, ReadOnlyDictionary<,>, ObservableCollection<>, HashSet<>, SortedSet<>, LinkedList<>, ReadOnlyCollection<>, ArraySegment<>, Stack, Queue, IReadOnlyList<>, IReadOnlyCollection<>, ReadOnlyCollection<>, ISet<>, BitArray, URI, NameValueCollection, StringDictionary, ExpandoObject, StringBuilder, Nullable<>, Lazy<>, Guid, Datatable, DateTime, Type, Task, Thread, Timespan, Enum, Exception, Array[], Array,,,,,...
KoobooJson的实现
序列化
代码语言:javascript复制class UserModel
{
public object Obj;
public string Name;
public int Age;
}
string json = JsonSerializer.ToJson(new UserModel());
在对类型第一次序列化时, KoobooJson会为这个类型生成大致是这样的解析代码.
代码语言:javascript复制void WriteUserModel(UserModel model,JsonSerializerHandler handler)
{
...配置选项处理...格式化器处理...堆栈无限引用处理...
handler.sb.Write("Obj:")
WriteObject(model.Obj);//在序列化时将为Object类型做二次真实类型查找
handler.sb.Write("Name:")
WriteString(model.Name);
handler.sb.Write("Age:")
WriteInt(model.Age);
}
如果是List<UserModel>的话, 那么将生成这样的代码
代码语言:javascript复制handler.sb.Write("[")
foreach(var user in users)
{
WriteUserModel(user);
WriteComma()
}
handler.sb.Write("]")
在当前版本中, KoobooJson序列化使用的容器为StringBuilder, 与直接ref char[]相比, 多了一些额外的调用. 将考虑在下个版本中构建一个轻便的char容器, 并会区分对象大小, 考虑栈数组和通过预扫描大小来减少对内存的开销,这将显著提升序列化速度.
反序列化
在对类型进行第一次反序列化时, KoobooJson会为这个类型生成大致是这样的解析代码.
代码语言:javascript复制UserModel model = JsonSerializer.ToObject<UserModel>("{"Obj":3,"Name":"Tom","Age":18}");
void ReadUserModel(string json,JsonDeserializeHandler handler)
{
...Null处理...
ReadObjLeft()
空元素处理...构造函数处理...配置项处理...格式化器处理...
while(i-->0){
switch(gechar())
{
case 'O':
switch(getchar())
case 'b':
switch(getchar())
case 'j':
ReadQuote();
ReadObject();
if(getchar()==',')
i ;
}
}
ReadObjRight()
}
KoobooJson生成反序列化代码, KoobooJson会假设json格式完全正确, 没有预先读取Json结构部分, 而是直接使用代码来描述结构, 所以KoobooJson少了一次对json结构的扫描, 执行过程中如果json结构发生错误, 会直接抛出异常.
而对于key的匹配, KoobooJson生成的是逐个char的自动机匹配代码, 目前KoobooJson是以字典树为算法, 逐个char进行类型比较, 与一次比较多个char相比, 这种方式显然没有达到最小的查询路径, 不过在jit优化下, 两种方式实现经测试效率几乎一样.
在反序列化读取字符时, 因为是对类型动态生成编码, 提前知道每个类型中的元素的字节长度和其类型的值长度, 所以KoobooJson出于更高的性能对反序列化采取了指针操作, 并加速字节读取.
代码语言:javascript复制case 3:
if (*(int*)Pointer != *(int*)o) return false;
if (*(Pointer 2) != *(o 2)) return false;
goto True;
case 4:
if (*(long*)Pointer != *(long*)o) return false;
goto True;
case 5:
if (*(long*)Pointer != *(long*)o) return false;
if (*(Pointer 4) != *(o 4)) return false;
因为是指针操作, KoobooJson在反序列化环节几乎不需要去维护一个char池来存放下一个需要读取的json结构片段.
功能介绍
KoobooJson当前支持6个API调用
代码语言:javascript复制string Kooboo.Json.JsonSerializer.ToJson<T>(T value, JsonSerializerOption option=null)
T Kooboo.Json.JsonSerializer.ToObject<T>(string json, JsonDeserializeOption option=null)
object Kooboo.Json.JsonSerializer.ToObject(string json, Type type, JsonDeserializeOption option=null)
void Kooboo.Json.JsonSerializer.ToJson<T>(T value, StreamWriter streamWriter, JsonSerializerOption option = null)
T Kooboo.Json.JsonSerializer.ToObject<T>(StreamReader streamReader, JsonDeserializeOption option = null)
object Kooboo.Json.JsonSerializer.ToObject(StreamReader streamReader, Type type, JsonDeserializeOption option = null)
忽略注释
在json字符串的读取中KoobooJson会自动忽略注释
代码语言:javascript复制string json = @"
/*注释*/
{//注释
/*注释*/""Name"" /*注释*/: /*注释*/""CMS"" /*注释*/,//注释
/*注释*/
""Children"":[//注释
1/*注释*/,
2/*注释*/
]//注释
}//注释
/*此处*/
";
var obj = JsonSerializer.ToObject(json);
obj=>Name:CMS
obj=>Children:Array(2)
忽略互引用所导致的堆栈循环
代码语言:javascript复制class A
{
public B b;
}
class B
{
public A a;
}
A.b=B;
B.a=A;
A指向B, B指向A, 在序列化时这种情况会发生无限循环.可通过KoobooJson的序列化配置项中的属性来设定这种情况下所对应的结果
代码语言:javascript复制JsonSerializerOption option = new JsonSerializerOption
{
ReferenceLoopHandling = JsonReferenceHandlingEnum.Null
};
string json = JsonSerializer.ToJson(a, option);
json => {"b":{"a":null}}
------
ReferenceLoopHandling = JsonReferenceHandlingEnum.Empty
json => {"b":{"a":{}}}
-----
ReferenceLoopHandling = JsonReferenceHandlingEnum.Remove
json => {"b":{}}
忽略Null值
代码语言:javascript复制class A
{
public string a;
}
A.a=null;
JsonSerializerOption option = new JsonSerializerOption { IsIgnoreValueNull = true };
var json = JsonSerializer.ToJson(A, option);
json => {}
排序特性
代码语言:javascript复制class A
{
[JsonOrder(3)]
public int a;
[JsonOrder(2)]
public int b;
[JsonOrder(1)]
public int c;
}
可通过JsonOrder(int orderNum)来排序序列化的json元素位置. 如果是正常没有通过JsonOrder排序元素,那么解析出来的Json则是默认顺序:{"a":0,"b":0,"c":0} 上面样例通过JsonOrder排序后是这样的:{"c":0,"b":0,"a":0}
Dictionary的Key格式
在Json规范中,键值对的键必须是字符串类型,在KoobooJson中,对Key的类型允许所有基元类型(Boolean, Byte, SByte, Int16, UInt16, Int32, UInt32, Int64, UInt64, IntPtr, UIntPtr, Char, Double, and Single)和String,以及Datetime,GUID,Enum,共18种支持的类型
JObject和JArray
反序列化时,对Object的类型解析,最终将会产生5种结果: Bool,数值(long,ulong,double),String,JArray,JObject 其中,JArray代表着数组,它拥有List<object>的所有特性. JObject代表着键值对,它拥有Dictionary<string,object>的所有特性.
忽略默认值元素
代码语言:javascript复制[IgnoreDefaultValue]
class User
{
public int? Num { get; set; }
public int Age { get; set; }
public string Name { get; set; }
}
var json = JsonSerializer.ToJson(new User());
json=> {}
如果你想在序列化时,忽略默认值的元素,那么可以对这个类型标记IgnoreDefaultValue特性,也可以标记在属性或者字段上
忽略序列化元素
代码语言:javascript复制class A
{
[IgnoreKey]
public int a;
public int b;
}
可通过IgnoreKey特性来标记序列化和反序列化要忽略的元素 json => {"b":0} 当然, 也可以通过配置来动态选择忽略对象
代码语言:javascript复制JsonSerializerOption option = new JsonSerializerOption { IgnoreKeys = new List<string>(){"b"} };
var json = JsonSerializer.ToJson(A, option);
json => {}
序列化时仅包含该元素
代码语言:javascript复制class A
{
[JsonOnlyInclude]
public int a;
public int b;
public int c;
}
json => {"a":0}
如果一个model里包含几十个元素, 而你仅想序列化其中一个, 那么就没必要对每一个元素进行IgnoreKey标记,只需要对想要序列化的元素标记JsonOnlyInclude即可
时间格式
代码语言:javascript复制JsonSerializerOption option = new JsonSerializerOption { DatetimeFormat=DatetimeFormatEnum.ISO8601 };
json => 2012-01-02T03:04:05Z
JsonSerializerOption option = new JsonSerializerOption { DatetimeFormat=DatetimeFormatEnum.RFC1123 };
json => Thu, 10 Apr 2008 13:30:00 GMT
JsonSerializerOption option = new JsonSerializerOption { DatetimeFormat=DatetimeFormatEnum.Microsoft };
json => /Date(628318530718)/
首字母大小写
代码语言:javascript复制class A
{
public int name;
}
JsonSerializerOption option = new JsonSerializerOption { JsonCharacterRead=JsonCharacterReadStateEnum.InitialUpper };
json => {"Name":0}
在对model序列化时可以指定key的首字母大小写,反序列化时也可以设置对字符串不区分大小写.首字母大小写属于内嵌支持, 在解析时并不会影响性能
别名特性
代码语言:javascript复制 class A
{
[Alias("R01_Name")]
public int name;
}
json => {"R01_Name":0}
当元素被标记Alias后,KoobooJson无论序列化还是反序列化都会按照Alias来进行解析
反序列化时指定构造函数
代码语言:javascript复制class A
{
public A(){}
[JsonDeserializeCtor(3,"ss")]
public A(int a,string b){}
}
在反序列化的时候, 我们不得不调用构造函数来以此创建对象. 在常规情况下, KoobooJson会通过优先级自动搜索最合适的构造函数,其优先级顺序为: public noArgs => private noArgs => public Args => private Args, 这其中, 会对有参构造函数进行默认值构造.
然而你也可以显式通过[JsonDeserializeCtor(params object[] args)]特性来指定反序列化时的构造函数, 这样 当KoobooJson创建A实例的时候就不是通过new A(); 而是new A(3,"ss");
值格式化特性
代码语言:javascript复制 class A
{
[Base64ValueFormat]
public byte[] a;
}
当你需要来覆写由KoobooJson进行元素解析的行为时, 我们可以继承一个 ValueFormatAttribute 来覆写行为.
代码语言:javascript复制 class Base64ValueFormatAttribute:ValueFormatAttribute
{
public override string WriteValueFormat(object value,Type type, JsonSerializerHandler handler, out bool isValueFormat)
{
isValueFormat=true;
if(value==null)
return "null";
else
return ConvertToBase64((byte[])value);
}
public override object ReadValueFormat(string value,Type type, JsonDeserializeHandler handler, out bool isValueFormat)
{
isValueFormat=true;
if(value=="null")
return null;
else
return Base64Convert(value);
}
}
值格式化特性也可以标记在结构体或类上, 而另一点是对于值格式化器, 也可以以全局的方式来进行配置: 以序列化为例, 可通过 JsonSerializerOption中的GlobalValueFormat委托来进行配置
代码语言:javascript复制JsonSerializerOption.GlobalValueFormat=(value,type,handler,isValueFormat)=>
{
if(type==typeof(byte[]))
{
isValueFormat=true;
if(value==null)
return "null";
else
return ConvertToBase64((byte[])value);
}
else
{
isValueFormat=false;
return null;
}
}
值得注意的是,对于byte[]类型的base64解析行为, KoobooJson已经内嵌在配置项中, 只要设置JsonSerializerOption.IsByteArrayFormatBase64=true即可
全局Key格式化
对于Model中的Key处理, KoobooJson支持全局的Key格式化器.
代码语言:javascript复制 class R01_User
{
public string R01_Name;
public int R01_Age;
}
如果我们想把R01这个前缀给去掉, 只需要注册全局Key格式化器的委托即可
代码语言:javascript复制JsonSerializerOption.GlobalKeyFormat=(key,parentType,handler)=>
{
if(parentType==typeof(R01_User))
{
return key.Substring(4);
}
return key;
}
这样,出来的json是这样的:{"Name":"","Age":""}
同样, 对于反序列化,我们也同样应该注册:
代码语言:javascript复制JsonDeserializeOption.GlobalKeyFormat=(key,parentType)=>
{
if(parentType==typeof(R01_User))
{
return "R01_" key;
}
return key;
}
其它
作者:小曾
出处:https://cloud.tencent.com/developer/article/1430405 欢迎转载,但请保留以上完整文章,在显要地方显示署名以及原文链接。