使用DispatchProxy实现带异步调用的缓存拦截是否可行?
使用DispatchProxy实现带异步调用的缓存拦截是否可行?
这个问题我之前做类似的拦截器时也碰到过,确实DispatchProxy的Invoke方法是同步签名,要在里面处理异步缓存操作会有点绕,咱们一步步捋清楚问题和解决思路。
首先得明确核心矛盾:你的Invoke方法是同步的,但里面调用的HybridCache.GetOrCreateAsync返回的是异步的ValueTask,如果强行在同步上下文里等待异步操作,很容易出现死锁或者无法正确获取结果的情况——尤其是如果被拦截的目标方法本身就是异步的(返回Task/ValueTask),这种强行同步等待的方式基本行不通。
可行的解决思路:区分同步/异步方法分别处理
正确的做法是不要一概而论,先判断被拦截的目标方法是同步还是异步,然后用不同的逻辑处理:
1. 处理异步目标方法(返回Task/ValueTask)
如果被拦截的方法本身就是异步的,那调用方本来就会通过await来获取结果,这时候你根本不需要在Invoke里等待异步缓存操作完成,而是把缓存操作的异步流程直接返回给调用方,让调用方的await去处理它。
修改你的Invoke方法,添加异步方法判断和处理逻辑:
protected override object? Invoke(MethodInfo? targetMethod, object?[]? args) { Console.WriteLine("Before caching"); if (targetMethod == null) return null; var returnType = targetMethod.ReturnType; // 情况1:目标方法是异步的(返回Task/ValueTask) if (typeof(Task).IsAssignableFrom(returnType)) { // 生成唯一缓存键(不要用固定值,否则所有方法调用会共用缓存) var cacheKey = $"{targetMethod.DeclaringType?.FullName}.{targetMethod.Name}_{JsonSerializer.Serialize(args)}"; // 执行缓存异步操作 var cacheValueTask = _hybridCache.GetOrCreateAsync<object?>(cacheKey, async _ => { var invokeResult = targetMethod.Invoke(_decorated, args); // 如果原方法返回的是Task,需要先await拿到真正的结果 if (invokeResult is Task taskResult) { await taskResult.ConfigureAwait(false); // 提取泛型Task的Result值 var resultProp = taskResult.GetType().GetProperty(nameof(Task<object>.Result)); return resultProp?.GetValue(taskResult); } return invokeResult; }); // 把ValueTask转换成目标方法需要的异步返回类型 if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(Task<>)) { // 处理Task<T>类型 var resultType = returnType.GetGenericArguments()[0]; var task = cacheValueTask.ContinueWith(t => Convert.ChangeType(t.Result, resultType), TaskContinuationOptions.OnlyOnRanToCompletion); return task; } else if (returnType == typeof(Task)) { // 处理非泛型Task类型 return cacheValueTask.AsTask(); } else if (returnType.IsGenericType && returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)) { // 处理ValueTask<T>类型 var resultType = returnType.GetGenericArguments()[0]; var task = cacheValueTask.ContinueWith(t => Convert.ChangeType(t.Result, resultType), TaskContinuationOptions.OnlyOnRanToCompletion); return Activator.CreateInstance(returnType, task); } else if (returnType == typeof(ValueTask)) { // 处理非泛型ValueTask类型 return new ValueTask(cacheValueTask.AsTask()); } } // 情况2:目标方法是同步的 var syncCacheKey = $"{targetMethod.DeclaringType?.FullName}.{targetMethod.Name}_{JsonSerializer.Serialize(args)}"; var syncResult = _hybridCache.GetOrCreateAsync<object?>(syncCacheKey, _ => { var val = targetMethod.Invoke(_decorated, args); return ValueTask.FromResult(val); }).AsTask().ConfigureAwait(false).GetAwaiter().GetResult(); return syncResult; }
2. 处理同步目标方法
如果被拦截的方法是同步的,那可以用ConfigureAwait(false).GetAwaiter().GetResult()来同步等待异步缓存操作完成——注意一定要加ConfigureAwait(false),它可以避免捕获当前的同步上下文,减少死锁的概率,这可能是你之前尝试没成功的原因之一。
额外注意点
- 缓存键不要用固定值:你现在代码里用的是固定的
"CacheKey",这样所有方法调用都会共用同一个缓存值,肯定会出问题,要根据方法名、参数生成唯一的键(比如上面代码里用方法全名+参数序列化的方式)。 - 避免死锁:如果你的代码运行在有同步上下文的环境(比如UI线程、ASP.NET Core之前的Web环境),强行同步等待异步操作很容易死锁,这也是为什么异步方法要传递异步流程而不是强行等待的原因。
整体来说,这种拦截方式是完全可行的,只要区分同步和异步方法分别处理,就可以避开同步/异步上下文的冲突问题。
内容来源于stack exchange
相关产品推荐
相关产品推荐

