本文有配套视频:
https://www.bilibili.com/video/av58096866/?p=6
前言
上回《【 .NET Core3.0 】框架之九 || 依赖注入IoC学习 + AOP界面编程初探》咱们说到了依赖注入Autofac的使用,不知道大家对IoC的使用是怎样的感觉,我个人表示还是比较可行的,至少不用自己再关心一个个复杂的实例化服务对象了,直接通过接口就满足需求,当然还有其他的一些功能,我还没有说到,抛砖引玉嘛,大家如果有好的想法,欢迎留言,也可以来群里,大家一起学习讨论。昨天在文末咱们说到了AOP面向切面编程的定义和思想,我个人简单使用了下,感觉主要的思路还是通过拦截器来操作,就像是一个中间件一样,今天呢,我给大家说两个小栗子,当然,你也可以合并成一个,也可以自定义扩展,因为我们是真个系列是基于Autofac框架,所以今天主要说的是基于Autofac的Castle动态代理的方法,静态注入的方式以后有时间可以再补充。
时间真快,转眼已经十天过去了,感谢大家的鼓励,批评指正,希望我的文章,对您有一点点儿的帮助,哪怕是有学习新知识的动力也行,至少至少,可以为以后跳槽增加新的谈资 [哭笑],这些天我们从面向对象OOP的开发,后又转向了面向接口开发,到分层解耦,现在到了面向切面编程AOP,往下走将会是,分布式,微服务等等,技术真是永无止境啊!好啦,马上开始动笔。
一、什么是 AOP 切面编程思想
什么是AOP?引用百度百科:AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。实现AOP主要由两种方式,
一种是编译时静态织入,优点是效率高,缺点是缺乏灵活性,.net下postsharp为代表者(好像是付费了。。)。
另一种方式是动态代理,优点是灵活性强,但是会影响部分效率,动态为目标类型创建代理,通过代理调用实现拦截。
AOP能做什么,常见的用例是事务处理、日志记录等等。
常见的AOP都是配合在Ioc的基础上进行操作,上边咱们讲了Autofac这个简单强大的Ioc框架,下面就讲讲Autofac怎么实现AOP。Autofac的AOP是通过Castle(也是一个容器)项目的核心部分实现的,名为Autofac.Extras.DynamicProxy,顾名思义,其实现方式为动态代理。当然AOP并不一定要和依赖注入在一起使用,自身也可以单独使用。
是不是很拗口,没关系,网上有一个博友的图片,大概讲了AOP切面编程:
说的很通俗易懂的话就是,我们在 service 方法的前边和后边,各自动态增加了一个方法,这样就包裹了每一个服务方法,从而实现业务逻辑的解耦。
AOP,我们并不陌生。可能大家感觉这个切面编程思想之前没有用到过,很新鲜的一个东西,其实不是的,之前我们开发的时候也一直在使用这种思想,那就是过滤器,我们可以想想,我们之前在开发 MVC 的时候,是不是经常要对action进行控制过滤,最常见的就是全局异常处理过滤器,只要有错误,就跳出去,记录日志,然后去一个自定义的异常页面,这个其实就是一个 AOP 的思想,但是这里请注意,这个思想是广义的 AOP 编程思想,今天要说的,是真正意义上的切面编程思想,是基于动态代理的基于服务层的编程思想,也是在以后的开发中使用很多的一种编程思想。
二、AOP 之实现日志记录
首先想一想,如果有这么一个需求,要记录整个项目的接口和调用情况,当然如果只是控制器的话,还是挺简单的,直接用一个过滤器或者一个中间件,还记得咱们开发Swagger拦截权限验证的中间件么,那个就很方便的把用户调用接口的名称记录下来,当然也可以写成一个切面,但是如果想看下与Service或者Repository层的调用情况呢,好像目前咱们只能在Service层或者Repository层去写日志记录了,那样的话,不仅工程大(当然你可以用工厂模式),而且耦合性瞬间就高了呀,想象一下,如果日志要去掉,关闭,修改,需要改多少地方!您说是不是,好不容易前边的工作把层级的耦合性降低了。别慌,这个时候就用到了AOP和Autofac的Castle结合的完美解决方案了。
经过这么多天的开发,几乎每天都需要引入Nuget包哈,我个人表示也不想再添加了,现在都已经挺大的了(47M当然包括全部dll文件),今天不会啦!其实都是基于昨天的两个Nuget包中已经自动生成的Castle组件。请看以下步骤:
1、定义服务接口与实现类
在上一篇文章中,我们说到了使用
AdvertisementServices.cs 和 IAdvertisementServices.cs
这个服务,我们新建两个层,分别包含这两个 cs 文件:
然后我们模拟下数据,再新建一个 Model 层,添加 AdvertisementEntity 实体类
namespace Blog.Core.Model
{ public class AdvertisementEntity { public int id { get; set; } public string name { get; set; } }
}
然后在上边的 service 方法中,返回一个List数据:
// 接口 public interface IAdvertisementServices { int Test(); List<AdvertisementEntity> TestAOP(); } // 实现类 public class AdvertisementServices : IAdvertisementServices { public int Test() { return 1; } public List<AdvertisementEntity> TestAOP() => new List<AdvertisementEntity>() { new AdvertisementEntity() { id = 1, name = "laozhang" } }; }
2、在API层中添加对该接口引用
还是在默认的控制器——weatherForecastController.cs 里,添加调用方法:
/// <summary> /// 测试AOP /// </summary> /// <returns></returns> [HttpGet] public List<AdvertisementEntity> TestAdsFromAOP() { return _advertisementServices.TestAOP(); }
这里采用的是依赖注入的方法,把 _advertisementServices 注入到控制器的,如果还不会,请看我上一篇文章。
3、添加AOP拦截器
在api层新建文件夹AOP,添加拦截器BlogLogAOP,并设计其中用到的日志记录Logger方法或者类
关键的一些知识点,注释中已经说明了,主要是有以下:
1、继承接口IInterceptor2、实例化接口IINterceptor的唯一方法Intercept3、void Proceed();表示执行当前的方法4、执行后,输出到日志文件。
namespace blog.core.test3._0.AOP
{ /// <summary> /// 拦截器BlogLogAOP 继承IInterceptor接口 /// </summary> public class BlogLogAOP : IInterceptor { /// <summary> /// 实例化IInterceptor唯一方法 /// </summary> /// <param name="invocation">包含被拦截方法的信息</param> public void Intercept(IInvocation invocation) { // 事前处理: 在服务方法执行之前,做相应的逻辑处理 var dataIntercept = "" + $"【当前执行方法】:{ invocation.Method.Name} \r\n" + $"【携带的参数有】: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n"; // 执行当前访问的服务方法,(注意:如果下边还有其他的AOP拦截器的话,会跳转到其他的AOP里) invocation.Proceed(); // 事后处理: 在service被执行了以后,做相应的处理,这里是输出到日志文件 dataIntercept += ($"【执行完成结果】:{invocation.ReturnValue}"); // 输出到日志文件 Parallel.For(0, 1, e => { LogLock.OutSql2Log("AOPLog", new string[] { dataIntercept }); }); } }
}
提示:这里展示了如何在项目中使用AOP实现对 service 层进行日志记录,如果你想实现异常信息记录的话,很简单,
注意,这个方法仅仅是针对同步的策略,如果你的service是异步的,这里获取不到,正确的写法,在文章底部的 GitHub 代码里,因为和 AOP 思想没有直接的关系,这里就不赘述。
4、将拦截器注入容器,代理服务
还记得昨天的Autofac容器 ConfigureContainer 么,我们继续对它进行处理:
1、先把拦截器注入容器;
2、然后对程序集的注入方法中匹配拦截器服务;
public void ConfigureContainer(ContainerBuilder builder) { var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath; //直接注册某一个类和接口 //左边的是实现类,右边的As是接口 builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>(); builder.RegisterType<BlogLogAOP>();//可以直接替换其他拦截器!一定要把拦截器进行注册 //注册要通过反射创建的组件 var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll"); var assemblysServices = Assembly.LoadFrom(servicesDllFile); builder.RegisterAssemblyTypes(assemblysServices) .AsImplementedInterfaces() .InstancePerLifetimeScope() .EnableInterfaceInterceptors() .InterceptedBy(typeof(BlogLogAOP));//可以放一个AOP拦截器集合 }
注意其中的两个方法
.EnableInterfaceInterceptors()//对目标类型启用接口拦截。拦截器将被确定,通过在类或接口上截取属性, 或添加 InterceptedBy ()
.InterceptedBy(typeof(BlogLogAOP));//允许将拦截器服务的列表分配给注册。
说人话就是,将拦截器添加到要注入容器的接口或者类之上。
5、运行项目,查看效果
这个时候,我们运行项目,然后访问api 的 TestAdsFromAOP() 接口,你就看到这根目录下生成了一个Log文件夹,里边有日志记录,当然记录很简陋,里边是获取到的实体类,大家可以自己根据需要扩展。
这里,面向服务层的日志记录就完成了,大家感觉是不是很平时的不一样?我们几乎什么都没做,只是增加了一个AOP的拦截器,就可以控制 service 层的任意一个方法,这就是AOP思想的精髓——业务的解耦。
那AOP仅仅是做日志记录么,还有没有其他的用途,这里我随便举一个例子——缓存。
三、AOP 实现数据缓存功能
想一想,如果我们要实现缓存功能,一般咱们都是将数据获取到以后,定义缓存,然后在其他地方使用的时候,在根据key去获取当前数据,然后再操作等等,平时都是在API接口层获取数据后进行缓存,今天咱们可以试试,在接口之前就缓存下来 —— 基于service层的缓存策略。
1、定义 Memory 缓存类和接口
这里既然要用到缓存,那我们就定义一个缓存类和接口,在 Helper 文件夹下,新建两个类文件,ICaching.cs 和 MemoryCaching.cs
你会问了,为什么上边的日志没有定义,因为我会在之后讲Redis的时候用到这个缓存接口。
/// <summary> /// 简单的缓存接口,只有查询和添加,以后会进行扩展 /// </summary> public interface ICaching { object Get(string cacheKey); void Set(string cacheKey, object cacheValue); } /// <summary> /// 实例化缓存接口ICaching /// </summary> public class MemoryCaching : ICaching { //引用Microsoft.Extensions.Caching.Memory;这个和.net 还是不一样,没有了Httpruntime了 private IMemoryCache _cache; //还是通过构造函数的方法,获取 public MemoryCaching(IMemoryCache cache) { _cache = cache; } public object Get(string cacheKey) { return _cache.Get(cacheKey); } public void Set(string cacheKey, object cacheValue) { _cache.Set(cacheKey, cacheValue, TimeSpan.FromSeconds(7200)); } }
2、定义一个缓存拦截器
还是继承IInterceptor,并实现Intercept,这个过程和上边 日志AOP 是一样,不多说,大家也正好可以自己动手练习一下。
新建缓存AOP:BlogCacheAOP.cs
/// <summary> /// 面向切面的缓存使用 /// </summary> public class BlogCacheAOP : AOPbase { //通过注入的方式,把缓存操作接口通过构造函数注入 private readonly ICaching _cache; public BlogCacheAOP(ICaching cache) { _cache = cache; } //Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义 public override void Intercept(IInvocation invocation) { //获取自定义缓存键 var cacheKey = CustomCacheKey(invocation); //根据key获取相应的缓存值 var cacheValue = _cache.Get(cacheKey); if (cacheValue != null) { //将当前获取到的缓存值,赋值给当前执行方法 invocation.ReturnValue = cacheValue; return; } //去执行当前的方法 invocation.Proceed(); //存入缓存 if (!string.IsNullOrWhiteSpace(cacheKey)) { _cache.Set(cacheKey, invocation.ReturnValue); } } }
代码中注释的很清楚,需要注意是两点:
1、采用依赖注入,把缓存注入到当前拦截器里;
2、继承了一个 AOPBase 抽象类,里边有如何定义缓存 key 等内容;
namespace blog.core.test3._0.AOP
{ public abstract class AOPbase : IInterceptor { /// <summary> /// AOP的拦截方法 /// </summary> /// <param name="invocation"></param> public abstract void Intercept(IInvocation invocation); /// <summary> /// 自定义缓存的key /// </summary> /// <param name="invocation"></param> /// <returns></returns> protected string CustomCacheKey(IInvocation invocation) { var typeName = invocation.TargetType.Name; var methodName = invocation.Method.Name; var methodArguments = invocation.Arguments.Select(GetArgumentValue).Take(3).ToList();//获取参数列表,最多三个 string key = $"{typeName}:{methodName}:"; foreach (var param in methodArguments) { key = $"{key}{param}:"; } return key.TrimEnd(':'); } /// <summary> /// object 转 string /// </summary> /// <param name="arg"></param> /// <returns></returns> protected static string GetArgumentValue(object arg) { if (arg is DateTime || arg is DateTime?) return ((DateTime)arg).ToString("yyyyMMddHHmmss"); if (arg is string || arg is ValueType || arg is Nullable) return arg.ToString(); if (arg != null) { if (arg.GetType().IsClass) { return MD5Encrypt16(Newtonsoft.Json.JsonConvert.SerializeObject(arg)); } } return string.Empty; } /// <summary> /// 16位MD5加密 /// </summary> /// <param name="password"></param> /// <returns></returns> public static string MD5Encrypt16(string password) { var md5 = new MD5CryptoServiceProvider(); string t2 = BitConverter.ToString(md5.ComputeHash(Encoding.Default.GetBytes(password)), 4, 8); t2 = t2.Replace("-", string.Empty); return t2; } }
}
3、注入拦截器到服务
具体的操作方法,上边我们都已经说到了,大家依然可以自己练习一下,这里直接把最终的代码展示一下:
注意://将 TService 中指定的类型的范围服务添加到实现 services.AddScoped<ICaching, MemoryCaching>();//记得把缓存注入!!!
4、运行,查看效果
你会发现,首次缓存是空的,然后将serv中取出来的数据存入缓存,第二次使用就是有值了,其他所有的地方使用,都不用再写了,而且也是面向整个程序集合的
5、多个AOP执行顺序问题
在我最新的 Github 项目中,我定义了四个 AOP :除了上边两个 LogAOP和 CacheAOP 以外,还有一个 RedisCacheAOP 和 事务BlogTranAOP,并且通过开关的形式在项目中配置是否启用:
那具体的执行顺序是什么呢,这里说下,就是从上至下的顺序,或者可以理解成挖金矿的形式,执行完上层的,然后紧接着来下一个AOP,最后想要回家,就再一个一个跳出去,在往上层走的时候,矿肯定就执行完了,就不用再操作了,直接出去,就像 break 一样。
6、无接口如何实现AOP
上边我们讨论了很多,但是都是接口框架的,
比如:Service.dll 和与之对应的 IService.dll,Repository.dll和与之对应的 IRepository.dll,我们可以直接在对应的层注入的时候,匹配上 AOP 信息,但是如果我们没有使用接口怎么办?
这里大家可以安装下边的实验下:
Autofac它只对接口方法 或者 虚virtual方法或者重写方法override才能起拦截作用。
如果没有接口
案例是这样的:
如果我们的项目是这样的,没有接口,会怎么办:
// 服务层类 public class StudentService { StudentRepository _studentRepository; public StudentService(StudentRepository studentRepository) { _studentRepository = studentRepository; } public string Hello() { return _studentRepository.Hello(); } } // 仓储层类 public class StudentRepository { public StudentRepository() { } public string Hello() { return "hello world!!!"; } } // controller 接口调用 StudentService _studentService; public ValuesController(StudentService studentService) { _studentService = studentService; }
如果是没有接口的单独实体类
public class Love
{ // 一定要是虚方法 public virtual string SayLoveU() { return "I ♥ U"; }
} //--------------------------- //只能注入该类中的虚方法
builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love))) .EnableClassInterceptors() .InterceptedBy(typeof(BlogLogAOP));
到了这里,我们已经明白了什么是AOP切面编程,也通过两个业务逻辑学会了如何去使用AOP编程,那这里有一个小问题,如果我某些service类和方法并不想做相应的AOP处理,该如何筛选呢?请继续看。
四、给缓存增加验证筛选
1、自定义缓存特性
在解决方案中添加新项目Blog.Core.Common,然后在该Common类库中添加 特性文件夹 和 特性实体类,以后特性就在这里
/// <summary> /// 这个Attribute就是使用时候的验证,把它添加到要缓存数据的方法中,即可完成缓存的操作。注意是对Method验证有效 /// </summary> [AttributeUsage(AttributeTargets.Method, Inherited = true)] public class CachingAttribute : Attribute { //缓存绝对过期时间 public int AbsoluteExpiration { get; set; } = 30; }
2、在AOP拦截器中进行过滤
添加Common程序集引用,然后修改缓存AOP类方法 BlogCacheAOP=》Intercept,简单对方法的方法进行判断
/// <summary>
/// 面向切面的缓存使用
/// </summary>
public class BlogCacheAOP : AOPbase
{ //通过注入的方式,把缓存操作接口通过构造函数注入 private readonly ICaching _cache; public BlogCacheAOP(ICaching cache) { _cache = cache; } //Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义 public override void Intercept(IInvocation invocation) { var method = invocation.MethodInvocationTarget ?? invocation.Method; //对当前方法的特性验证 var qCachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)) as CachingAttribute; //只有那些指定的才可以被缓存,需要验证 if (qCachingAttribute != null) { //获取自定义缓存键 var cacheKey = CustomCacheKey(invocation); //根据key获取相应的缓存值 var cacheValue = _cache.Get(cacheKey); if (cacheValue != null) { //将当前获取到的缓存值,赋值给当前执行方法 invocation.ReturnValue = cacheValue; return; } //去执行当前的方法 invocation.Proceed(); //存入缓存 if (!string.IsNullOrWhiteSpace(cacheKey)) { _cache.Set(cacheKey, invocation.ReturnValue); } } }
}
我们增加了一个 if 判断,只有那些带有缓存特性的类和方法才会被执行这个 AOP 拦截。
3、在service层中增加缓存特性
在指定的Service层中的某些类的某些方法上增加特性(一定是方法,不懂的可以看定义特性的时候AttributeTargets.Method)
4、特定缓存效果展示
运行项目,打断点,就可以看到,普通的Query或者CURD等都不继续缓存了,只有咱们特定的 getBlogs()方法,带有缓存特性的才可以
当然,这里还有一个小问题,就是所有的方法还是走的切面,只是增加了过滤验证,大家也可以直接把那些需要的注入,不需要的干脆不注入Autofac容器,我之所以需要都经过的目的,就是想把它和日志结合,用来记录Service层的每一个请求,包括CURD的调用情况。
五、基于AOP的Redis缓存
1、核心:Redis缓存切面拦截器
在上篇文章中,我们已经定义过了一个拦截器,只不过是基于内存Memory缓存的,并不适应于Redis,上边咱们也说到了Redis必须要存入指定的值,比如字符串,而不能将异步对象 Task<T> 保存到硬盘上,所以我们就修改下拦截器方法,一个专门应用于 Redis 的切面拦截器:
/// <summary> /// 面向切面的缓存使用 /// </summary> public class BlogRedisCacheAOP : CacheAOPbase { //通过注入的方式,把缓存操作接口通过构造函数注入 private readonly IRedisCacheManager _cache; public BlogRedisCacheAOP(IRedisCacheManager cache) { _cache = cache; } //Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义 public override void Intercept(IInvocation invocation) { var method = invocation.MethodInvocationTarget ?? invocation.Method; //对当前方法的特性验证 var qCachingAttribute = method.GetCustomAttributes(true).FirstOrDefault(x => x.GetType() == typeof(CachingAttribute)) as CachingAttribute; if (qCachingAttribute != null) { //获取自定义缓存键 var cacheKey = CustomCacheKey(invocation); //注意是 string 类型,方法GetValue var cacheValue = _cache.GetValue(cacheKey); if (cacheValue != null) { //将当前获取到的缓存值,赋值给当前执行方法 var type = invocation.Method.ReturnType; var resultTypes = type.GenericTypeArguments; if (type.FullName == "System.Void") { return; } object response; if (typeof(Task).IsAssignableFrom(type)) { //返回Task<T> if (resultTypes.Any()) { var resultType = resultTypes.FirstOrDefault(); // 核心1,直接获取 dynamic 类型 dynamic temp = Newtonsoft.Json.JsonConvert.DeserializeObject(cacheValue, resultType); response = Task.FromResult(temp); } else { //Task 无返回方法 指定时间内不允许重新运行 response = Task.Yield(); } } else { // 核心2,要进行 ChangeType response = Convert.ChangeType(_cache.Get<object>(cacheKey), type); } invocation.ReturnValue = response; return; } //去执行当前的方法 invocation.Proceed(); //存入缓存 if (!string.IsNullOrWhiteSpace(cacheKey)) { object response; //Type type = invocation.ReturnValue?.GetType(); var type = invocation.Method.ReturnType; if (typeof(Task).IsAssignableFrom(type)) { var resultProperty = type.GetProperty("Result"); response = resultProperty.GetValue(invocation.ReturnValue); } else { response = invocation.ReturnValue; } if (response == null) response = string.Empty; _cache.Set(cacheKey, response, TimeSpan.FromMinutes(qCachingAttribute.AbsoluteExpiration)); } } else { invocation.Proceed();//直接执行被拦截方法 } } }
上边的代码和memory缓存的整体结构差不多的,相信都能看的懂的,最后我们就可以很任性的在Autofac容器中,进行任意缓存切换了,是不是很棒!
再次感觉小伙伴JoyLing,不知道他博客园地址。
六、一些其他问题需要考虑
1、时间问题,阻塞,浪费资源问题等
定义切面有时候是方便,初次使用会很别扭,使用多了,可能会对性能有些许的影响,因为会大量动态生成代理类,性能损耗,是特别高的请求并发,比如万级每秒,还是要深入的研究,不可随意使用,但是基本平时开发的时候,还是可以使用的,毕竟性价比挺高的,我说的也是九牛一毛,大家继续加油吧!
2、静态注入
基于Net的IL语言层级进行注入,性能损耗可以忽略不计,Net使用最多的Aop框架PostSharp(好像收费了;)采用的即是这种方式。
大家可以参考这个博文:https://www.cnblogs.com/mushroom/p/3932698.html
七、CODE
https://github.com/anjoy8/Blog.Core
https://gitee.com/laozhangIsPhi/Blog.Core