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

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介:

微软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
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
2天前
|
编译器 PHP 开发者
PHP 8新特性解析与实战应用####
随着PHP 8的发布,这一经典编程语言迎来了诸多令人瞩目的新特性和性能优化。本文将深入探讨PHP 8中的几个关键新功能,包括命名参数、JIT编译器、新的字符串处理函数以及错误处理改进等。通过实际代码示例,展示如何在现有项目中有效利用这些新特性来提升代码的可读性、维护性和执行效率。无论你是PHP新手还是经验丰富的开发者,本文都将为你提供实用的技术洞察和最佳实践指导。 ####
13 1
|
8天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
13天前
RS-485网络中的标准端接与交流电端接应用解析
RS-485,作为一种广泛应用的差分信号传输标准,因其传输距离远、抗干扰能力强、支持多点通讯等优点,在工业自动化、智能建筑、交通运输等领域得到了广泛应用。在构建RS-485网络时,端接技术扮演着至关重要的角色,它直接影响到网络的信号完整性、稳定性和通信质量。
|
23天前
|
机器学习/深度学习 人工智能 自然语言处理
思通数科AI平台在尽职调查中的技术解析与应用
思通数科AI多模态能力平台结合OCR、NLP和深度学习技术,为IPO尽职调查、融资等重要交易环节提供智能化解决方案。平台自动识别、提取并分类海量文档,实现高效数据核验与合规性检查,显著提升审查速度和精准度,同时保障敏感信息管理和数据安全。
79 11
|
19天前
|
自然语言处理 并行计算 数据可视化
免费开源法律文档比对工具:技术解析与应用
这款免费开源的法律文档比对工具,利用先进的文本分析和自然语言处理技术,实现高效、精准的文档比对。核心功能包括文本差异检测、多格式支持、语义分析、批量处理及用户友好的可视化界面,广泛适用于法律行业的各类场景。
|
21天前
|
安全 编译器 PHP
PHP 8新特性解析与实践应用####
————探索PHP 8的创新功能及其在现代Web开发中的实际应用
|
23天前
|
机器学习/深度学习 人工智能 自然语言处理
医疗行业的语音识别技术解析:AI多模态能力平台的应用与架构
AI多模态能力平台通过语音识别技术,实现实时转录医患对话,自动生成结构化数据,提高医疗效率。平台具备强大的环境降噪、语音分离及自然语言处理能力,支持与医院系统无缝集成,广泛应用于门诊记录、多学科会诊和急诊场景,显著提升工作效率和数据准确性。
|
24天前
|
机器学习/深度学习 人工智能 安全
TPAMI:安全强化学习方法、理论与应用综述,慕工大、同济、伯克利等深度解析
【10月更文挑战第27天】强化学习(RL)在实际应用中展现出巨大潜力,但其安全性问题日益凸显。为此,安全强化学习(SRL)应运而生。近日,来自慕尼黑工业大学、同济大学和加州大学伯克利分校的研究人员在《IEEE模式分析与机器智能汇刊》上发表了一篇综述论文,系统介绍了SRL的方法、理论和应用。SRL主要面临安全性定义模糊、探索与利用平衡以及鲁棒性与可靠性等挑战。研究人员提出了基于约束、基于风险和基于监督学习等多种方法来应对这些挑战。
50 2
|
3天前
|
存储 供应链 算法
深入解析区块链技术的核心原理与应用前景
深入解析区块链技术的核心原理与应用前景
15 0
|
4天前
|
存储 监控 API
深入解析微服务架构及其在现代应用中的实践
深入解析微服务架构及其在现代应用中的实践
15 0

推荐镜像

更多
下一篇
无影云桌面