上一篇我们简单介绍了RESTful WebAPI涉及到的一些基础知识,并初步完善了系统的一些功能;本章我们将介绍下AOP并使用动态代理的方式实现记录日志的功能
AOP是Accept Oriented Programming的缩写,即面向切面编程。它与IOC控制反转,OOP面向对象编程等思想一样,都是一种编程思想,它是通过预编译方式和运行期间动态代理的方式来实现程序功能统一维护的一种技术。简单来说就是就是在不影响核心逻辑的情况下,为程序提供"可拔插"的扩展功能。如下图,来源为—韩俊俊,什么是面向切面编程AOP
随着关注点的不同会导致不同的切面,比如部分方法需要授权才能继续操作,一些方法中我们需要记录日志或是异常信息等,它们与核心逻辑没有必然的联系,它们独立且分散却又是程序中必不可少的一部分。C#语言是一种面向对象语言,它会基于OOP思想的封装、继承、多态三大特性将公共行为封装为一个类,但是当我们需要将独立的对象引入公共行为时,会发现它与OOP思想产生了一定的冲突,这时就需要运用AOP思想来解决这类问题。
在.NET Core中,实现AOP思想的常用对象有中间件(Middleware)、过滤器(Filter)和基于AOP思想的拦截器。其中拦截器又分为静态代理、动态代理;静态代理会在编译时静态植入,优点是效率高,缺点是缺乏灵活性;动态代理会为目标创建代理,通过代理调用实现拦截,优点是灵活性强,缺点是会影响部分效率。
上述三个对象它们对应了不同的应用场景:
通常情况下,当我们想记录项目接口的调用情况时,可以使用过滤器或者自定义一个中间件来实现,但如果想看下与数据层或逻辑层的调用情况,就比较复杂了,在这些层级中进行添加输出日志的功能显然不是一个合理的解决办法。这里我们采用动态代理的方式来解决,其核心思想就是将服务的实例交给代理类来控制,代理类可以在其内部方法中控制执行或者是添加自己的处理逻辑,下面我们来看下记录逻辑层调用信息的具体实现。
其实反射类Reflection中已经封装了代理方法,但是需要在StartUp中的ConfigureServices方法里指明代理类与服务实例的映射关系,这就导致没有较好的方法在控制器中使用。
由于之前我们已经使用Autofac容器替换了系统容器,所以这里我们可以选择使用一款封装好了的且与Autofac配合度较高的第三方插件Castle.Core,在BlogSystem.Core层使用NuGet安装如下包,它包含了Castle.Core
在BlogSystem.Core层中添加AOP文件夹,并添加一个名为LogAop的类,继承自拦截器接口IInterceptor(需要引用Castle.DynamicProxy)并实现其方法,这里我们先添加invocation.Proceed()方法,如下:
之后我们就可以在该方法内部自定义相关逻辑的,需要注意的是我们的系统内部大多数是异步操作,所以需要判断是否为异步方法并进行拦截,否则会拦截失败。这里逻辑基本上是参照的老张的哲学的,个人就稍微改了下,具体实现如下:
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
原文:https://www.cnblogs.com/Jscroop/p/12940066.html