在.Net中关于AOP的实现(补遗)

 

在《在.Net中关于AOP的实现》我通过动态代理的技术,基本上实现了AOP的几个技术要素,包括aspect,advice,pointcut。在文末我提到采用配置文件方式,来获取advice和pointcut之间的映射,从而使得构建aspect具有扩展性。

 

细细思考这个问题,我发现使用delegate来构建advice,似乎并非一个明智的选择。我在建立映射关系时,是将要拦截的方法名和拦截需要实现的aspect逻辑建立一个对应关系,而该aspect逻辑确实可以通过delegate,使其指向一族方法签名与该委托完全匹配的方法。这使得advice能够抽象化,以便于具体实现的扩展。然而,委托其实现毕竟是面向过程的范畴,虽然在.Net下,delegate本身仍是一个类对象,然而在创建具体的委托实例时,仍然很难通过配置文件和反射技术来获得。

 

考虑到委托具有的接口抽象的本质,也许采用接口的方式来取代委托更为可行。在之前的实现方案中,我为advice定义了两个委托:

public delegate void BeforeAOPHandle(IMethodCallMessage callMsg);

public delegate void AfterAOPHandle(IMethodReturnMessage replyMsg);

 

我可以定义两个接口IBeforeAction和IAfterAction,分别与这两个委托相对应:

    public interface IBeforeAdvice

    {

        void BeforeAdvice(IMethodCallMessage callMsg);

}

    public interface IAfterAdvice

    {

        void AfterAdvice(IMethodReturnMessage returnMsg);

}

通过定义的接口,可以将Advice与Aspect分离开来,这也完全符合OO思想中的“责任分离”原则。

(注:为什么要为Advice定义两个接口?这是考虑到有些Aspect只需要提供Before或After两个逻辑之一,如权限控制,就只需要before Action。)

 

那么当类库使用者,要定义自己的Aspect时,就可以定义具体的Advice类,来实现这两个接口,以及具体的Advice逻辑了。例如,之前提到的日志Aspect:

    public class LogAdvice:IAfterAdvice,IBeforeAdvice

    {

        #region IBeforeAdvice Members

 

        public void BeforeAdvice(IMethodCallMessage callMsg)

        {

            if (callMsg == null)

            {

                return;

            }

            Console.WriteLine("{0}({1},{2})", callMsg.MethodName, callMsg.GetArg(0), callMsg.GetArg(1));

        }

 

        #endregion

 

        #region IAfterAdvice Members

 

        public void AfterAdvice(IMethodReturnMessage returnMsg)

        {

            if (returnMsg == null)

            {

                return;

            }

            Console.WriteLine("Result is {0}", returnMsg.ReturnValue);

        }

 

        #endregion

}

 

而在AOPSink类的派生类中,添加方法名与Advice映射关系(此映射关系,我们即可理解为AOP的pointcut)时,就可以添加实现了Advice接口的类对象,如:

         public override void AddAllBeforeAdvices()

         {

            AddBeforeAdvice("ADD",new LogAdvice());

            AddBeforeAdvice("SUBSTRACT", new LogAdvice());

         }

         public override void AddAllAfterAdvices()

         {

             AddAfterAdvice("ADD",new LogAdvice());

            AddAfterAdvice("SUBSTRACT", new LogAdvice());

         }

由于LogAdvice类实现了接口IBeforeAdvice和IAfterAdvice,因此诸如new LogAdvice的操作均可以通过反射来创建该实例,如:

IBeforeAdvice beforeAdvice =

(IBeforeAdvice)Activator.CreateInstance("Wayfarer.AOPSample","Wayfarer.AOPSample.LogAdvice").Unwrap();

而CreateInstance()方法的参数值,是完全可以通过配置文件来配置的:

<aop>

    <aspect value ="LOG">

         <advice type="before" assembly="Wayfarer.AOPSample" class="Wayfarer.AOPSample.LogAdvice">

             <pointcut>ADDpointcut>

             <pointcut>SUBSTRACTpointcut>

         advice>

         <advice type="after" assembly="Wayfarer.AOPSample" class="Wayfarer.AOPSample.LogAdvice">

             <pointcut>ADDpointcut>

             <pointcut>SUBSTRACTpointcut>

         advice>

    aspect>   

aop>

这无疑改善了AOP实现的扩展性。

 

《在.Net中关于AOP的实现》实现AOP的方案,要求包含被拦截方法的类必须继承ContextBoundObject。这是一个比较大的限制。不仅如此,ContextBoundObject对程序的性能也有极大的影响。我们可以做一个小测试。定义两个类,其中一个类继承ContextBoundObject。它们都实现了一个累加的操作:

class NormalObject

    {

        public void Sum(int n)

        {

            int sum = 0;

            for (int i = 1; i <= n; i++)

            {

                sum += i;

            }

            Console.WriteLine("The result is {0}",sum);

            Thread.Sleep(10);

        }

    }

 

    class MarshalObject:ContextBoundObject

    {

        public void Sum(int n)

        {

            int sum = 0;

            for (int i = 1; i <= n; i++)

            {

                sum += i;

            }

            Console.WriteLine("The result is {0}", sum);

            Thread.Sleep(10);

        }

    }

然后执行这两个类的Sum()方法,测试其性能:

    class Program

    {

        static void Main(string[] args)

        {

            long normalObjMs, marshalObjMs;

            Stopwatch watch = new Stopwatch();

            NormalObject no = new NormalObject();

            MarshalObject mo = new MarshalObject();

 

            watch.Start();

            no.Sum(1000000);

            watch.Stop();

            normalObjMs = watch.ElapsedMilliseconds;

            watch.Reset();

 

            watch.Start();

            mo.Sum(1000000);

            watch.Stop();

            marshalObjMs = watch.ElapsedMilliseconds;

            watch.Reset();

 

            Console.WriteLine("The normal object consume {0} milliseconds.",normalObjMs);

            Console.WriteLine("The contextbound object consume {0} milliseconds.",marshalObjMs);           

            Console.ReadLine();

        }

    }

得到的结果如下:

aopresult1.GIF
 

从性能的差异看,两者之间的差距是比较大的。如果将其应用在企业级的复杂逻辑上,这种区别就非常明显了,对系统带来的影响也是非常巨大的。

 

另外,在《在.Net中关于AOP的实现》文章后,有朋友发表了很多中肯的意见。其中有人提到了AOPAttribute继承ContextAttribute的问题。评论中提及微软在以后的版本中,不再提供ContextAttribute。如果真是如此,确有必要放弃继承ContextAttribute的形式。不过,在.Net中,除了ContextAttribute之外,还提供有一个接口IContextAttribute,该接口的定义为:

public interface IContextAttribute

{

        void GetPropertiesForNewContext(IConstructionCallMessage msg);

        bool IsContextOK(Context ctx, IConstructionCallMessage msg);       

}

此时只需要将原来的AOPAttribute实现该接口即可:

    public abstract class AOPAttribute:Attribute,IContextAttribute//ContextAttribute

    {

        #region IContextAttribute Members

        public void GetPropertiesForNewContext(IConstructionCallMessage ctorMsg)

        {

            AOPProperty property = GetAOPProperty();

            property.AspectXml = m_AspectXml;

            property.AspectXmlFlag = m_AspectXmlFlag;

            ctorMsg.ContextProperties.Add(property);

        }

 

        public bool IsContextOK(Context ctx, IConstructionCallMessage ctorMsg)

        {

            return false;

        }

        #endregion

}

不知道,IContextAttribute似乎也会在未来的版本中被取消呢?

 

然而,从总体来看,这种使用ContextBoundObject的方式是不太理想的,也许它只能停留在实验室阶段,或许期待微软在未来的版本中得到更好的解决!?

 

当然,如果采用Castle的DynamicProxy技术,可以突破必须继承CotextBoundObject的局限,但随着而来的局限却是AOP拦截的方法,要求必须是virtual的。坦白说,这样的限制,不过与前者乃“五十步笑百步”的区别而已。我还是期待有更好的解决方案。

 

说到AOP的几大要素,在这里可以补充说说,它主要包括:

1、Cross-cutting concern

 

  在OO模型中,虽然大部份的类只有单一的、特定的功能,但它们通常会与其他类有着共同的第二需求。例如,当线程进入或离开某个方法时,我们可能既要在数据访问层的类中记录日志,又要在UI层的类中记录日志。虽然每个类的基本功能极然不同,但用来满足第二需求的代码却基本相同。

 

2、Advice

 

  它是指想要应用到现有模型的附加代码。例如在《在.Net中关于AOP的实现》的例子中,是指关于打印日志的逻辑代码。

 

3、Point-cut

 

  这个术语是指应用程序中的一个执行点,在这个执行点上需要采用前面的cross-cutting concern。如例子中,执行Add()方法时出现一个Point-cut,当方法执行完毕,离开方法时又出现另一个Point-cut。

 

4、Aspect

 

Point-cut和advice结合在一起就叫做aspect。如例子中的Log和Monitor。在对本例的重构中,我已经AOPSink更名为Aspect,相应的LogAOPSink、MonitorAOPSink也更名为LogAspect,MonitorAspect。

 

以上提到的PointCut和Advice在AOP技术中,通常称为动态横切技术。与之相对应的,是较少被提及的静态横切。它与动态横切的区别在于它并不修改一个给定对象的执行行为,相反,它允许通过引入附加的方法属性和字段来修改对象固有的结构。在很多AOP实现中,将静态横切称为introduce或者mixin。

 

在开发应用系统时,如果需要在不修改原有代码的前提下,引入第三方产品和API库,静态横切技术是有很大的用武之地的。从这一点来看,它有点类似于设计模式中提到的Adapter模式需要达到的目标。不过,看起来静态横切技术应比Adapter模式更加灵活和功能强大。

 

例如,一个已经实现了收发邮件的类Mail。然而它并没有实现地址验证的功能。现在第三方提供了验证功能的接口IValidatable:

public interface IValidatable

{

    bool ValidateAddress();

}

如果没有AOP,采用设计模式的方式,在不改变Mail类的前提下,可以通过Adapter模式,引入MailAdater,继承Mail类,同时实现IValidatable接口。采用introduce技术,却更容易实现该功能的扩展,我们只需要定义aspect:(注:java代码,使用了AspectJ)

import com.acme.validate.Validatable;

 

public aspect EmailValidateAspect

{

   declare parents: Email implements IValidatable;

 

   public boolean Email.validateAddress(){

     if(this.getToAddress() != null){

          return true;

     }else{

          return false;

     }

   }

}

 

从上可以看到,通过EmailValidateAspect方面,为Email类introduce了新的方法ValidateAddress()。非常容易的就完成了Email的扩展。

 

我们可以比较一下,如果采用Adapter模式,原有的Email类是不能被显示转换为IValidatable接口的,也即是说如下的代码是不可行的:

Email mail = new Email();

IValidatable validate = ((IValidatable)mail).ValidateAddress();

要调用ValidateAddress()方法,必须通过EmailAdapter类。然而通过静态横切技术,上面的代码就完全可行了。

 

静态横切的技术在企业应用上还需要进一步验证和测试,不过遗憾的是,《在.Net中关于AOP的实现》一文采用的动态代理技术,是无法完成实现静态横切的目标的。

posted @ 2005-09-09 13:09  张逸  阅读(4345)  评论(12编辑  收藏  举报