用AOP思想改造一个服务器的数据存储

背景是有一个游戏服务器一直以来都是写SQL的, 后来改过一段时间的redis, 用的是别的员工写的类orm方式将实体类型映射成各种key-value对进行写入, 但是仍有一个缺点就是需要在增\删\改的时候显式调用API, 更糟糕的是要注明删\改的字段名, 不然就会整个实体重写入. 实际使用中经常会出现写错字段名, 或是重命名字段之后忘记修改旧的字段名字符串参数, 甚至忘记调用update API导致数据没有保存...(同样的情况也发生在写SQL的时候)

其实增/删/改用到的SQL或是reids api都非常简单, 都可以自动生成, 免去反复书写的麻烦也容易写错的问题. 

于是乎, 我计划通过监视/拦截增/删/改 方法以在数据修改的同时自动生成SQL/Redis调用, 来对改属性值进行保存. 其实增删比较简单, 只要自定义一个集合类型继承下IEnumerable<T>接口, 然后实现增删方法即可, 对原代码的修改量也是极小的. 

但是改就有点麻烦了, 需要拦截属性的set方法, 这是以前没有怎么用过的AOP领域.

一些调查之后, 觉得有两种方式比较适合.

一是静态织入, 就是用类似代码模板的方式, 先写实体类型模板, 然后生成真正的实体类型类库, 在生成的时候进行修改在属性的set方法中注入PropertyChangedNotify事件, 服务中使用的是生成后的实体类库. PostSharp貌似不错, 但是不想引入更多的开销就罢了. 用T4或者上Emit或许不错.

二是动态, 大致上就是生成一个代理类型, 拥有原类型的所有接口, 但是重新给实现了一遍. 可以是组合的方式, 将原类型实例作为私有成员保存, Emit生成所有原类型的开放方法; 也可以是继承的方式, 重载. 考虑到对原代码的修改量, 我觉得继承重载不错. 

比如原实体类型如下:

public class Person
{
    public string Id{get;set;}
    public string Name{get;set;}
}

对这个类实现修改保存的托管, 那么直接使用这个类型是不行的了(除非方案1静态修改注入), 只有使用动态生成的代理实体类, 而如果使用组合方式的代理类型, 在OO的情况下很别扭, 所以我选择用继承重载的方式. 这就需要对这个实体类型进行改造, 将所有需要监视的属性修改成virutal声明 ctrl+H搞定. 

然后, 因为是增删改, 所以Key是必不可少的, 那么实体需要用attribute标记出Key属性, 支持多属性组合Key. 因为原代码用过一段时间的entityframework, 所以这个改造非常顺利. 

最后的类型定义如下:

[DataContract]
public class Person
{
    [Key]  
    public string Id{get;set;}
    public virtual string Name{get;set;}
}

首先用attribute标记Person为需要代理的类型, 便于在实际使用中纠错和预加载, 然后标记Id属性为Key, 并且不需要重载, 其他属性声明为virtual表示需要重载进行监视.

然后手写一个代理类型:

public class PersonProxy : Person
{
    public override string Name
    {
        set
        {
            base.Name= value;
            Interceptor.Invoke(this, "Name", value);
        }
    }  
}

public static class Interceptor
{
    public static void Invoke(object instance, string name, object value)
    {
        //log
    }
}

再定义个Factory的静态类, 实现静态泛型函数Create<T>()来创建Person的代理类型PersonProxy. 其实后面调用的都是代理类型, 只是使用方法和Person一致, 兼容旧代码.

只是这样的话, 还不如使用静态织入了. 所以上面手写的这个PersonProxy需要动态生成, 这样通过泛型来支持所有自定义class, 减少频繁修改. 于是Emit登场了.

不过我对IL和Emit认识浅薄, 我的办法是使用VS自带工具 -- ildasm, 用它打开刚才生成的PersonProxy查看IL代码, 反复对比总结之后终于将这个PersonProxy类型的动态生成用Emit实现成功并修改成泛型通用. 这套特殊的IL学习方法论真是屡试不爽. 方法大致如此抛砖引玉, 所以这里也就不贴emit的代码了. Invoke织入也可以做各种进化, 以达成更高级的功能, 比如invoke在赋值前, 写入失败回退throw exception等等...

这样ProxyFactory.Create<T>()函数就写好了.

在Invoke里生成需要SQL或是redis set方法就可以了. 

最后再实现增删代理的集合类型, 在Add的时候直接将插入的对象通过ProxyFactory.Create<T>()转成proxy子类对象, 为了避免Add之后继续使用传入的原对象而丢失对属性set的监控, 修改Add的声明为Add(ref T obj)强行修改引用为代理对象. 从入口处根绝, 完美. 

理论Ok, 于是封装成类库. 

最后放上代码地址 : https://github.com/Roytin/SmartDb

上一篇:2021/1/16


下一篇:closest