开源完美模块组件化可扩展的Xml解析器Hand.ParseXml

一、首先对比Xml反序列化

1. 性能对比

1.1 性能测试结果如下

  • Deserialize是反序列化结果
  • GetResult是基于反射的对象构建方法,比反序列化快31%
  • GetResult2是定制对象构建组件,避免了反射,比GetResult快一点
  • Custom继承重写EntityParser,避免字典查找,比GetResult2更快一点
Method Mean Error StdDev Median Ratio Gen0 Gen1 Allocated Alloc Ratio
Deserialize 1,349.1 ns 6.44 ns 6.89 ns 1,349.2 ns 1.00 0.7240 0.0300 12.2 KB 1.00
GetResult 924.9 ns 5.21 ns 6.00 ns 924.7 ns 0.69 0.6750 0.0250 11.38 KB 0.93
GetResult2 802.2 ns 2.22 ns 2.38 ns 803.5 ns 0.59 0.6500 - 10.95 KB 0.90
Custom 767.9 ns 1.94 ns 2.08 ns 767.7 ns 0.57 0.6500 - 10.95 KB 0.90

1.2 相关代码如下

csharp 复制代码
    private static readonly XmlSerializer _serializer = new(typeof(User));
    private static readonly FirstReader<User> _parser = HandXml.Default.Entity<User>()
        .WithItem<int>(nameof(User.Id))
        .WithItem(nameof(User.Name))
        .WithItem<int>(nameof(User.Age))
        .First();
    private static readonly FirstReader<User> _parser2 = HandXml.Default.Entity(UserBuilder.Creater)
        .WithItem<int>(nameof(User.Id))
        .WithItem(nameof(User.Name))
        .WithItem<int>(nameof(User.Age))
        .First();
    private static readonly UserParser _customParser = new(HandXml.Default);

    [Benchmark(Baseline = true)]
    public User? Deserialize()
    {
        using var stringReader = new StringReader(text);
        return (User?)_serializer.Deserialize(stringReader);
    }
    [Benchmark]
    public User GetResult()
    {
        return _parser.Get(text);
    }
    [Benchmark]
    public User GetResult2()
    {
        return _parser2.Get(text);
    }
    [Benchmark]
    public User Custom()
    {
        return _customParser.Get(text);
    }
csharp 复制代码
[Serializable]
public class User
{
    public int Id { get; set; }
    public string Name { get; set; }
    public int Age { get; set; }
}
/// <summary>
/// 建造者模式
/// </summary>
public class UserBuilder2
    : IMemberBuilder<User>
{
    private readonly User _original = new();
    /// <summary>
    /// 原始对象
    /// </summary>
    public User Original
        => _original;
    /// <inheritdoc />
    public User Build()
        => _original;
    /// <inheritdoc />
    public void Save<TMember>(string name, TMember value)
    {
        switch (name)
        {
            case nameof(User.Id):
                if (value is int idValue)
                    _original.Id = idValue;
                break;
            case nameof(User.Name):
                if (value is string nameValue)
                    _original.Name = nameValue;
                break;
            case nameof(User.Age):
                if (value is int ageValue)
                    _original.Age = ageValue;
                break;
            default:
                break;
        }
    }
    /// <summary>
    /// 工厂模式
    /// </summary>
    public static readonly ICreator<UserBuilder2> Creater = new DefaultCreater<UserBuilder2>();
}
public class UserParser(HandXml xml)
    : EntityParser<User>(xml, UserBuilder2.Creater, null, true)
{
    #region 配置
    private readonly IXmlParser<int> _id = xml.Content<int>();
    private readonly ContentReader _name = xml.Content();
    private readonly IXmlParser<int> _age = xml.Content<int>();
    #endregion
    /// <inheritdoc />
    public override void ReadAttributes(IMemberStore entity, XmlReader reader) { }
    /// <inheritdoc />
    public override void ReadItem(IMemberBuilder<User> entity, XmlReader reader, string name)
    {
        if (entity is UserBuilder2 builder)
            ReadItem(builder.Original, reader, name);
        else
            base.ReadItem(entity, reader, name);
    }
    /// <summary>
    /// 使用自定义构造器
    /// </summary>
    /// <param name="entity"></param>
    /// <param name="reader"></param>
    /// <param name="name"></param>
    public void ReadItem(User entity, XmlReader reader, string name)
    {        
        switch (name)
        {
            case nameof(User.Id):
                if (_id.TryParser(reader, out var idResult))
                    entity.Id = idResult;
                break;
            case nameof(User.Name):
                if (_name.TryParser(reader, out var nameResult))
                    entity.Name = nameResult;
                break;
            case nameof(User.Age):
                if (_age.TryParser(reader, out var ageResult))
                    entity.Age = ageResult;
                break;
        }
    }
}

1.3 性能分析

  • ParseXml开箱即用性能就比XmlSerializer好不少
  • 通过定制对象构建组件或扩展重写能进一步提高性能

2. 通用性对比

2.1 XmlSerializer通过Attribute标记映射

  • 通过XmlRoot、XmlAttribute和XmlElement来映射属性
  • 一个类只能支持一种格式的xml文件
csharp 复制代码
[XmlRoot("Root")]
public class Person
{
    [XmlAttribute("PersonId")]
    public int Id { get; set; }
    [XmlElement("PersonName")]
    public string Name { get; set; }

    private static readonly XmlSerializer _serializer = new(typeof(Person));

    [Fact]
    public void Deserialize()
    {
        var text = @"<?xml version=""1.0"" encoding=""utf-8""?>
            <Root PersonId=""1"">
    	        <PersonName>张三</PersonName>
            </Root>";
        using var stringReader = new StringReader(text);
        Person? person = _serializer.Deserialize(stringReader) as Person;
        Assert.NotNull(person);
        Assert.Equal(1, person.Id);
        Assert.Equal("张三", person.Name);
    }
    [Fact]
    public void Throw()
    {
        var text = @"<?xml version=""1.0"" encoding=""utf-8""?>
            <Person>
                <Id>1</Id>
    	        <Name>张三</Name>
            </Person>";
        using var stringReader = new StringReader(text);
        Assert.ThrowsAny<InvalidOperationException>(() => _serializer.Deserialize(stringReader));
    }
}

2.2 ParseXml通过Fluent API配置

  • WithItem配置节点
  • WithAttribute配置属性
  • Entity支持可选参数配置当前标签映射
  • 映射的成员可以是属性也可以是字段和构造函数的参数
  • 配置的解析Xml对象可以定义为静态或注入IOC容器,该对象解析Xml不产生状态支持多线程同时调用(线程安全)
  • 同一个类可以配置多个解析对象用来把不同格式的Xml转化为相同类型的对象
csharp 复制代码
public record User(int Id, string Name);
.
    [Fact]
    public void WithNode()
    {
        var id = 123;
        var name = "Jxj";
        var text = @$"<?xml version=""1.0"" encoding=""utf-8""?>
<User>
	<Id>{id}</Id>
	<Name>{name}</Name>
</User>";

        var userParser = HandXml.Default.Entity<User>()
            .WithItem<int>(nameof(User.Id))
            .WithItem(nameof(User.Name))
            .First();
        User result = userParser.Get(text);
        Assert.NotNull(result);
        Assert.Equal(id, result.Id);
        Assert.Equal(name, result.Name);
    }
    [Fact]
    public void WithAttribute()
    {
        var id = 123;
        var name = "Jxj";
        var text = @$"<?xml version=""1.0"" encoding=""utf-8""?>
            <User Id=""{id}"" Name=""{name}"" />";

        var userParser = HandXml.Default.Entity<User>()
            .WithAttribute<int>(nameof(User.Id))
            .WithAttribute(nameof(User.Name))
            .First("User");
        User result = userParser.Get(text);
        Assert.NotNull(result);
        Assert.Equal(id, result.Id);
        Assert.Equal(name, result.Name);
    }
    [Fact]
    public void Content()
    {
        var id = 123;
        var name = "Jxj";
        var text = @$"<?xml version=""1.0"" encoding=""utf-8""?>
            <User Id=""{id}"">{name}</User>";

        var userParser = HandXml.Default.Entity<User>(nameof(User.Name))
            .WithAttribute<int>(nameof(User.Id))
            .First("User");
        User result = userParser.Get(text);
        Assert.NotNull(result);
        Assert.Equal(id, result.Id);
        Assert.Equal(name, result.Name);
    }

2.3 通用性分析

| 特性 | XmlSerializer | ParseXml |

| :---------- |: ---------------- | :----------------------------- |

| 同类Xml格式 | 只支持一种格式 | 支持多种 |

| 映射对象 | 只映射属性 | 映射属性、字段和构造函数参数 |

| 映射Xml结构 | 支持属性和子节点 | 支持属性、子节点和当前节点 |

| 结果类型 | object需要转化 | 当前类型,直接使用 |

二、ParseXml主要功能

1. 读取单个节点

1.1 读取原始值

  • 使用Single方法读取单个节点原始值
csharp 复制代码
var summaryReader = HandXml.Default.First("summary");
string summary = summaryReader.Get(xmlReader);

1.2 读取强类型值

  • 使用Single泛型方法读取单个节点强类型值
csharp 复制代码
var idParser = HandXml.Default.First<int>("Id");
int id = idParser.Get(xmlReader);

2. 读取单个属性

2.1 读取原始值

  • 使用Attribute方法读取属性原始值
csharp 复制代码
var text = @"<member name = ""F:GenerateConvertTests.Supports.ColumnType.Identity"">
    <summary>
    自增列
    </summary>
</member>";
using var stringReader = new StringReader(text);
using var xmlReader = XmlReader.Create(stringReader);
var nameReader = HandXml.Default.Attribute("name")
    .First("member");
string name = nameReader.Get(xmlReader);

2.2 读取强类型值

  • 使用Attribute泛型方法读取属性强类型值
csharp 复制代码
var text = @$"<?xml version=""1.0"" encoding=""utf-8""?>
    <User Id=""123"">Jxj</User>";
using var stringReader = new StringReader(text);
using var xmlReader = XmlReader.Create(stringReader);
var idReader = HandXml.Default.Attribute<int>("Id")
    .First("User");
int result = idReader.Get(xmlReader);

3. 解析到实体

  • xml反序列化

3.1 从子节点解析

  • Entity泛型方法指定实体类型
  • WithItem方法读取子节点原始值并绑到实体
  • WithItem泛型方法读取子节点强类型值并绑到实体
csharp 复制代码
var text = @"<?xml version=""1.0"" encoding=""utf-8""?>
    <User>
	    <Id>123</Id>
	    <Name>Jxj</Name>
	    <Age>20</Age>
    </User>";
 using var stringReader = new StringReader(text);
 using var xmlReader = XmlReader.Create(stringReader);
 var userParser = HandXml.Default.Entity<User>()
     .WithItem<int>(nameof(User.Id))
     .WithItem(nameof(User.Name))
     .WithItem<int>(nameof(User.Age))
     .First();
 User result = userParser.Get(xmlReader);

3.2 从属性解析

  • Entity泛型方法指定实体类型
  • WithAttribute方法读取属性原始值并绑到实体
  • WithAttribute泛型方法读取属性强类型值并绑到实体
csharp 复制代码
var text = @$"<?xml version=""1.0"" encoding=""utf-8""?>
    <User Id=""123"" Name=""Jxj"" Age=""20"" />";
using var stringReader = new StringReader(text);
using var xmlReader = XmlReader.Create(stringReader);
var config = HandXml.Default;
var userParser = config.Entity<User>()
    .WithAttribute<int>(nameof(User.Id))
    .WithAttribute(nameof(User.Name))
    .WithAttribute<int>(nameof(User.Age))
    .First("User");
User result = userParser.Get(xmlReader);

3.3 从本节点解析

  • Entity可选参数contentName指定本节点原始值绑定实体的成员名
csharp 复制代码
var text = @$"<?xml version=""1.0"" encoding=""utf-8""?>
    <User Id=""123"" Age=""age"">Jxj</User>";
using var stringReader = new StringReader(text);
using var xmlReader = XmlReader.Create(stringReader);
var config = HandXml.Default;
var userParser = config.Entity<User>(nameof(User.Name))
    .WithAttribute<int>(nameof(User.Id))
    .WithAttribute<int>(nameof(User.Age))
    .First("User");
User result = userParser.Get(xmlReader);

4. 解析集合

  • 通过Repeat方法定义集合解析
csharp 复制代码
var text = @"<?xml version=""1.0"" encoding=""utf-8""?>
    <Users>
    <User>
        <Id>1</Id>
        <Name>张三</Name>
    </User>
    <User>
        <Id>2</Id>
        <Name>李四</Name>
    </User>
    <User>
        <Id>3</Id>
        <Name><![CDATA[<B>王二</B>]]></Name>
    </User>
    </Users>";
using var stringReader = new StringReader(text);
using var xmlReader = XmlReader.Create(stringReader);
var config = HandXml.Default;
var elementReader = config.Entity<User>()
    .WithItem<int>(nameof(User.Id))
    .WithItem(nameof(User.Name))
    .Repeat(nameof(User));
User[] result = elementReader.Get(xmlReader)
    .ToArray();

5. 包装类型转化

  • Convert方法转入一个转化器可以变成另一个类型的解析器
csharp 复制代码
var text = @$"<?xml version=""1.0"" encoding=""utf-8""?>
    <User Id=""123"">Jxj</User>";
using var stringReader = new StringReader(text);
using var xmlReader = XmlReader.Create(stringReader);
var idReader = HandXml.Default.Attribute<long>("Id")
    .Convert(id => new UserId(id))
    .First("User");
UserId result = idReader.Get(xmlReader);

6. 复杂类型

  • 通过WithItem可以一个解析器绑定到主解析器的成员上
  • 通过WithRepeate可以把集合解析器绑定到主解析器的成员上

6.1 复杂类型的Case

csharp 复制代码
using var fs = new FileStream("rss.xml", FileMode.Open, FileAccess.Read);
using var xmlReader = XmlReader.Create(fs);
var config = HandXml.Default;
var imageParser = config.Entity<RssImage>()
    .WithItem("url", nameof(RssImage.Url))
    .WithItem("name", nameof(RssImage.Name));
var itemParser = config.Entity<RssItem>()
    .WithItem("title", nameof(RssItem.Title))
    .WithItem("link", nameof(RssItem.Link))
    .Repeat("item");
var rssParser = config.Entity<Rss>()
    .WithItem<string>("title", nameof(Rss.Title))
    .WithItem(imageParser, "image", nameof(Rss.Image))
    .WithRepeat(itemParser, nameof(Rss.Items));
Rss rss = rssParser.Get(xmlReader);

6.2 支持绑定子解析器的WithItem重载方法

  • item为子解析器
  • node为调用该解析器的子节点名
  • member为子解析器解析结果绑定实体成员名
  • TItem为子解析器解析结果和实体成员共同的类型
  • 如果类型不一致结果会被抛弃
csharp 复制代码
EntityParser<TEntity> WithItem<TItem>(IXmlParser<TItem> item, string node, string member);

6.3 WithRepeat方法

  • repeate为集合解析器
  • member为绑定实体成员名
  • 类型为TItem\[\]
  • 如果实体成员类型不是TItem\[\]不能用该方法,应该是WithItem结合Convert来使用
csharp 复制代码
EntityParser<TEntity> WithRepeat<TItem>(RepeatReader<TItem> repeat, string member);

三、ParseXml的模块化

1. HandXml是ParseXml的配置基座

  • HandXml包含Builders、Converters和DefaultValues
  • Builders、Converters和DefaultValues参与到解析Xml的各个过程内
  • 通过Builders、Converters和DefaultValues配置可以调整Xml解析的结果和性能
csharp 复制代码
class HandXml
{
    /// <summary>
    /// 构造器提供者
    /// </summary>
    IMemberBuilderProvider Builders { get; }
        /// <summary>
    /// 转化器
    /// </summary>
    XmlConvertCacher Converters { get; }
        /// <summary>
    /// 默认值提供者
    /// </summary>
    DefaultValueBuilder DefaultValues { get; }
}

1.1 Builders负责对象构建

  • 默认使用反射构建
  • 支持配置
csharp 复制代码
    /// <summary>
    /// 注册工厂
    /// </summary>
    /// <typeparam name="TProvider"></typeparam>
    /// <typeparam name="TEntity"></typeparam>
    /// <param name="provider"></param>
    /// <param name="creator"></param>
    /// <returns></returns>
    TProvider UseCreator<TProvider, TEntity>(this TProvider provider, ICreator<IMemberBuilder<TEntity>> creator);
        /// <summary>
    /// 注册工厂
    /// </summary>
    /// <typeparam name="TProvider"></typeparam>
    /// <typeparam name="TEntity"></typeparam>
    /// <param name="provider"></param>
    /// <param name="func"></param>
    /// <returns></returns>
    TProvider UseCreator<TProvider, TEntity>(this TProvider provider, Func<IMemberBuilder<TEntity>> func);

1.2 Converters负责xml节点原始处理

  • 默认使用XmlConvert处理
  • 默认支持18种基础类型(bool、byte、short、char、sbyte、ushort、int、uint、long、ulong、float、double、decimal、DateTime、string、Guid、DateTimeOffset和TimeSpan等)
  • 支持配置
csharp 复制代码
/// <summary>
/// 配置转化器
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <typeparam name="TConverter"></typeparam>
/// <param name="converter"></param>
HandXml Use<TValue, TConverter>(TConverter converter);
/// <summary>
/// 配置转化器
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="converter"></param>
HandXml Use<TValue>(Converter<string, TValue> converter);

1.3 DefaultValues负责默认值

  • 各类型的默认值默认是default
  • 支持配置
csharp 复制代码
/// <summary>
/// 设置默认值
/// </summary>
/// <typeparam name="TValue"></typeparam>
/// <param name="value"></param>
/// <returns></returns>
DefaultValueBuilder Use<TValue>(TValue value);

四、ParseXml的组件化

1. Xml解析接口IXmlParser

  • 对Xml单节点、属性、重复节点或者复杂节点的解析类都实现了IXmlParser
csharp 复制代码
/// <summary>
/// Xml解析接口
/// </summary>
/// <typeparam name="TResult"></typeparam>
interface IXmlParser<TResult>
{
    /// <summary>
    /// 解析
    /// </summary>
    /// <param name="reader"></param>
    /// <param name="result"></param>
    /// <returns></returns>
    bool TryParser(XmlReader reader, out TResult result);
}

2. IXmlParser的主要实现类

  • AttributeReader和ContentReader是基础实现
  • 其他类都是通过组装IXmlParser实例来实现
  • 通过完美的组件化可以模拟和解析复杂的Xml文件

| 类 | 作用 |

| ------- ------- ------- ------ | :------------------------------------------------------------------------- |

| AttributeReader | 读取属性的原始文本 |

| ContentReader | 读取节点的原始文本 |

| PrimitiveReader | 调用AttributeReader或ContentReader,使用Converters把文本转化为需要的类型 |

| ConvertParser<TSource, TDest> | 把一种类型的解析器封装为另一种类型的解析器 |

| FirstReader<TResult> | 解析碰到的第一个该节点 |

| RepeatReader<TResult> | 解析连续重复的该节点 |

| EntityParser<TEntity> | 解析包含多个属性或子节点为实体类型的成员 |

csharp 复制代码
class PrimitiveReader<TPrimitive>(IXmlParser<string> original, IConverter<string, TPrimitive> converter, TPrimitive defaultValue);
ConvertParser<TSource, TDest>(HandXml xml, IXmlParser<TSource> original, IConverter<TSource, TDest> converter, TDest defaultValue);
class FirstReader<TResult>(string element, IXmlParser<TResult> original, TResult defaultValue);
class RepeatReader<TResult>(HandXml xml, string name, IXmlParser<TResult> item);
class EntityParser<TEntity>(HandXml xml, ICreator<IMemberBuilder<TEntity>> creator, IMemberParser? content, bool hasItem = false);

五、ParseXml的可扩展性

1. EntityParser<TEntity>的扩展性

  • EntityParser可以用来处理复杂Xml和复杂类型
  • 可扩展性在处理复杂问题时可简化配置或提供性能

2. UserParser扩展EntityParser的示例

  • UserParser封装了对象解析器,提高性能
  • UserParser封装了节点配置,简化使用
  • 重写ReadAttributes跳过属性解析提高性能
  • 重写ReadItem,使用switch代替字典查找提高性能
csharp 复制代码
public class UserParser(HandXml xml)
    : EntityParser<User>(xml, UserBuilder2.Creater, null, true)
{
    #region 配置
    private readonly IXmlParser<int> _id = xml.Content<int>();
    private readonly ContentReader _name = xml.Content();
    private readonly IXmlParser<int> _age = xml.Content<int>();
    #endregion
    /// <inheritdoc />
    public override void ReadAttributes(IMemberStore entity, XmlReader reader) { }
    /// <inheritdoc />
    public override void ReadItem(IMemberBuilder<User> entity, XmlReader reader, string name)
    {
        if (entity is UserBuilder2 builder)
            ReadItem(builder.Original, reader, name);
        else
            base.ReadItem(entity, reader, name);
    }
    /// <summary>
    /// 使用自定义构造器
    /// </summary>
    /// <param name="entity"></param>
    /// <param name="reader"></param>
    /// <param name="name"></param>
    public void ReadItem(User entity, XmlReader reader, string name)
    {        
        switch (name)
        {
            case nameof(User.Id):
                if (_id.TryParser(reader, out var idResult))
                    entity.Id = idResult;
                break;
            case nameof(User.Name):
                if (_name.TryParser(reader, out var nameResult))
                    entity.Name = nameResult;
                break;
            case nameof(User.Age):
                if (_age.TryParser(reader, out var ageResult))
                    entity.Age = ageResult;
                break;
        }
    }
}

nuget安装: dotnet add package Hand.ParseXml --version 0.3.1-alpha

源码托管地址: https://github.com/donetsoftwork/HandCore.net/tree/master/Hand.ParseXml ,欢迎大家直接查看源码。

gitee同步更新:https://gitee.com/donetsoftwork/HandCore.net/tree/master/Hand.ParseXml

如果大家喜欢请动动您发财的小手手帮忙点一下Star,谢谢!!!

相关推荐
我命由我123452 天前
Android 构建项目问题:XML 片段出错,com.ctc.wstx.exc.WstxUnexpectedCharException
android·xml·android studio·android jetpack·android-studio·android runtime
PixelBai3 天前
JSON转XML使用教程:从入门到精通
xml·json
脑子运行超载6 天前
Jackson处理和mybatis的xml转换问题
xml·jackson·mybatis·javatype
tianyuanwo22 天前
深入解析 RISC-V 虚拟化中的 UEFI 固件配置:从 XML 到 NVRAM 的生命周期管理
xml·linux·risc-v
糖果店的幽灵24 天前
软件测试接口测试从入门到精通:常见接口类型与数据格式
xml·软件测试·json·接口测试·数据格式
摇滚侠25 天前
SpringMVC 入门到实战 配置类替换 XML 配置文件 86-91
xml·java·后端·spring·maven·intellij-idea
就叫_这个吧1 个月前
IDEA Mybatis xml文件,实现sql语句联想,自动填入补充
xml·mysql·intellij-idea·mybatis
范什么特西1 个月前
配置文件xml和properties
xml·前端
曹牧1 个月前
Oracle:xml转义
xml·数据库·oracle