在 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 代理類後置通知");
    }
}

實現切面類和介面快取

實現過程:

  1. 定義 CacheAttribute 特性來標記需要快取的方法。
  2. 定義 CacheInterceptor 切面,實現在記憶體快取資料的邏輯。
  3. 使用切面,生成對介面的動態代理類,並且將代理類注入到 IOC 容器中。
  4. 介面通過 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: 大,量,數,據

最後

其實還有很多細節可以完善一下,比如說快取重新整理規則,服務端重新整理客戶端快取等等,不過客戶端 AOP 的實現差不多就這樣了。

覺得對你有幫助點個推薦或者留言交流一下唄!

原始碼 https://github.com/yijidao/blog/tree/master/WPF/PrismAop

相關文章