Enterprise Library深入解析与灵活应用(9):个人觉得比较严重的关于CachingCallHandler的Bug

时间:2022-04-22
本文章向大家介绍Enterprise Library深入解析与灵活应用(9):个人觉得比较严重的关于CachingCallHandler的Bug,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

微软EnterLib的Policy Injection Application Block(PIAB)是一个比较好用的轻量级的AOP框架,你可以通过创建自定义的CallHandler实现某些CrossCutting的逻辑,并以自定义特性或者配置的方式应用到目标方法上面。PIAB自身也提供了一系列的CallHandler,其中CachingCallHandler直接利用HttpRuntime的Cache实现了基于方法级别的缓存。但是,PIAB发布到现在,CachingCallHandler就一直存着一个问题:如果目标方法具有Out参数并且返回类型不是void,会抛出IndexOutOfRangeException,如果返回类型为void,out参数也不会被缓存。不知道微软对此作何考虑,反正我觉得这是一个不可原谅的Bug。(Source Code从这里下载)

一、问题重现

这个问题还还重现,为了比较我们先来看看正常情况下CachingCallHandler的表现。下面我定义了一个简单的接口:IMembershipService, 包含一个方法GetUserName根据传入的User ID返回User Name。MembershipService实现了该接口,为了方便大家确定方法执行的结果是否被缓存,我让每次执行都返回一个GUID。CachingCallHandler直接以自定义特性的方式应用到GetUserName方法上。

   1: using System;
   2: using System.Threading;
   3: using Microsoft.Practices.EnterpriseLibrary.PolicyInjection;
   4: namespace CachingCallHandler4OutParam
   5: {
   6:     public interface IMembershipService
   7:     {
   8:         string GetUserName(string userId);
   9:     }
  10:  
  11:     public class MembershipService : IMembershipService
  12:     {        
  13:         [CachingCallHandler]
  14:         public string GetUserName(string userId)
  15:         {
  16:             return Guid.NewGuid().ToString();
  17:         }
  18:     }
  19: }

现在,在Main方法中,编写如下的代码:通过PolicyInjection的Create<TType, TInterface>创建能够被PIAB截获的Proxy对象,并在一个无限循环中传入相同的参数调用GetUserName方法。从输出结果我们看到,返回的UserName都是相同的,从而证明了第一次执行的结果被成功缓存。

   1: using System;
   2: using System.Threading;
   3: using Microsoft.Practices.EnterpriseLibrary.PolicyInjection;
   4: namespace CachingCallHandler4OutParam
   5: {
   6:     class Program
   7:     {
   8:         static void Main(string[] args)
   9:         {
  10:             IMembershipService svc = PolicyInjection.Create<MembershipService, IMembershipService>();
  11:             while(true)
  12:             {                
  13:                 Console.WriteLine(svc.GetUserName("007"));
  14:                 Thread.Sleep(1000);
  15:             }
  16:         }
  17:     }    
  18: }

输出结果:


E1E8EA0F-7620-4879-BA5D-33356568336E
E1E8EA0F-7620-4879-BA5D-33356568336E
E1E8EA0F-7620-4879-BA5D-33356568336E
E1E8EA0F-7620-4879-BA5D-33356568336E
E1E8EA0F-7620-4879-BA5D-33356568336E
E1E8EA0F-7620-4879-BA5D-33356568336E

现在我们修改我们的程序:将GetUserName改成TryGetUserName,将UserName以输出参数的形式反悔,Bool类型的返回值表示UserId是否存在,相信大家都会认为这是一个很常见的API定义方式。

using System;
using System.Threading;
using Microsoft.Practices.EnterpriseLibrary.PolicyInjection;
using Microsoft.Practices.EnterpriseLibrary.PolicyInjection.CallHandlers;
namespace CachingCallHandler4OutParam
{
    class Program
    {
        static void Main(string[] args)
        {
            IMembershipService svc = PolicyInjection.Create<MembershipService, IMembershipService>();
            string userName;
            while (true)
            {
                svc.TryGetUserName("007", out userName);
                Console.WriteLine(userName);
                Thread.Sleep(1000);
            }
        }
    }

    public interface IMembershipService
    {
        bool TryGetUserName(string userId, out string userName);
    }

    public class MembershipService : IMembershipService
    {
        [CachingCallHandler]
        public bool TryGetUserName(string userId, out string userName)
        {
            userName = Guid.NewGuid().ToString();
            return true;
        }       
    }
}

运行上面一段程序之后,会抛出如下一个IndexOutOfRangeException,从StatckTrace我们可以知道,该异常实际上是在将方法调用返回消息转换成相应的输出参数是出错导致的:

Stack Trace:

at System.Runtime.Remoting.Proxies.RealProxy.PropagateOutParameters(IMessage msg, Object[] outArgs, Object returnValue)
at System.Runtime.Remoting.Proxies.RealProxy.HandleReturnMessage(IMessage reqMsg, IMessage retMsg)
at System.Runtime.Remoting.Proxies.RealProxy.PrivateInvoke(MessageData& msgData, Int32 type)
at CachingCallHandler4OutParam.IMembershipService.TryGetUserName(String userId, String& userName)
at CachingCallHandler4OutParam.Program.Main(String[] args) in e:EnterLibCachingCallHandler4OutParamCachingCallHandler4OutParamProgram.cs:line 15
at System.AppDomain._nExecuteAssembly(Assembly assembly, String[] args)
at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
at System.Threading.ThreadHelper.ThreadStart()

二、是什么导致异常的抛出?

我们现在通过CachingCallHandler的Invoke方法的实现,可以看出一些问题:该CallHander仅仅会缓存方法的返回值(this.AddToCache(key, return2.ReturnValue);),而不是缓存输出参数;由于仅仅只有返回值被缓存,所以最终创建的IMethodReturn不包含输出参数,从而导致返回的消息与参数列表不一致,导致异常的发生。

   1: public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
   2: {
   3:     if (this.TargetMethodReturnsVoid(input))
   4:     {
   5:         return getNext()(input, getNext);
   6:     }
   7:     object[] inputs = new object[input.Inputs.Count];
   8:     for (int i = 0; i < inputs.Length; i++)
   9:     {
  10:         inputs[i] = input.Inputs[i];
  11:     }
  12:     string key = this.keyGenerator.CreateCacheKey(input.MethodBase, inputs);
  13:     object[] objArray2 = (object[])HttpRuntime.Cache.Get(key);
  14:     if (objArray2 == null)
  15:     {
  16:         IMethodReturn return2 = getNext()(input, getNext);
  17:         if (return2.Exception == null)
  18:         {
  19:             this.AddToCache(key, return2.ReturnValue);
  20:         }
  21:         return return2;
  22:     }
  23:     return input.CreateMethodReturn(objArray2[0], new object[] { input.Arguments });
  24: }

三、问题如何解决?

现在我们来Fix这个Bug,让它支持输出参数并对输出参数和返回值一并缓存。为此,我首先创建了如下一个OutputParamter类表示输出参数,属性Value和Index分别表示参数值和在方法参数列表中的位置:

   1: public class OutputParameter
   2: {
   3:     public object Value
   4:     { get; private set; }
   5:  
   6:     public int Index
   7:     { get; private set; }
   8:  
   9:     public OutputParameter(object value, int index)
  10:     {
  11:         this.Value = value;
  12:         this.Index = index;
  13:     }
  14: }

然后将需要进行缓存的方法返回值和输出参数封装在一个单独的类中,我将它起名为InvocationResult. 两个属性ReturnValue和Outputs分别表示返回值和输出参数。StreamlineArguments方法结合传入的所以参数列表返回一个方法参数值的数组,该数组的元素顺序需要与方法的参数列表相匹配。

   1: public class InvocationResult
   2: {
   3:     public object ReturnValue
   4:     { get; private set; }
   5:  
   6:     public OutputParameter[] Outputs
   7:     { get; set; }
   8:  
   9:     public InvocationResult(object returnValue, OutputParameter[] outputs)
  10:     {
  11:         Guard.ArgumentNotNull(returnValue, "returnValue");
  12:         this.ReturnValue = returnValue;
  13:         if (null == outputs)
  14:         {
  15:             this.Outputs = new OutputParameter[0];
  16:         }
  17:         else
  18:         {
  19:             this.Outputs = outputs;
  20:         }
  21:     }
  22:  
  23:     public bool TryGetParameterValue(int index, out object parameterValue)
  24:     {
  25:         parameterValue = null;
  26:         var result = this.Outputs.Where(param => param.Index == index);
  27:         if (result.Count() > 0)
  28:         {
  29:             parameterValue = result.ToArray()[0].Value;
  30:             return true;
  31:         }
  32:         return false;
  33:     }
  34:  
  35:     public object[] StreamlineArguments(IParameterCollection arguments)
  36:     {
  37:         var list = new List<object>();
  38:         object paramValue;
  39:         for (int i = 0; i < arguments.Count; i++)
  40:         {
  41:             if (this.TryGetParameterValue(i, out paramValue))
  42:             {
  43:                 list.Add(paramValue);
  44:             }
  45:             else
  46:             {
  47:                 list.Add(arguments[i]);
  48:             }
  49:         }
  50:  
  51:         return list.ToArray();
  52:     }
  53: }

然后在现有CachingCallHandler的基础上,添加如下两个辅助方法:AddToCache和GetInviocationResult,分别用于将InvocationResult对象加入缓存,以及根据IMethodInvocation和IMethodReturn对象创建InvocationResult对象。最后将类名改成FixedCachingCallHandler以示区别。

   1: public class FixedCachingCallHandler : ICallHandler
   2: {
   3:     //其他成员
   4:     private void AddToCache(string key, InvocationResult result)
   5:     {
   6:         HttpRuntime.Cache.Insert(key, result, null, Cache.NoAbsoluteExpiration, this.expirationTime, CacheItemPriority.Normal, null);
   7:     }
   8:  
   9:     
  10:     private InvocationResult GetInvocationResult(IMethodInvocation input, IMethodReturn methodReturn)
  11:     {
  12:         var outParms = new List<OutputParameter>();
  13:  
  14:         for (int i = 0; i < input.Arguments.Count; i++)
  15:         {
  16:             ParameterInfo paramInfo = input.Arguments.GetParameterInfo(i);
  17:             if (paramInfo.IsOut)
  18:             {
  19:                 OutputParameter param = new OutputParameter(input.Arguments[i], i);
  20:                 outParms.Add(param);
  21:             }
  22:         }
  23:  
  24:         return new InvocationResult(methodReturn.ReturnValue, outParms.ToArray());
  25:     }
  26:     
  27: }

最后我们重写Invoke方法, 去处对返回类型void的过滤,并实现对基于InvocationResult对象的缓存和获取:

   1: public class FixedCachingCallHandler : ICallHandler
   2: {
   3:     //其他成员
   4:     public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
   5:     {
   6:         object[] inputs = new object[input.Inputs.Count];
   7:         for (int i = 0; i < inputs.Length; i++)
   8:         {
   9:             inputs[i] = input.Inputs[i];
  10:         }
  11:         string key = this.keyGenerator.CreateCacheKey(input.MethodBase, inputs);
  12:         InvocationResult result = (InvocationResult)HttpRuntime.Cache.Get(key);
  13:         if (result == null)
  14:         {
  15:             IMethodReturn return2 = getNext()(input, getNext);
  16:             if (return2.Exception == null)
  17:             {
  18:                 this.AddToCache(key, this.GetInvocationResult(input, return2));
  19:             }
  20:             return return2;
  21:         }
  22:         return input.CreateMethodReturn(result.ReturnValue, result.StreamlineArguments(input.Arguments));
  23:  
  24:         return returnValue;
  25:     }
  26:  
  27:     private InvocationResult GetInvocationResult(IMethodInvocation input, IMethodReturn methodReturn)
  28:     {
  29:         var outParms = new List<OutputParameter>();
  30:  
  31:         for (int i = 0; i < input.Arguments.Count; i++)
  32:         {
  33:             ParameterInfo paramInfo = input.Arguments.GetParameterInfo(i);
  34:             if (paramInfo.IsOut)
  35:             {
  36:                 OutputParameter param = new OutputParameter(input.Arguments[i], i);
  37:                 outParms.Add(param);
  38:             }
  39:         }
  40:  
  41:         return new InvocationResult(methodReturn.ReturnValue, outParms.ToArray());
  42:     }    
  43: }

应用新的CachingCallHandler,你将会得到正确的结果:

4DD83AE8-070B-49df-9781-6F4673C85189
4DD83AE8-070B-49df-9781-6F4673C85189
4DD83AE8-070B-49df-9781-6F4673C85189
4DD83AE8-070B-49df-9781-6F4673C85189
4DD83AE8-070B-49df-9781-6F4673C85189