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。
一、問題重現
這個問題還還重現,為了比較我們先來看看正常 情況下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:\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
出處:http://artech.cnblogs.com
本文配套源碼