随着业务越来越复杂,最近决定把一些频繁查询但是数据不会怎么变更的接口做一下缓存,这种功能一般用 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

在 WPF 客户端实现 AOP 和接口缓存的更多相关文章

  1. springboot 2.x整合redis,spring aop实现接口缓存

    pox.xml: <dependency> <groupId>org.springframework.boot</groupId> <artifactId&g ...

  2. 使用AOP 实现Redis缓存注解,支持SPEL

    公司项目对Redis使用比较多,因为之前没有做AOP,所以缓存逻辑和业务逻辑交织在一起,维护比较艰难所以最近实现了针对于Redis的@Cacheable,把缓存的对象依照类别分别存放到redis的Ha ...

  3. 在ASP.NET Core中使用AOP来简化缓存操作

    前言 关于缓存的使用,相信大家都是熟悉的不能再熟悉了,简单来说就是下面一句话. 优先从缓存中取数据,缓存中取不到再去数据库中取,取到了在扔进缓存中去. 然后我们就会看到项目中有类似这样的代码了. pu ...

  4. .NetCore之接口缓存

    1.问题:我们平时做开发的时候肯定都有用到缓存这个功能,一般写法是在需要的业务代码里读取缓存.判断是否存在.不存在则读取数据库再设置缓存这样一个步骤.但是如果我们有很多地方业务都有用到缓存,我们就需要 ...

  5. ssm+redis 如何更简洁的利用自定义注解+AOP实现redis缓存

    基于 ssm + maven + redis 使用自定义注解 利用aop基于AspectJ方式 实现redis缓存 如何能更简洁的利用aop实现redis缓存,话不多说,上demo 需求: 数据查询时 ...

  6. wpf 客户端【JDAgent桌面助手】开发详解(四) popup控件的win8.0的bug

    目录区域: 业余开发的wpf 客户端终于完工了..晒晒截图 wpf 客户端[JDAgent桌面助手]开发详解-开篇 wpf 客户端[JDAgent桌面助手]详解(一)主窗口 圆形菜单... wpf 客 ...

  7. SpringCloud使用Feign调用其他客户端带参数的接口,传入参数为null或报错status 405 reading IndexService#del(Integer);

    SpringCloud使用Feign调用其他客户端带参数的接口,传入参数为null或报错status 405 reading IndexService#del(Integer); 第一种方法: 如果你 ...

  8. Springboot学习06-Spring AOP封装接口自定义校验

    Springboot学习06-Spring AOP封装接口自定义校验 关键字 BindingResult.Spring AOP.自定义注解.自定义异常处理.ConstraintValidator 前言 ...

  9. wpf 客户端【JDAgent桌面助手】开发详解(三) 瀑布流效果实现与UI虚拟化优化大数据显示

    目录区域: 业余开发的wpf 客户端终于完工了..晒晒截图 wpf 客户端[JDAgent桌面助手]开发详解-开篇 wpf 客户端[JDAgent桌面助手]详解(一)主窗口 圆形菜单... wpf 客 ...

随机推荐

  1. 《剑指offer》面试题25. 合并两个排序的链表

    问题描述 输入两个递增排序的链表,合并这两个链表并使新链表中的节点仍然是递增排序的. 示例1: 输入:1->2->4, 1->3->4 输出:1->1->2-> ...

  2. C#检测外部exe程序弹窗错误,并重启

    private void button2_Click(object sender, EventArgs e) { string mainTitle = System.Configuration.Con ...

  3. windows10双系统删除linux

    问题 在这里删除后会发现有残留一个引导区,几百m(下图已经删除完),而且启动会进linux引导,然后必须f12进入选择启动项才可以启动windows 解决方法 使用删除引导就可以了 再使用傲梅分区助手 ...

  4. Cesium源码剖析---Post Processing之物体描边(Silhouette)

    Cesium在1.46版本中新增了对整个场景的后期处理(Post Processing)功能,包括模型描边.黑白图.明亮度调整.夜视效果.环境光遮蔽等.对于这么炫酷的功能,我们绝不犹豫,先去翻一翻它的 ...

  5. 将Cesium ion上的3D Tiles和Bing imagery应用到osgEarth

    Cesium中文网:http://cesiumcn.org/ | 国内快速访问:http://cesium.coinidea.com/ Pelican Mapping 激动的宣布支持加载Cesium ...

  6. java原码、反码、补码、位运算

    1.对于有符号的数(java中的数都是有符号的) 二进制的最高位是符号位:0表示正数,1表示负数 正数的原码,反码,补码都一样 负数的反码=它的原码符号位不变,其它位取反 负数的补码=它的反码+1 0 ...

  7. golang中的udp编程

    1. udp server package main import ( "fmt" "net" ) func main() { // udp server li ...

  8. java多态instanceof介绍

    1 public static void method(Animal a) {//类型判断 2 a.eat(); 3 if(a instanceof Cat) {//instanceof:用于判断对象 ...

  9. Task+ConcurrentQueue多线程编程

    队列(Queue)代表了一个先进先出的对象集合.当您需要对各项进行先进先出的访问时,则使用队列.当您在列表中添加一项,称为入队,当您从列表中移除一项时,称为出队. ConcurrentQueue< ...

  10. C编译器中“不是所有的控件路径都返回值”报错

    编译器的判断逻辑是是否在所有的分支中都返回了值,即if不成立时也必须返回值.编译器认为如果三个if都不成立则此函数可能没有返回值,故报错.需要将第三个if改为else或者去掉if体直接return.