开发者社区> 行者武松> 正文

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
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
ML之catboost:catboost模型中常用的Pool类型数据结构源代码解读、案例应用之详细攻略(一)
ML之catboost:catboost模型中常用的Pool类型数据结构源代码解读、案例应用之详细攻略
80 0
[软考考点解析]软件设计师--主存与Cache地址映射方式
1. 题目 主存与Cache的地址映射方式中,____方式可以实现任意主存的任意一块装入Cache中任意位置,只有装满才需要替换。 A 全相联 B 直接映射 C 组相联 D 串并联
24 0
iOS网络编程之六——数据缓存类NSURLCache使用解析
iOS网络编程之六——数据缓存类NSURLCache使用解析
42 0
8、web爬虫讲解2—urllib库爬虫—ip代理—用户代理和ip代理结合应用
使用IP代理 ProxyHandler()格式化IP,第一个参数,请求目标可能是http或者https,对应设置build_opener()初始化IPinstall_opener()将代...
1701 0
《区块链DAPP开发入门、代码实现、场景应用》笔记4——Ethereum Wallet中部署合约
账号创建完成之后,账号余额是0,但是部署合约是需要消耗GAS的,因此需要获取一定的以太币才能够继续本次实现。
1566 0
阿里云云效功能升级,支持快速创建多个应用的独立测试环境
作为测试人员来说,搭建测试环境是测试实施的一个重要阶段,测试环境适合与否会严重影响测试结果的真实性和正确性。为了更好地服务用户,近日,阿里云研发协同平台-云效宣布功能升级,支持快速创建多个应用的独立测试环境,对于企业开发人员和测试人员来说,又是一大福音。
4594 0
Visual Studio 2017 - Windows应用程序打包成exe文件(2)- Advanced Installer
原文:Visual Studio 2017 - Windows应用程序打包成exe文件(2)- Advanced Installer Advanced Installer :Free for 30 days.
1604 0
iOS Jailbreak Principles - Sock Port 漏洞解析(一)UAF 与 Heap Spraying
本文同步发表在 [掘金社区](https://juejin.im/post/5dd10660e51d453fac0a598d) 和 [微信公众号](https://mp.weixin.qq.com/s?__biz=MzU2NjU5NzMwNA==&mid=2247483739&idx=1&sn=2a60b11800c80ca32e8f6ddea6284ae7&chksm=fcab428ccbdcc
379 0
如何使用 GeoTrellis 和 React 构建地理处理应用程序
这篇博文是使用 GeoTrellis、Akka HTTP和 React 创建GIS处理的网页应用程序的指南。翻译自凯利·英尼斯 (Kelly Innes)的博客。
181 0
+关注
行者武松
杀人者,打虎武松也。
17142
文章
2569
问答
文章排行榜
最热
最新
相关电子书
更多
OceanBase 入门到实战教程
立即下载
阿里云图数据库GDB,加速开启“图智”未来.ppt
立即下载
实时数仓Hologres技术实战一本通2.0版(下)
立即下载