学习ASP.NET Core(07)-AOP动态代理与日志

上一篇咱们简单介绍了RESTful WebAPI涉及到的一些基础知识,并初步完善了系统的一些功能;本章咱们将介绍下AOP并使用动态代理的方式实现记录日志的功能html


1、面向切面编程

一、什么是AOP

AOP是Accept Oriented Programming的缩写,即面向切面编程。它与IOC控制反转,OOP面向对象编程等思想同样,都是一种编程思想,它是经过预编译方式和运行期间动态代理的方式来实现程序功能统一维护的一种技术。简单来讲就是就是在不影响核心逻辑的状况下,为程序提供"可拔插"的扩展功能。以下图,来源为—韩俊俊,什么是面向切面编程AOPvue

二、AOP思想的产生

随着关注点的不一样会致使不一样的切面,好比部分方法须要受权才能继续操做,一些方法中咱们须要记录日志或是异常信息等,它们与核心逻辑没有必然的联系,它们独立且分散却又是程序中必不可少的一部分。C#语言是一种面向对象语言,它会基于OOP思想的封装、继承、多态三大特性将公共行为封装为一个类,可是当咱们须要将独立的对象引入公共行为时,会发现它与OOP思想产生了必定的冲突,这时就须要运用AOP思想来解决这类问题。编程

三、AOP相关术语(了解)

  1. 横切关注点:用于一个系统的多个部分的片断功能,好比受权验证,日志记录,异常处理等;
  2. 通知(Advice):执行横切关注点(独立功能)的代码;
  3. 链接点(JoinPoint):程序执行通知的地方,好比一个类里面有10个方法,那么这10个方法在建立方法对象前,建立完成调用方法前以及调用方法后均可以看做是一个链接点;
  4. 切入点(PointCut):至关于AOP的“where”,它是链接点的”集合“,好比上面10个方法只想在其中几个链接点使用通知,那么这几个链接点就称为切入点;
  5. 切面(Aspect):切面是通知和切入点的结合;
  6. 引入(Introduction): 容许咱们向现有的类添加新的方法或属性,就是把切面用到目标类中;
  7. 目标(Target): 引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑;
  8. 代理(Porxy): 向目标对象增长通知以后建立的对象,由这个对象来访问实际的目标对象;
  9. 织入(Weaving): 将切面应用到目标对象来建立新的代理对象的过程;

四、.NET Core中的实现

在.NET Core中,实现AOP思想的经常使用对象有中间件(Middleware)、过滤器(Filter)和基于AOP思想的拦截器。其中拦截器又分为静态代理、动态代理;静态代理会在编译时静态植入,优势是效率高,缺点是缺少灵活性;动态代理会为目标建立代理,经过代理调用实现拦截,优势是灵活性强,缺点是会影响部分效率。c#

上述三个对象它们对应了不一样的应用场景:后端

  • 中间件:处理的是请求管道;一般用于底层服务的通讯
  • 过滤器:处理的是Action方法和URL;一般用于身份验证,参数验证等
  • 拦截器:处理的是对象的元数据,包括类、方法名、参数等;一般用于配合处理业务逻辑

2、AOP动态代理

一般状况下,当咱们想记录项目接口的调用状况时,可使用过滤器或者自定义一个中间件来实现,但若是想看下与数据层或逻辑层的调用状况,就比较复杂了,在这些层级中进行添加输出日志的功能显然不是一个合理的解决办法。这里咱们采用动态代理的方式来解决,其核心思想就是将服务的实例交给代理类来控制,代理类能够在其内部方法中控制执行或者是添加本身的处理逻辑,下面咱们来看下记录逻辑层调用信息的具体实现。网络

一、引入动态代理

其实反射类Reflection中已经封装了代理方法,可是须要在StartUp中的ConfigureServices方法里指明代理类与服务实例的映射关系,这就致使没有较好的方法在控制器中使用。前后端分离

因为以前咱们已经使用Autofac容器替换了系统容器,因此这里咱们能够选择使用一款封装好了的且与Autofac配合度较高的第三方插件Castle.Core,在BlogSystem.Core层使用NuGet安装以下包,它包含了Castle.Core异步

二、设计拦截器

在BlogSystem.Core层中添加AOP文件夹,并添加一个名为LogAop的类,继承自拦截器接口IInterceptor(须要引用Castle.DynamicProxy)并实现其方法,这里咱们先添加invocation.Proceed()方法,以下:学习

以后咱们就能够在该方法内部自定义相关逻辑的,须要注意的是咱们的系统内部大多数是异步操做,因此须要判断是否为异步方法并进行拦截,不然会拦截失败。这里逻辑基本上是参照的老张的哲学的,我的就稍微改了下,具体实现以下:spa

using BlogSystem.Core.Helpers;
using Castle.DynamicProxy;
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;

namespace BlogSystem.Core.AOP
{
    public class LogAop : IInterceptor
    {
        private readonly IHttpContextAccessor _accessor;

        private static readonly string FileName = "AOPInterceptor-" + DateTime.Now.ToString("yyyyMMddHH") + ".log";

        //支持单个写线程和多个读线程的锁
        private static readonly ReaderWriterLockSlim Lock = new ReaderWriterLockSlim();

        public LogAop(IHttpContextAccessor accessor)
        {
            _accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
        }

        public void Intercept(IInvocation invocation)
        {
            var userId = JwtHelper.JwtDecrypt(_accessor.HttpContext.Request.Headers["Authorization"]).UserId;

            //记录被拦截方法执行前的信息
            var logData = $"【执行用户】:{userId} \r\n" +
                          $"【执行时间】:{DateTime.Now:yyyy/MM/dd HH:mm:ss}  \r\n" +
                          $"【执行方法】: {invocation.Method.Name}  \r\n" +
                          $"【执行参数】:{string.Join(", ", invocation.Arguments.Select(x => (x ?? "").ToString()).ToArray())} \r\n";
            try
            {
                //调用下一个拦截器直到目标方法
                invocation.Proceed();

                //判断是否为异步方法
                if (IsAsyncMethod(invocation.Method))
                {
                    var type = invocation.Method.ReturnType;
                    var resultProperty = type.GetProperty("Result");
                    if (resultProperty == null) return;
                    var result = resultProperty.GetValue(invocation.ReturnValue);
                    logData += $"【执行完成】:{JsonConvert.SerializeObject(result)}";
                    Parallel.For(0, 1, e =>
                    {
                        WriteLog(new[] { logData });
                    });
                }
                else//同步方法
                {
                    logData += $"【执行完成】:{invocation.ReturnValue}";
                    Parallel.For(0, 1, e =>
                    {
                        WriteLog(new[] { logData });
                    });
                }

            }
            catch (Exception ex)
            {
                LogException(ex, logData);
            }
        }

        //判断是否为异步方法
        private bool IsAsyncMethod(MethodInfo method)
        {
            return method.ReturnType == typeof(Task) ||
                   method.ReturnType.IsGenericType && method.ReturnType.GetGenericTypeDefinition() == typeof(Task<>);
        }

        //日志写入方法
        public static void WriteLog(string[] parameters, bool isHeader = true)
        {
            try
            {
                //进入写模式
                Lock.EnterWriteLock();

                //获取或建立文件夹
                var path = Path.Combine(Directory.GetCurrentDirectory(), "AOPLog");
                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }

                //获取log文件路径
                var logFilePath = Path.Combine(path, FileName);

                //转换及拼接字符
                var logContent = string.Join("\r\n", parameters);
                if (isHeader)
                {
                    logContent = "---------------------------------------\r\n"
                                 + DateTime.Now + "\r\n" + logContent + "\r\n";
                }

                //写入文件
                File.AppendAllText(logFilePath, logContent);
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
            finally
            {
                //退出写入模式,释放资源占用
                Lock.ExitWriteLock();
            }
        }

        //记录异常信息
        private void LogException(Exception ex, string logData)
        {
            if (ex == null) return;

            logData += $"【出现异常】:{ex.Message + ex.InnerException}\r\n";

            Parallel.For(0, 1, e =>
            {
                WriteLog(new[] { logData });
            });
        }
    }
}

三、注入服务和分配拦截器

动态代理代理的是服务,从咱们的项目结构上看就是BLL层。这里咱们在StartUp类中基于Autofac实现的方法ConfigureContainer内部进行拦截器的注册和分配操做,原先DALL和BLL写在一块儿了,这里须要拆开,以下:

四、运行实现效果

运行后执行两个方法,效果以下图所示。可是这里存在一个小问题,就是在用户已登陆的状况下,Swagger执行无需受权的方法时是不传递jwt字段的,因此这里userId为空,暂时没有找到解决方案,有了解的朋友可在评论区告知,先在此谢过

本章完~


本人知识点有限,若文中有错误的地方请及时指正,方便你们更好的学习和交流。

本文部份内容参考了网络上的视频内容和文章,仅为学习和交流,地址以下:

老张的哲学,系列教程一目录:.netcore+vue 先后端分离

韩俊俊,什么是面向切面编程AOP

声明

相关文章
相关标签/搜索