【问题标题】:How can I make method signature caching?如何进行方法签名缓存?
【发布时间】:2011-03-11 23:25:01
【问题描述】:

我正在用 .NET 和 C# 构建一个应用程序,我想通过使用属性/注释而不是方法中的显式代码来缓存一些结果。

我想要一个看起来有点像这样的方法签名:

[Cache, timeToLive=60]
String getName(string id, string location)

它应该根据输入进行散列,并将其用作结果的键。 当然,会有一些配置文件告诉它如何实际放入 memcached、本地字典或其他东西。

你知道这样的框架吗?

我什至对 Java 也有兴趣

【问题讨论】:

    标签: c# .net caching methods


    【解决方案1】:

    使用Microsoft Enterprise Library 中的 CacheHandler,您可以轻松实现这一目标。 例如:

    [CacheHandler(0, 30, 0)]
    public Object GetData(Object input)
    {
    }
    

    将使对该方法的所有调用缓存 30 分钟。所有调用都会根据输入数据和方法名称获得一个唯一的缓存键,因此如果您使用不同的输入调用该方法两次,则它不会被缓存,但如果您在超时间隔内使用相同的输入调用它 >1 次,则方法只执行一次。

    我在 Microsoft 的代码中添加了一些额外的功能:

    我修改后的版本如下:

    using System;
    using System.Diagnostics;
    using System.IO;
    using System.Reflection;
    using System.Runtime.Remoting.Contexts;
    using System.Text;
    using System.Web;
    using System.Web.Caching;
    using System.Web.UI;
    using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
    using Microsoft.Practices.Unity.InterceptionExtension;
    
    
    namespace Middleware.Cache
    {
        /// <summary>
        /// An <see cref="ICallHandler"/> that implements caching of the return values of
        /// methods. This handler stores the return value in the ASP.NET cache or the Items object of the current request.
        /// </summary>
        [ConfigurationElementType(typeof (CacheHandler)), Synchronization]
        public class CacheHandler : ICallHandler
        {
            /// <summary>
            /// The default expiration time for the cached entries: 5 minutes
            /// </summary>
            public static readonly TimeSpan DefaultExpirationTime = new TimeSpan(0, 5, 0);
    
            private readonly object cachedData;
    
            private readonly DefaultCacheKeyGenerator keyGenerator;
            private readonly bool storeOnlyForThisRequest = true;
            private TimeSpan expirationTime;
            private GetNextHandlerDelegate getNext;
            private IMethodInvocation input;
    
    
            public CacheHandler(TimeSpan expirationTime, bool storeOnlyForThisRequest)
            {
                keyGenerator = new DefaultCacheKeyGenerator();
                this.expirationTime = expirationTime;
                this.storeOnlyForThisRequest = storeOnlyForThisRequest;
            }
    
            /// <summary>
            /// This constructor is used when we wrap cached data in a CacheHandler so that 
            /// we can reload the object after it has been removed from the cache.
            /// </summary>
            /// <param name="expirationTime"></param>
            /// <param name="storeOnlyForThisRequest"></param>
            /// <param name="input"></param>
            /// <param name="getNext"></param>
            /// <param name="cachedData"></param>
            public CacheHandler(TimeSpan expirationTime, bool storeOnlyForThisRequest,
                                IMethodInvocation input, GetNextHandlerDelegate getNext,
                                object cachedData)
                : this(expirationTime, storeOnlyForThisRequest)
            {
                this.input = input;
                this.getNext = getNext;
                this.cachedData = cachedData;
            }
    
    
            /// <summary>
            /// Gets or sets the expiration time for cache data.
            /// </summary>
            /// <value>The expiration time.</value>
            public TimeSpan ExpirationTime
            {
                get { return expirationTime; }
                set { expirationTime = value; }
            }
    
            #region ICallHandler Members
    
            /// <summary>
            /// Implements the caching behavior of this handler.
            /// </summary>
            /// <param name="input"><see cref="IMethodInvocation"/> object describing the current call.</param>
            /// <param name="getNext">delegate used to get the next handler in the current pipeline.</param>
            /// <returns>Return value from target method, or cached result if previous inputs have been seen.</returns>
            public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
            {
                lock (input.MethodBase)
                {
                    this.input = input;
                    this.getNext = getNext;
    
                    return loadUsingCache();
                }
            }
    
            public int Order
            {
                get { return 0; }
                set { }
            }
    
            #endregion
    
            private IMethodReturn loadUsingCache()
            {
                //We need to synchronize calls to the CacheHandler on method level
                //to prevent duplicate calls to methods that could be cached.
                lock (input.MethodBase)
                {
                    if (TargetMethodReturnsVoid(input) || HttpContext.Current == null)
                    {
                        return getNext()(input, getNext);
                    }
    
                    var inputs = new object[input.Inputs.Count];
                    for (int i = 0; i < inputs.Length; ++i)
                    {
                        inputs[i] = input.Inputs[i];
                    }
    
                    string cacheKey = keyGenerator.CreateCacheKey(input.MethodBase, inputs);
                    object cachedResult = getCachedResult(cacheKey);
    
                    if (cachedResult == null)
                    {
                        var stopWatch = Stopwatch.StartNew();
                        var realReturn = getNext()(input, getNext);
                        stopWatch.Stop();
                        if (realReturn.Exception == null && realReturn.ReturnValue != null)
                        {
                            AddToCache(cacheKey, realReturn.ReturnValue);
                        }
                        return realReturn;
                    }
    
                    var cachedReturn = input.CreateMethodReturn(cachedResult, input.Arguments);
    
                    return cachedReturn;
                }
            }
    
            private object getCachedResult(string cacheKey)
            {
                //When the method uses input that is not serializable 
                //we cannot create a cache key and can therefore not 
                //cache the data.
                if (cacheKey == null)
                {
                    return null;
                }
    
                object cachedValue = !storeOnlyForThisRequest ? HttpRuntime.Cache.Get(cacheKey) : HttpContext.Current.Items[cacheKey];
                var cachedValueCast = cachedValue as CacheHandler;
                if (cachedValueCast != null)
                {
                    //This is an object that is reloaded when it is being removed.
                    //It is therefore wrapped in a CacheHandler-object and we must
                    //unwrap it before returning it.
                    return cachedValueCast.cachedData;
                }
                return cachedValue;
            }
    
            private static bool TargetMethodReturnsVoid(IMethodInvocation input)
            {
                var targetMethod = input.MethodBase as MethodInfo;
                return targetMethod != null && targetMethod.ReturnType == typeof (void);
            }
    
            private void AddToCache(string key, object valueToCache)
            {
                if (key == null)
                {
                    //When the method uses input that is not serializable 
                    //we cannot create a cache key and can therefore not 
                    //cache the data.
                    return;
                }
    
                if (!storeOnlyForThisRequest)
                {
                    HttpRuntime.Cache.Insert(
                        key,
                        valueToCache,
                        null,
                        System.Web.Caching.Cache.NoAbsoluteExpiration,
                        expirationTime,
                        CacheItemPriority.Normal, null);
                }
                else
                {
                    HttpContext.Current.Items[key] = valueToCache;
                }
            }
        }
    
        /// <summary>
        /// This interface describes classes that can be used to generate cache key strings
        /// for the <see cref="CacheHandler"/>.
        /// </summary>
        public interface ICacheKeyGenerator
        {
            /// <summary>
            /// Creates a cache key for the given method and set of input arguments.
            /// </summary>
            /// <param name="method">Method being called.</param>
            /// <param name="inputs">Input arguments.</param>
            /// <returns>A (hopefully) unique string to be used as a cache key.</returns>
            string CreateCacheKey(MethodBase method, object[] inputs);
        }
    
        /// <summary>
        /// The default <see cref="ICacheKeyGenerator"/> used by the <see cref="CacheHandler"/>.
        /// </summary>
        public class DefaultCacheKeyGenerator : ICacheKeyGenerator
        {
            private readonly LosFormatter serializer = new LosFormatter(false, "");
    
            #region ICacheKeyGenerator Members
    
            /// <summary>
            /// Create a cache key for the given method and set of input arguments.
            /// </summary>
            /// <param name="method">Method being called.</param>
            /// <param name="inputs">Input arguments.</param>
            /// <returns>A (hopefully) unique string to be used as a cache key.</returns>
            public string CreateCacheKey(MethodBase method, params object[] inputs)
            {
                try
                {
                    var sb = new StringBuilder();
    
                    if (method.DeclaringType != null)
                    {
                        sb.Append(method.DeclaringType.FullName);
                    }
                    sb.Append(':');
                    sb.Append(method.Name);
    
                    TextWriter writer = new StringWriter(sb);
    
                    if (inputs != null)
                    {
                        foreach (var input in inputs)
                        {
                            sb.Append(':');
                            if (input != null)
                            {
                                //Diffrerent instances of DateTime which represents the same value
                                //sometimes serialize differently due to some internal variables which are different.
                                //We therefore serialize it using Ticks instead. instead.
                                var inputDateTime = input as DateTime?;
                                if (inputDateTime.HasValue)
                                {
                                    sb.Append(inputDateTime.Value.Ticks);
                                }
                                else
                                {
                                    //Serialize the input and write it to the key StringBuilder.
                                    serializer.Serialize(writer, input);
                                }
                            }
                        }
                    }
    
                    return sb.ToString();
                }
                catch
                {
                    //Something went wrong when generating the key (probably an input-value was not serializble.
                    //Return a null key.
                    return null;
                }
            }
    
            #endregion
        }
    }
    

    Microsoft 最值得称赞的是这段代码。我们只添加了诸如在请求级别而不是跨请求缓存之类的东西(比您想象的更有用)并修复了一些错误(例如,相等的 DateTime 对象序列化为不同的值)。

    【讨论】:

    • 这需要使用Unity依赖注入框架,对吧? (如果为真,您可能应该在答案中注意到这一点)。
    • 这正是我要找的 :) 我会试一试,然后回来给答案打分
    • +1。很好,我稍作修改以便能够将其与自定义缓存提供程序一起使用(这个很好的示例是面向 Web 的)。
    【解决方案2】:

    完全按照您的描述进行,即写作

    public class MyClass {
      [Cache, timeToLive=60]
      string getName(string id, string location){
        return ExpensiveCall(id, location);
      }
    }
    
    // ...
    MyClass c = new MyClass();
    string name = c.getName("id", "location");
    string name_again = c.getName("id", "location");
    

    并且只有一次调用昂贵的调用并且不需要使用其他代码(f.x.CacheHandler&lt;MyClass&gt; c = new CacheHandler&lt;MyClass&gt;(new MyClass());)包装类,您需要查看Aspect Oriented Programming 框架。这些通常通过重写字节码来工作,因此您需要在编译过程中添加另一个步骤 - 但您在此过程中获得了很多权力。有许多 AOP 框架,但 .NET 的 PostSharpAspectJ 是最受欢迎的。您可以轻松地 Google 如何使用它们来添加您想要的缓存方面。

    【讨论】:

    • 我也想到了一些类似的东西,但我不确定如何获得输入参数的良好哈希值。我也希望有现成的东西,所以我不需要自己动手。
    猜你喜欢
    • 2023-02-03
    • 1970-01-01
    • 1970-01-01
    • 2023-03-11
    • 2010-12-30
    • 2013-11-04
    • 2022-07-16
    • 2020-04-22
    • 1970-01-01
    相关资源
    最近更新 更多