在|在 WPF 客户端实现 AOP 和接口缓存
随着业务越来越复杂,最近决定把一些频繁查询但是数据不会怎么变更的接口做一下缓存,这种功能一般用 AOP 就能实现了,找了一下客户端又没现成的直接可以用,嗐,就只能自己开发了。
代理模式和AOP
理解代理模式后,对 AOP 自然就手到擒来,所以先来点前置知识。
代理模式是一种使用一个类来控制另一个类方法调用的范例代码。
代理模式有三个角色:
- ISubject 接口,职责是定义行为。
- ISubject 的实现类 RealSubject,职责是实现行为。
- ISubject 的代理类 ProxySubject,职责是控制对 RealSubject 的访问。
- 普通代理。
- 强制代理,强制的意思就是不能直接访问 RealSubject 的方法,必须通过代理类访问。
- 动态代理,动态的意思是通过反射生成代理类,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();
}///
/// 这就是切入点
///
///
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 proceed)
{
Before();
await proceed(invocation, proceedInfo);
After();
}protected override async Task InterceptAsync(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func> 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 取得的接口实现类来访问实现。
客户端内存缓存使用 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 InterceptAsync(IInvocation invocation, IInvocationProceedInfo proceedInfo, Func> proceed)
{
var attribute = invocation.Method.GetCustomAttribute();
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 = https://www.it610.com/article/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(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(), ProxyGenerationOptions.Default);
else if (serviceType.IsClass())
proxyType = _proxyBuilder.CreateClassProxyTypeWithTarget(
serviceType, ArrayTools.Empty(), 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(typeof(TInterceptor[]))),
setup: Setup.DecoratorOf(useDecorateeReuse: true, decorateeServiceKey: serviceKey));
}
// 链式编程,方便添加多个切面
public static IContainerRegistry InterceptAsync(
this IContainerRegistry containerRegistry, object serviceKey = null)
where TInterceptor : class, IAsyncInterceptor
{
var container = containerRegistry.GetContainer();
container.Intercept>(serviceKey);
return containerRegistry;
}
}
- 定义目标接口,并且在方法上标记一下
public interface ITestService
{
///
/// 一个查询大量数据的接口
///
///
[Cache]
Task GetLargeData();
}public class TestService : ITestService
{
public async Task 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(_ => new MemoryCache(new MemoryCacheOptions()));
// 注入切面类
containerRegistry.Register>();
// 注入接口和应用切面类
containerRegistry.RegisterSingleton()
.InterceptAsync();
containerRegistry.RegisterSingleton()
.InterceptAsync();
}
...
}
效果
// AopView.xaml
// AopView.xaml.cs
cache.Click += (sender, args) => ContainerLocator.Container.Resolve().GetLargeData();
// 输出
// 第一次点击打印
// 从接口查询数据// 之后点击打印
// [Cache]Key: PrismAop.Service.TestService2.GetLargeData(), Value: 大,量,数,据
最后
其实还有很多细节可以完善一下,比如说缓存刷新规则,服务端刷新客户端缓存等等,不过客户端 AOP 的实现差不多就这样了。 觉得对你有帮助点个推荐或者留言交流一下呗! 源码 https://github.com/yijidao/blog/tree/master/WPF/PrismAop
推荐阅读
- 在线Excel文件解析转换成JSON格式
- 网站在线客服系统GOFLY源码开发日志- 2. 开发命令行应用
- java运行时添加枚举值,尝试在Java的运行时从枚举中获取关联的值
- macos|走出第一步 —— 在Windows上安装Mac OS
- 为什么java首次运行慢_第一次Java循环运行缓慢,为什么([Sun HotSpot 1.5,sparc])
- java怎么创建日期_用户如何在我的日历(java)中创建日期()
- star法则java简历_在简历中使用STAR法则
- 那个名为 XROS 的操作系统,倒在了元宇宙浪潮中
- HMS|HMS Core在MWC2022展示最新开放能力,助力开发者构建精品应用
- 成人教育在线培训网站开源