欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

【我们一起写框架】C#的AOP框架

程序员文章站 2022-05-12 13:00:20
前言 AOP,大家都是听过的,它是一种面向切面的设计模式。 不过AOP虽然是被称为设计模式,但我们应该很少能看到AOP设计的框架。为什么呢? 因为,AOP单独设计的框架几乎是无法使用的。普遍的情况是,AOP要是和其他设计模式结合在一起使用。 所以,AOP虽然是设计模式,但我认为它更接近一种设计元素, ......

前言

aop,大家都是听过的,它是一种面向切面的设计模式。

不过aop虽然是被称为设计模式,但我们应该很少能看到aop设计的框架。为什么呢?

因为,aop单独设计的框架几乎是无法使用的。普遍的情况是,aop要是和其他设计模式结合在一起使用。

所以,aop虽然是设计模式,但我认为它更接近一种设计元素,是我们在设计框架的作料。

其实aop的原理就是将公共的部分提取出来,这件事,即便不考虑设计模式,每个开发人员在工作时也是会做的。也就是说,在aop设计模式被提出来之前,我们就在应用aop的设计了。

那么,为什么还要单独将aop拿出来说事呢?

我认为,主要目的应该是要强化切面的重要性。因为设计框架时加入aop的理念,确实会让框架更加立体。

aop的应用

aop既然是一种作料,那么它的应用就是多种多样的;它可以出现在任何场合的。

下面我们举出一个例子,来说明aop的应用。

----------------------------------------------------------------------------------------------------

我们在开发的时候,通常会有这样的需求。

[将函数的入参和返回值记录到日志中][入参中为负数抛出异常]

当我们面对这样的需求时,通常会将入参和返回值全部传到一个独立的操作函数中,对其进行相应的操作。

这样实现,就是aop的理念;不过开发者处理时,稍微繁琐了一点,因为每个函数都要处理。

为了减少这种重复操作,让我们一起来编写函数的切面aop吧。

aop框架的实现

首先,我们一起看下aop框架应用后的效果。

在下面代码中,可以看到,我们定义了一个aoptest类,然后调用了他的test方法,之后传入了一个正数和一个负数,如果函数抛出异常,我们将输出异常的消息。

class program
{
    static void main(string[] args)
    {
        aoptest test = new aoptest();
        try
        { 
            test.test(518);
            test.test(-100);
        }
        catch(exception ex)
        {
            console.writeline(ex.message);
        }
        console.readline();
    }
}

接下来我们看下aoptest类的定义。

[kiba]
public class aoptest : contextboundobject
{ 
    
    public string test(int para)
    {
        console.writeline(para);
        return "数字为:" + para;
    }
}

代码如上所示,很简单,就是输出了入参,不过有两个地方需要注意,该类继承了contextboundobject类,并且拥有一个kiba的特性。

然后,我们看下运行结果。

【我们一起写框架】C#的AOP框架

从运行结果中我们看到,第一个函数正常输出,但第二个函数抛出了异常,而且异常的message是异常两个汉字。

这就是我们aop实行的效果了,我们的aop框架对函数入参进行了判断,如果是正数,就正常运行,如果为负数就抛出异常。

下面我们一起来看看aop框架是如何实现这样的效果的。

首先我们一起来看下kiba这个特性。

[attributeusage(attributetargets.class)]
public class kibaattribute : contextattribute
{
    public kibaattribute()
        : base("kiba")
    {
    } 
    public override void getpropertiesfornewcontext(iconstructioncallmessage ctormsg)
    {
        ctormsg.contextproperties.add(new kibacontextproperty()); 
    }
}

代码如上所示,很简单很基础的一个特性,不过它继承了contextattribute类,并重写了其下的方法getpropertiesfornewcontext。

这个方法是干什么的呢?

我们可以从函数名的直译来理解它是干什么的,getpropertiesfornewcontext直译过来就是创建新对象时获取他的属性。然后我们看到,我们重新了该方法后又为他添加了一个新的属性。

而我们添加的这个新的属性将截获拥有该特性的类的函数。

【ps:该描述并不是contextattribute真实的运行逻辑,不过,初学时,我们可以先这样理解,当我们更深入的理解了函数的运行机制后,自然就明白该类的意义。】

 下面我们看下kibacontextproperty类。

public class kibacontextproperty : icontextproperty, icontributeobjectsink
{
    public kibacontextproperty()
    {
    } 
    public imessagesink getobjectsink(marshalbyrefobject obj, imessagesink next)
    {
        return new kibamessagesink(next);
    } 
    public bool isnewcontextok(context newctx)
    {
        return true;
    }  
    public void freeze(context newctx)
    {
    }  
    public string name
    {
        get { return "kiba"; }
    }
}

代码如上所示,依然很简单,只是继承并实现了icontextproperty和icontributeobjectsink两个接口。

其中我们重点看下getobjectsink方法,该方法用于截获函数。

我们可以看到该方法的两个参数,但我们只用到了一个imessagesink ,并且,该方法的返回值也是imessagesink。

所以,我们可以想到,该方法的本来面目是这样的。

public imessagesink getobjectsink(marshalbyrefobject obj, imessagesink next)
{
    return next; 
} 

也就是说,imessagesink 封装了函数的一切内容,那么我们的aop实现的地方也就找到了。

于是我们用kibamessagesink类处理一下imessagesink 。

kibamessagesink代码如下:

public class kibamessagesink : imessagesink
{
    private kaspec kaspec = new kaspec(); 
    private imessagesink nextsink; 
    public kibamessagesink(imessagesink next)
    {
        nextsink = next;
    } 
    public imessagesink nextsink
    {
        get
        {
            return nextsink;
        }
    } 
    public imessage syncprocessmessage(imessage msg)
    { 
        imethodcallmessage call = msg as imethodcallmessage; 
        if (call != null)
        {
            //拦截消息,做前处理
            kaspec.preexcute(call.methodname, call.inargs);
        }
        for (int i = 0; i < call.inargs.count(); i++)
        {
            var para = call.inargs[i];
            var type = para.gettype();
            string typename = type.tostring().replace("system.nullable`1[", "").replace("]", "").replace("system.", "").tolower();
            if (typename == "int32")
            {
                int inparame = convert.toint16(call.inargs[i]);
                if (inparame < 0)
                {
                    throw new exception("异常");
                }
            } 
        }
        //传递消息给下一个接收器 
        imessage retmsg = nextsink.syncprocessmessage(call as imessage);   
        imethodreturnmessage dispose = retmsg as imethodreturnmessage;
        if (dispose != null)
        { 
            //调用返回时进行拦截,并进行后处理
            kaspec.endexcute(dispose.methodname, dispose.outargs, dispose.returnvalue, dispose.exception);
        } 
        return retmsg;
    } 
    public imessagectrl asyncprocessmessage(imessage msg, imessagesink replysink)
    {
        return null;
    } 
}

我们重点看下syncprocessmessage方法。

可以看到,我们在方法调用先调用了kaspec类的preexcute方法,该方法用于把入参输出到日志中。

接下来,我们对入参进行了判断,如果入参是负数,我们将不执行函数,直接抛出异常。

然后我们调用kaspec类的endexcute方法,将返回值输出到日志中。

再然后,我们才返回imessage,让函数完结。

下面我们一起看下kaspec类的实现。

/// <summary>
/// 切面
/// </summary>
public class kaspec
{ 
    #region 处理
    /// <summary>
    /// 前处理
    /// </summary> 
    public void preexcute(string methodname, object[] inparams)
    {

        logger.info("==================== " + methodname + ":" + " start====================");
        logger.info(string.format("参数数量:{0}", inparams.count()));

        for (int i = 0; i < inparams.count(); i++)
        {
            logger.info(string.format("参数序号[{0}] ============    参数类型:{1}    执行类:{1}", i + 1, inparams[i])); 
            logger.info("传入参数:");
            string paramxmlstr = xmlserializertostring(inparams[i], encoding.utf8);
            logger.info(paramxmlstr);
        }
    } 
    /// <summary>
    /// 后处理
    /// </summary> 
    public void endexcute(string methodname, object[] outparams, object returnvalue, exception ex)
    {
        type mytype = returnvalue.gettype();
        logger.info(string.format("返回值类型:{0}", mytype.name));
        logger.info("返回值:");
        if (mytype.name != "void")
        {
            string resxmlstr = datacontractserializertostring(returnvalue, encoding.utf8);
            logger.info(resxmlstr);
        }
       
        if (outparams.count() > 0)//out 返回参数
        {
            logger.info(string.format("out返回参数数量:{0}", outparams.count())); 
            for (int i = 0; i < outparams.count(); i++)
            {
                logger.info(string.format("参数序号[{0}] == 参数值:{1}", i + 1, outparams[i]));
            }
        }

        if (ex != null)
        {
            logger.error(ex);
        }
        logger.info("==================== " + methodname + ":" + " end====================");
    }
}

代码如上所示,就是简单的日志输出。

到此,我们的aop框架就编写完成了;其上的代码编写都是为kaspec服务,因为kaspec才是切面。

也就是说,只要将特性kiba赋予给类,那么该类的函数,就被拦截监听,然后我们就可以kaspec切面中,做我们想做的操作了。

最后,我们再回头看下aoptest类。

[kiba]
public class aoptest : contextboundobject

可以看到,该类不止拥有kiba特性,还继承了contextboundobject类,该类是干什么的呢?

contextboundobject类是内容边界对象,只有继承了contextboundobject类的类,其类中才会驻留的context上下文,并且会被contextattribute特性拦截监听。

呃,其实,这样解释还是有点不太正确,不过我也没找到更好的说明方式,如果你还理解不了,也可以去msdn查询下,当然,msdn的解释是反人类的,需要做好心理准备。

----------------------------------------------------------------------------------------------------

框架代码已经传到github上了,欢迎大家下载。

github地址:https://github.com/kiba518/kaop

----------------------------------------------------------------------------------------------------

注:此文章为原创,欢迎转载,请在文章页面明显位置给出此文链接!
若您觉得这篇文章还不错,请点击下右下角的推荐】,非常感谢!