閱讀908 返回首頁    go 阿裏雲 go 技術社區[雲棲]


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我們可以知道,該異常實際上是在將方法調用返回消息轉換成相應的輸出參數是出錯導致的:

imageStack 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:\EnterLib\CachingCallHandler4OutParam\CachingCallHandler4OutParam\Program.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

作者:蔣金楠
微信公眾賬號:大內老A
微博:www.weibo.com/artech
如果你想及時得到個人撰寫文章以及著作的消息推送,或者想看看個人推薦的技術資料,可以掃描左邊二維碼(或者長按識別二維碼)關注個人公眾號(原來公眾帳號蔣金楠的自媒體將會停用)。
本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁麵明顯位置給出原文連接,否則保留追究法律責任的權利。
原文鏈接

最後更新:2017-10-30 11:04:08

  上一篇:go  WCF技術剖析之二十五: 元數據(Metadata)架構體係全景展現[WS標準篇]
  下一篇:go  WCF技術剖析之二十五: 元數據(Metadata)架構體係全景展現[元數據描述篇]