网站建设佰金手指科杰十三,中国二级建造师网官网,做网站什么职业,wordpress评论邮箱通知功能本文有配套视频#xff1a;https://www.bilibili.com/video/av58096866/?p6前言上回《【 .NET Core3.0 】框架之九 || 依赖注入IoC学习 AOP界面编程初探》咱们说到了依赖注入Autofac的使用#xff0c;不知道大家对IoC的使用是怎样的感觉#xff0c;我个人表示还是比较可行… 本文有配套视频https://www.bilibili.com/video/av58096866/?p6 前言 上回《【 .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(); ListAdvertisementEntity TestAOP(); } // 实现类 public class AdvertisementServices : IAdvertisementServices { public int Test() { return 1; } public ListAdvertisementEntity TestAOP() new ListAdvertisementEntity() { new AdvertisementEntity() { id 1, name laozhang } }; } 2、在API层中添加对该接口引用还是在默认的控制器——weatherForecastController.cs 里添加调用方法 /// summary /// 测试AOP /// /summary /// returns/returns [HttpGet] public ListAdvertisementEntity 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 nameinvocation包含被拦截方法的信息/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.RegisterTypeAdvertisementServices().AsIAdvertisementServices(); builder.RegisterTypeBlogLogAOP();//可以直接替换其他拦截器一定要把拦截器进行注册 //注册要通过反射创建的组件 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 是一样不多说大家也正好可以自己动手练习一下。新建缓存AOPBlogCacheAOP.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 nameinvocation/param public abstract void Intercept(IInvocation invocation); /// summary /// 自定义缓存的key /// /summary /// param nameinvocation/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 namearg/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 namepassword/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.AddScopedICaching, MemoryCaching();//记得把缓存注入 4、运行查看效果你会发现首次缓存是空的然后将serv中取出来的数据存入缓存第二次使用就是有值了其他所有的地方使用都不用再写了而且也是面向整个程序集合的 5、多个AOP执行顺序问题 在我最新的 Github 项目中我定义了四个 AOP 除了上边两个 LogAOP和 CacheAOP 以外还有一个 RedisCacheAOP 和 事务BlogTranAOP并且通过开关的形式在项目中配置是否启用 那具体的执行顺序是什么呢这里说下就是从上至下的顺序或者可以理解成挖金矿的形式执行完上层的然后紧接着来下一个AOP最后想要回家就再一个一个跳出去在往上层走的时候矿肯定就执行完了就不用再操作了直接出去就像 break 一样。6、无接口如何实现AOP 上边我们讨论了很多但是都是接口框架的比如Service.dll 和与之对应的 IService.dllRepository.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.Method4、特定缓存效果展示运行项目打断点就可以看到普通的Query或者CURD等都不继续缓存了只有咱们特定的 getBlogs()方法带有缓存特性的才可以 当然这里还有一个小问题就是所有的方法还是走的切面只是增加了过滤验证大家也可以直接把那些需要的注入不需要的干脆不注入Autofac容器我之所以需要都经过的目的就是想把它和日志结合用来记录Service层的每一个请求包括CURD的调用情况。 五、基于AOP的Redis缓存 1、核心Redis缓存切面拦截器 在上篇文章中我们已经定义过了一个拦截器只不过是基于内存Memory缓存的并不适应于Redis上边咱们也说到了Redis必须要存入指定的值比如字符串而不能将异步对象 TaskT 保存到硬盘上所以我们就修改下拦截器方法一个专门应用于 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)) { //返回TaskT 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.Getobject(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 七、CODEhttps://github.com/anjoy8/Blog.Corehttps://gitee.com/laozhangIsPhi/Blog.Core