ASP.NET CORE 第五篇 AOP面向切面编程浅解析:简单日志记录 + 服务切面缓存

原文作者:老张的哲学

零、今天完成的深红色部分

image.png

一、AOP 之 实现日志记录(服务层)

首先想一想,如果有一个需求(这个只是我的一个想法,真实工作中可能用不上),要记录整个项目的接口和调用情况,当然如果只是控制器的话,还是挺简单的,直接用一个过滤器或者一个中间件,还记得咱们开发Swagger拦截权限验证的中间件么,那个就很方便的把用户调用接口的名称记录下来,当然也可以写成一个切面,但是如果想看下与Service或者Repository层的调用情况呢,好像目前咱们只能在Service层或者Repository层去写日志记录了,那样的话,不仅工程大(当然你可以用工厂模式),而且耦合性瞬间就高了呀,想象一下,如果日志要去掉,关闭,修改,需要改多少地方!您说是不是,好不容易前边的工作把层级的耦合性降低了。别慌,这个时候就用到了AOP和Autofac的Castle结合的完美解决方案了。
  经过这么多天的开发,几乎每天都需要引入Nuget包哈,我个人表示也不想再添加了,现在都已经挺大的了(47M当然包括全部dll文件),今天不会辣!其实都是基于昨天的两个Nuget包中已经自动生成的Castle组件。请看以下步骤:

1、定义服务接口与实现类

首先这里使用到了 BlogArticle 的实体类(这里我保留了sqlsugar的特性,没需要的可以手动删除):

using SqlSugar;
using System;

namespace Blog.Core.Model
{
    public class BlogArticle
    {
        /// <summary>
        /// 主键
        /// </summary>
        /// 这里之所以没用RootEntity,是想保持和之前的数据库一致,主键是bID,不是Id
        [SugarColumn(IsNullable = false,IsPrimaryKey = true,IsIdentity = true)]
        public int bID { get; set; }

        /// <summary>
        /// 创建人
        /// </summary>
        [SugarColumn(Length = 60,IsNullable = true)]
        public string bsubmitter { get; set; }

        /// <summary>
        /// 标题blog
        /// </summary>
        [SugarColumn(Length = 256,IsNullable = true)]
        public string btitle { get; set; }

        /// <summary>
        /// 类别
        /// </summary>
        [SugarColumn(Length = int.MaxValue,IsNullable = true)]
        public string bcategory { get; set; }

        [SugarColumn(IsNullable = true,ColumnDataType = "text")]
        public string bcontent { get; set; }

        /// <summary>
        /// 访问量
        /// </summary>
        public int btraffic { get; set; }

        /// <summary>
        /// 评论数量
        /// </summary>
        public int bcommentNum { get; set; }

        /// <summary>
        /// 修改时间
        /// </summary>
        public DateTime bUploadTime { get; set; }

        /// <summary>
        /// 创建时间
        /// </summary>
        public DateTime bCreateTime { get; set; }

        /// <summary>
        /// 备注
        /// </summary>
        [SugarColumn(Length = int.MaxValue,IsNullable = true)]
        public string bRemark { get; set; }

        /// <summary>
        /// 逻辑删除
        /// </summary>
        [SugarColumn(IsNullable = true)]
        public bool? IsDeleted { get; set; }
    }
}

在IBlogArticleServices.cs定义一个获取博客列表接口 ,并在BlogArticleServices实现该接口

using Blog.Core.IRepository;
using Blog.Core.IServices;
using Blog.Core.Model;
using Blog.Core.Services.Base;
using System.Collections.Generic;
using System.Threading.Tasks;

namespace Blog.Core.Services
{
    public class ArticleServices : BaseServices<Article>, IArticleServices
    {
        readonly IArticleRepository dal;

        public ArticleServices(IArticleRepository dal)
        {
            this.dal = dal;
            base.baseDal = dal;
        }

        /// <summary>
        /// 获取博客列表
        /// </summary>
        /// <returns></returns>
        public async Task<List<Article>> getBlogsAsync()
        {
            var blogList = await dal.Query(a => a.bID > 0,a => a.bID);
            return blogList;
        }
    }
}

2、在API层中添加对该接口引用
(注意RESTful接口路径命名规范,我这么写只是为了测试)

/// <summary>
        /// 获取博客列表
        /// </summary>
        /// <returns></returns>
        [HttpGet]
        [Route("GetBlogs")]
        public async Task<List<BlogArticle>> GetBlogs()
        {

            return await blogArticleServices.getBlogs();
        }

3、添加AOP拦截器

在Blog.Core新建文件夹AOP,并添加拦截器BlogLogAOP,并设计其中用到的日志记录Logger方法或者类

image.png

关键的一些知识点,注释中已经说明了,主要是有以下:
1、继承接口IInterceptor
2、实例化接口IINterceptor的唯一方法Intercept
3、void Proceed();表示执行当前的方法和object ReturnValue { get; set; }执行后调用,object[] Arguments参数对象
4、中间的代码是新建一个类,还是单写,就很随意了。

using Castle.DynamicProxy;
using System;
using System.IO;
using System.Linq;

namespace Blog.Core.API.AOP
{
    /// <summary>
    /// 拦截器BlogLogAOP继承IInterceptor接口
    /// </summary>
    public class BlogLogAOP : IInterceptor
    {
        /// <summary>
        /// 实例化IInterceptor唯一方法
        /// </summary>
        /// <param name="invocation">包含拦截方法的信息</param>
        public void Intercept(IInvocation invocation)
        {
            // 记录被拦截方法的日志信息
            var dataInterceptor = $"{DateTime.Now.ToString("yyyyMMddHHmmss")}" +
                $"当前执行方法:{invocation.Method.Name} " +
                $"参数是: {string.Join(", ", invocation.Arguments.Select(a => (a ?? "").ToString()).ToArray())} \r\n";

            // 在被拦截的方法执行完毕后 继续执行当前方法
            invocation.Proceed();

            dataInterceptor += ($"方法执行完毕,返回结果:{invocation.ReturnValue}");

            #region 输出当前项目日志
            var path = Directory.GetCurrentDirectory() + @"\Log";
            if (!Directory.Exists(path))
            {
                Directory.CreateDirectory(path);
            }
            string fileName = path + $@"\InterceptLog-{DateTime.Now.ToString("yyyyMMddHHmmss")}.log";
            StreamWriter sw = File.AppendText(fileName);
            sw.WriteLine(dataInterceptor);
            sw.Close();
            #endregion
        }
    }
}

提示:这里展示了如何在项目中使用AOP实现对 service 层进行日志记录,如果你想实现异常信息记录的话,很简单,

注意,这个方法仅仅是针对同步的策略,如果你的service是异步的,这里获取不到,正确的写法,在文章底部的 GitHub 代码里,因为和 AOP 思想没有直接的关系,这里就不赘述。

image.png

下边的是完整代码:

/// <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";

    try
    {
        MiniProfiler.Current.Step($"执行Service方法:{invocation.Method.Name}() -> ");
        //在被拦截的方法执行完毕后 继续执行当前方法,注意是被拦截的是异步的
        invocation.Proceed();


        // 异步获取异常,先执行
        if (IsAsyncMethod(invocation.Method))
        {

            //Wait task execution and modify return value
            if (invocation.Method.ReturnType == typeof(Task))
            {
                invocation.ReturnValue = InternalAsyncHelper.AwaitTaskWithPostActionAndFinally(
                    (Task)invocation.ReturnValue,
                    async () => await TestActionAsync(invocation),
                    ex =>
                    {
                        LogEx(ex, ref dataIntercept);
                    });
            }
            else //Task<TResult>
            {
                invocation.ReturnValue = InternalAsyncHelper.CallAwaitTaskWithPostActionAndFinallyAndGetResult(
                 invocation.Method.ReturnType.GenericTypeArguments[0],
                 invocation.ReturnValue,
                 async () => await TestActionAsync(invocation),
                 ex =>
                 {
                     LogEx(ex, ref dataIntercept);
                 });

            }

        }
        else
        {// 同步1


        }
    }
    catch (Exception ex)// 同步2
    {
        LogEx(ex, ref dataIntercept);

    }

    dataIntercept += ($"【执行完成结果】:{invocation.ReturnValue}");

    Parallel.For(0, 1, e =>
    {
        LogLock.OutSql2Log("AOPLog", new string[] { dataIntercept });
    });

    _hubContext.Clients.All.SendAsync("ReceiveUpdate", LogLock.GetLogData()).Wait();
}
4、添加到Autofac容器中,实现注入

还记得昨天的容器么,先把拦截器注入,然后对程序集的注入方法中添加拦截器服务即可

builder.RegisterType<BlogLogAOP>();//可以直接替换其他拦截器!一定要把拦截器进行注册

            var assemblysServices = Assembly.Load("Blog.Core.Services");

            //builder.RegisterAssemblyTypes(assemblysServices).AsImplementedInterfaces();//指定已扫描程序集中的类型注册为提供所有其实现的接口。

            builder.RegisterAssemblyTypes(assemblysServices)
                      .AsImplementedInterfaces()
                      .InstancePerLifetimeScope()
                      .EnableInterfaceInterceptors()//引用Autofac.Extras.DynamicProxy;
                      .InterceptedBy(typeof(BlogLogAOP));//可以直接替换拦截器

注意其中的两个方法
.EnableInterfaceInterceptors()//对目标类型启用接口拦截。拦截器将被确定,通过在类或接口上截取属性, 或添加 InterceptedBy ()
.InterceptedBy(typeof(BlogLogAOP));//允许将拦截器服务的列表分配给注册。
说人话就是,将拦截器添加到要注入容器的接口或者类之上。

5、运行项目,查看效果

嗯,你就看到这根目录下生成了一个Log文件夹,里边有日志记录,当然记录很简陋,里边是获取到的实体类,大家可以自己根据需要扩展

****
image

这里,面向服务层的日志记录就完成了,大家感觉是不是很平时的不一样?

二、AOP 之 实现接口数据的缓存功能

想一想,如果我们要实现缓存功能,一般咱们都是将数据获取到以后,定义缓存,然后在其他地方使用的时候,在根据key去获取当前数据,然后再操作等等,平时都是在API接口层获取数据后进行缓存,今天咱们可以试试,在接口之前就缓存下来。

1、定义 Memory 缓存类和接口

老规矩,定义一个缓存类和接口,你会问了,为什么上边的日志没有定义,因为我会在之后讲Redis的时候用到这个缓存接口
ICaching.cs:

using System;
using System.Collections.Generic;
using System.Text;

namespace Blog.Core.Common.MemoryCache
{
    /// <summary>
    /// 简单的缓存接口,只有查询和添加,以后会进行扩展
    /// </summary>
    public interface ICaching
    {
        object Get(string key);
        void Set(string key,object value);
    }
}

MemoryCaching.cs:

using Microsoft.Extensions.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Text;

namespace Blog.Core.Common.MemoryCache
{
    /// <summary>
    /// 实例化缓存接口ICaching
    /// </summary>
    public class MemoryCaching : ICaching
    {
        // 引用Microsoft.Extensions.Caching.Memory;这个和.net 还是不一样,没有了Httpruntime了
        private IMemoryCache _cache;

        /// <summary>
        /// 还是通过构造函数的方法,获取
        /// </summary>
        /// <param name="cache"></param>
        public MemoryCaching(IMemoryCache cache)
        {
            _cache = cache;
        }

        public object Get(string key)
        {
            return _cache.Get(key);
        }

        public void Set(string key, object value)
        {
            _cache.Set(key, value);
        }
    }
}

2、定义一个缓存拦截器

还是继承IInterceptor,并实现Intercept

using Blog.Core.Common.MemoryCache;
using Castle.DynamicProxy;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Blog.Core.API.AOP
{
    /// <summary>
    /// 面向切面缓存的使用
    /// </summary>
    public class BlogCacheAOP : IInterceptor
    {
        // 通过注入的方式,把缓存操作接口通过构造函数注入
        private ICaching _cache;
        public BlogCacheAOP(ICaching cache)
        {
            _cache = cache;
        }

        /// <summary>
        /// Intercept方法是拦截的关键所在,也是IInterceptor接口中的唯一定义
        /// </summary>
        /// <param name="invocation"></param>
        public 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);
            }
        }

        /// <summary>
        /// 定义缓存键
        /// </summary>
        /// <param name="invication"></param>
        /// <returns></returns>
        private string CustomCacheKey(IInvocation invication)
        {
            var typeName = invication.TargetType.Name;
            var methodName = invication.Method.Name;

            // 获取参数列表,我最多需要三个即可
            var methodArguments = invication.Arguments.Select(GetArgumentValue).Take(3).ToList();
            string key = $"{typeName}:{methodName}";
            foreach(var param in methodArguments)
            {
                key += $"{param}";
            }
            return key.TrimEnd(':');
        }

        /// <summary>
        /// object 转 string
        /// </summary>
        /// <param name="arg"></param>
        /// <returns></returns>
        private string GetArgumentValue(object arg)
        {
            if (arg is int || arg is long || arg is string)
                return arg.ToString();
            if (arg is DateTime)
                return ((DateTime)arg).ToString("yyyyMMddHHmmss");
            return "";
        }
    }
}

注释的很清楚,基本都是情况

3、注入缓存拦截器

ConfigureServices不用动,只需要改下拦截器的名字就行

注意:
//将 TService 中指定的类型的范围服务添加到实现
services.AddScoped<ICaching, MemoryCaching>();//记得把缓存注入!!!

image.png

4、运行,查看效果

你会发现,首次缓存是空的,然后将Repository仓储中取出来的数据存入缓存,第二次使用就是有值了,其他所有的地方使用,都不用再写了,而且也是面向整个程序集合的

image

****

image

5、多个AOP执行顺序问题

在我最新的 Github 项目中,我定义了三个 AOP :除了上边两个 LogAOP和 CacheAOP 以外,还有一个 RedisCacheAOP,并且通过开关的形式在项目中配置是否启用:

image.png

那具体的执行顺序是什么呢,这里说下,就是从上至下的顺序,或者可以理解成挖金矿的形式,执行完上层的,然后紧接着来下一个AOP,最后想要回家,就再一个一个跳出去,在往上层走的时候,矿肯定就执行完了,就不用再操作了,直接出去,就像 break 一样,可以参考这个动图:

1468246-20190419142358007-1374098637.gif

6、无接口如何实现AOP

上边我们讨论了很多,但是都是接口框架的,
比如:Service.dll 和与之对应的 IService.dll,Repository.dll和与之对应的 IRepository.dll,我们可以直接在对应的层注入的时候,匹配上 AOP 信息,但是如果我们没有使用接口怎么办?
这里大家可以安装下边的实验下:
Autofac它只对接口方法 或者 虚virtual方法或者重写方法override才能起拦截作用。

如果没有接口

案例是这样的:

如果我们的项目是这样的,没有接口,会怎么办:


image
// 服务层类 
   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;
    }
image.png

如果是没有接口的单独实体类

public class Love
    {
        // 一定要是虚方法
        public virtual string SayLoveU()
        {
            return "I ♥ U";
        }

    }

//---------------------------

//只能注入该类中的虚方法
builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love)))
    .EnableClassInterceptors()
    .InterceptedBy(typeof(BlogLogAOP));

三、还有其他的一些问题需要考虑

1、可以针对某一层的指定类的指定方法进行操作,这里就不写了,大家可以自己实验
配合Attribute就可以只拦截相应的方法了。因为拦截器里面是根据Attribute进行相应判断的!!

builder.RegisterAssemblyTypes(assembly)
   .Where(type => typeof(IQCaching).IsAssignableFrom(type) && !type.GetTypeInfo().IsAbstract) .AsImplementedInterfaces()
   .InstancePerLifetimeScope()
   .EnableInterfaceInterceptors()
   .InterceptedBy(typeof(QCachingInterceptor));

2、时间问题,阻塞,浪费资源问题等
 定义切面有时候是方便,初次使用会很别扭,使用多了,可能会对性能有些许的影响,因为会大量动态生成代理类,性能损耗,是特别高的请求并发,比如万级每秒,还是不建议生产环节推荐。所以说切面编程要深入的研究,不可随意使用,我说的也是九牛一毛,大家继续加油吧!

3、静态注入

基于Net的IL语言层级进行注入,性能损耗可以忽略不计,Net使用最多的Aop框架PostSharp(好像收费了;)采用的即是这种方式。

大家可以参考这个博文:https://www.cnblogs.com/mushroom/p/3932698.html

四、结语

今天的讲解就到了这里了,通过这两个小栗子,大家应该能对面向切面编程有一些朦胧的感觉了吧,感兴趣的可以深入的研究,也欢迎一起讨论,刚刚在缓存中,我说到了缓存接口,就引入了下次的讲解内容,Redis的高性能缓存框架,内存存储的数据结构服务器,可用作数据库,高速缓存和消息队列代理。下次再见咯~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,830评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,992评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,875评论 0 331
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,837评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,734评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,091评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,550评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,217评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,368评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,298评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,350评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,027评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,623评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,706评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,940评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,349评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,936评论 2 341

推荐阅读更多精彩内容