在 WPF 客戶端實現 AOP 和介面快取
阿新 • • 發佈:2022-03-01
隨著業務越來越複雜,最近決定把一些頻繁查詢但是資料不會怎麼變更的介面做一下快取,這種功能一般用 AOP 就能實現了,找了一下客戶端又沒現成的直接可以用,嗐,就只能自己開發了。
代理模式和AOP
理解代理模式後,對 AOP 自然就手到擒來,所以先來點前置知識。
代理模式是一種使用一個類來控制另一個類方法呼叫的範例程式碼。
代理模式有三個角色:
- ISubject 介面,職責是定義行為。
- ISubject 的實現類 RealSubject,職責是實現行為。
- ISubject 的代理類 ProxySubject,職責是控制對 RealSubject 的訪問。
代理模式有三種實現:
- 普通代理。
- 強制代理,強制的意思就是不能直接訪問 RealSubject 的方法,必須通過代理類訪問。
- 動態代理,動態的意思是通過反射生成代理類,AOP 一般就是基於動態代理。
AOP 有四個關鍵知識點:
- 切入點 JoinPoint。就是 RealSubject 中的被控制訪問的方法。
- 通知 Advice,就是代理類中的方法,可以控制或者增強 RealSubject 的方法,有前置通知、後置通知、環繞通知等等
- 織入 Weave,就是按順序呼叫通知和 RealSubject 方法的過程。
- 切面 Aspect,多個切入點就會形成一個切面。
public interface ISubject { void DoSomething(string value); Task DoSomethingAsync(string value); } public class RealSubject : ISubject { public void DoSomething(string value) { Debug.WriteLine(value); } public async Task DoSomethingAsync(string value) { await Task.Delay(2000); Debug.WriteLine(value); } } public class Proxy : ISubject { private readonly ISubject _realSubject; public Proxy() { _realSubject = new RealSubject(); } /// <summary> /// 這就是切入點 /// </summary> /// <param name="value"></param> public void DoSomething(string value) { // 這個過程就是織入 Before(); _realSubject.DoSomething(value); After(); } public Task DoSomethingAsync(string value) { throw new NotImplementedException(); } public void Before() { Debug.WriteLine("普通代理類前置通知"); } public void After() { Debug.WriteLine("普通代理類後置通知"); } }
我使用的是 Castle.Core 這個庫來實現動態代理。但是這個代理有返回值的非同步方法自己寫起來比較費勁,但是 github 已經有不少庫封裝了實現過程,這裡我用 Castle.Core.AsyncInterceptor 來實現非同步方法的代理。
public class CastleInterceptor : StandardInterceptor { protected override void PostProceed(IInvocation invocation) { Debug.WriteLine("Castle 代理類前置通知"); } protected override void PreProceed(IInvocation invocation) { Debug.WriteLine("Castle 代理類後置通知"); } } public class AsyncCastleInterceptor : AsyncInterceptorBase { protected override async Task InterceptAsync(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task> proceed) { Before(); await proceed(invocation, proceedInfo); After(); } protected override async Task<TResult> InterceptAsync<TResult>(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task<TResult>> proceed) { Before(); var result = await proceed(invocation, proceedInfo); After(); return result; } public void Before() { Debug.WriteLine("非同步 Castle 代理類前置通知"); } public void After() { Debug.WriteLine("非同步 Castle 代理類後置通知"); } }
實現切面類和介面快取
實現過程:
- 定義 CacheAttribute 特性來標記需要快取的方法。
- 定義 CacheInterceptor 切面,實現在記憶體快取資料的邏輯。
- 使用切面,生成對介面的動態代理類,並且將代理類注入到 IOC 容器中。
- 介面通過 IOC 取得的介面實現類來訪問實現。
客戶端使用了 Prism 的 IOC 來實現控制反轉,Prism 支援多種 IOC,我這裡使用 DryIoc,因為其他幾個 IOC 已經不更新了。
客戶端記憶體快取使用 Microsoft.Extensions.Caching.Memory,這個算是最常用的了。
- 定義 CacheAttribute 特性來標記需要快取的方法。
[AttributeUsage(AttributeTargets.Method)]
public class CacheAttribute : Attribute
{
public string? CacheKey { get; }
public long Expiration { get; }
public CacheAttribute(string? cacheKey = null, long expiration = 0)
{
CacheKey = cacheKey;
Expiration = expiration;
}
public override string ToString() => $"{{ CacheKey: {CacheKey ?? "null"}, Expiration: {Expiration} }}";
}
- 定義 CacheInterceptor 切面類,實現在記憶體快取資料的邏輯
public class CacheInterceptor : AsyncInterceptorBase
{
private readonly IMemoryCache _memoryCache;
public CacheInterceptor(IMemoryCache memoryCache)
{
_memoryCache = memoryCache;
}
...
// 攔截非同步方法
protected override async Task<TResult> InterceptAsync<TResult>(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func<IInvocation, IInvocationProceedInfo, Task<TResult>> proceed)
{
var attribute = invocation.Method.GetCustomAttribute<CacheAttribute>();
if (attribute == null)
{
return await proceed(invocation, proceedInfo).ConfigureAwait(false);
}
var cacheKey = attribute.CacheKey ?? GenerateKey(invocation);
if (_memoryCache.TryGetValue(cacheKey, out TResult cacheValue))
{
if (cacheValue is string[] array)
{
Debug.WriteLine($"[Cache] Key: {cacheKey}, Value: {string.Join(',', array)}");
}
return cacheValue;
}
else
{
cacheValue = await proceed(invocation, proceedInfo).ConfigureAwait(false);
_memoryCache.Set(cacheKey, cacheValue);
return cacheValue;
}
}
// 生成快取的 Key
private string GenerateKey(IInvocation invocation)
{
...
}
// 格式化一下
private string FormatArgumentString(ParameterInfo argument, object value)
{
...
}
}
- 定義擴充套件類來生成切面,並且實現鏈式程式設計,可以方便地對一個介面新增多個切面類。
public static class DryIocInterceptionAsyncExtension
{
private static readonly DefaultProxyBuilder _proxyBuilder = new DefaultProxyBuilder();
// 生成切面
public static void Intercept<TService, TInterceptor>(this IRegistrator registrator, object serviceKey = null)
where TInterceptor : class, IInterceptor
{
var serviceType = typeof(TService);
Type proxyType;
if (serviceType.IsInterface())
proxyType = _proxyBuilder.CreateInterfaceProxyTypeWithTargetInterface(
serviceType, ArrayTools.Empty<Type>(), ProxyGenerationOptions.Default);
else if (serviceType.IsClass())
proxyType = _proxyBuilder.CreateClassProxyTypeWithTarget(
serviceType, ArrayTools.Empty<Type>(), ProxyGenerationOptions.Default);
else
throw new ArgumentException(
$"{serviceType} 無法被攔截, 只有介面或者類才能被攔截");
registrator.Register(serviceType, proxyType,
made: Made.Of(pt => pt.PublicConstructors().FindFirst(ctor => ctor.GetParameters().Length != 0),
Parameters.Of.Type<IInterceptor[]>(typeof(TInterceptor[]))),
setup: Setup.DecoratorOf(useDecorateeReuse: true, decorateeServiceKey: serviceKey));
}
// 鏈式程式設計,方便新增多個切面
public static IContainerRegistry InterceptAsync<TService, TInterceptor>(
this IContainerRegistry containerRegistry, object serviceKey = null)
where TInterceptor : class, IAsyncInterceptor
{
var container = containerRegistry.GetContainer();
container.Intercept<TService, AsyncInterceptor<TInterceptor>>(serviceKey);
return containerRegistry;
}
}
- 定義目標介面,並且在方法上標記一下
public interface ITestService
{
/// <summary>
/// 一個查詢大量資料的介面
/// </summary>
/// <returns></returns>
[Cache]
Task<string[]> GetLargeData();
}
public class TestService : ITestService
{
public async Task<string[]> GetLargeData()
{
await Task.Delay(2000);
var result = new[]{"大","量","數","據"};
Debug.WriteLine("從介面查詢資料");
return result;
}
}
- 向 IOC 容器注入切面類和業務介面。
public partial class App
{
protected override void RegisterTypes(IContainerRegistry containerRegistry)
{
// 注入快取類
containerRegistry.RegisterSingleton<IMemoryCache>(_ => new MemoryCache(new MemoryCacheOptions()));
// 注入切面類
containerRegistry.Register<AsyncInterceptor<CacheInterceptor>>();
// 注入介面和應用切面類
containerRegistry.RegisterSingleton<ITestService, TestService>()
.InterceptAsync<ITestService, CacheInterceptor>();
containerRegistry.RegisterSingleton<ITestService2, TestService2>()
.InterceptAsync<ITestService2, CacheInterceptor>();
}
...
}
效果
// AopView.xaml
<Button x:Name="cache" Content="Aop快取介面資料" />
// AopView.xaml.cs
cache.Click += (sender, args) => ContainerLocator.Container.Resolve<ITestService>().GetLargeData();
// 輸出
// 第一次點選列印
// 從介面查詢資料
// 之後點選列印
// [Cache] Key: PrismAop.Service.TestService2.GetLargeData(), Value: 大,量,數,據