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

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

微软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
如果你想及时得到个人撰写文章以及著作的消息推送,或者想看看个人推荐的技术资料,可以扫描左边二维码(或者长按识别二维码)关注个人公众号(原来公众帐号 蒋金楠的自媒体将会停用)。
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。
相关文章
|
13天前
|
缓存 Kubernetes Docker
GitLab Runner 全面解析:Kubernetes 环境下的应用
GitLab Runner 是 GitLab CI/CD 的核心组件,负责执行由 `.gitlab-ci.yml` 定义的任务。它支持多种执行方式(如 Shell、Docker、Kubernetes),可在不同环境中运行作业。本文详细介绍了 GitLab Runner 的基本概念、功能特点及使用方法,重点探讨了流水线缓存(以 Python 项目为例)和构建镜像的应用,特别是在 Kubernetes 环境中的配置与优化。通过合理配置缓存和镜像构建,能够显著提升 CI/CD 流水线的效率和可靠性,助力开发团队实现持续集成与交付的目标。
|
1月前
|
机器学习/深度学习 人工智能 自然语言处理
AI技术深度解析:从基础到应用的全面介绍
人工智能(AI)技术的迅猛发展,正在深刻改变着我们的生活和工作方式。从自然语言处理(NLP)到机器学习,从神经网络到大型语言模型(LLM),AI技术的每一次进步都带来了前所未有的机遇和挑战。本文将从背景、历史、业务场景、Python代码示例、流程图以及如何上手等多个方面,对AI技术中的关键组件进行深度解析,为读者呈现一个全面而深入的AI技术世界。
147 10
|
2天前
|
JSON 小程序 UED
微信小程序 app.json 配置文件解析与应用
本文介绍了微信小程序中 `app.json` 配置文件的详细
32 12
|
10天前
|
供应链 搜索推荐 API
深度解析1688 API对电商的影响与实战应用
在全球电子商务迅猛发展的背景下,1688作为知名的B2B电商平台,为中小企业提供商品批发、分销、供应链管理等一站式服务,并通过开放的API接口,为开发者和电商企业提供数据资源和功能支持。本文将深入解析1688 API的功能(如商品搜索、详情、订单管理等)、应用场景(如商品展示、搜索优化、交易管理和用户行为分析)、收益分析(如流量增长、销售提升、库存优化和成本降低)及实际案例,帮助电商从业者提升运营效率和商业收益。
86 17
|
26天前
|
安全 API 数据安全/隐私保护
速卖通AliExpress商品详情API接口深度解析与实战应用
速卖通(AliExpress)作为全球化电商的重要平台,提供了丰富的商品资源和便捷的购物体验。为了提升用户体验和优化商品管理,速卖通开放了API接口,其中商品详情API尤为关键。本文介绍如何获取API密钥、调用商品详情API接口,并处理API响应数据,帮助开发者和商家高效利用这些工具。通过合理规划API调用策略和确保合法合规使用,开发者可以更好地获取商品信息,优化管理和营销策略。
|
2月前
|
存储 监控 API
深入解析微服务架构及其在现代应用中的实践
深入解析微服务架构及其在现代应用中的实践
86 12
|
1月前
|
机器学习/深度学习 搜索推荐 API
淘宝/天猫按图搜索(拍立淘)API的深度解析与应用实践
在数字化时代,电商行业迅速发展,个性化、便捷性和高效性成为消费者新需求。淘宝/天猫推出的拍立淘API,利用图像识别技术,提供精准的购物搜索体验。本文深入探讨其原理、优势、应用场景及实现方法,助力电商技术和用户体验提升。
|
2月前
|
编译器 PHP 开发者
PHP 8新特性解析与实战应用####
随着PHP 8的发布,这一经典编程语言迎来了诸多令人瞩目的新特性和性能优化。本文将深入探讨PHP 8中的几个关键新功能,包括命名参数、JIT编译器、新的字符串处理函数以及错误处理改进等。通过实际代码示例,展示如何在现有项目中有效利用这些新特性来提升代码的可读性、维护性和执行效率。无论你是PHP新手还是经验丰富的开发者,本文都将为你提供实用的技术洞察和最佳实践指导。 ####
41 1
|
2月前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
2月前
|
Java 测试技术 API
Java 反射机制:深入解析与应用实践
《Java反射机制:深入解析与应用实践》全面解析Java反射API,探讨其内部运作原理、应用场景及最佳实践,帮助开发者掌握利用反射增强程序灵活性与可扩展性的技巧。
158 4

热门文章

最新文章

推荐镜像

更多