上一篇我们简单介绍了RESTful WebAPI涉及到的一些基础知识,并初步完善了系统的一些功能;本章我们将介绍下AOP并使用动态代理的方式实现记录日志的功能
一、面向切面编程
1、什么是AOP
AOP是Accept Oriented Programming的缩写,即面向切面编程。它与IOC控制反转,OOP面向对象编程等思想一样,都是一种编程思想,它是通过预编译方式和运行期间动态代理的方式来实现程序功能统一维护的一种技术。简单来说就是就是在不影响核心逻辑的情况下,为程序提供"可拔插"的扩展功能。如下图,来源为—韩俊俊,什么是面向切面编程AOP
2、AOP思想的产生
随着关注点的不同会导致不同的切面,比如部分方法需要授权才能继续操作,一些方法中我们需要记录日志或是异常信息等,它们与核心逻辑没有必然的联系,它们独立且分散却又是程序中必不可少的一部分。C#语言是一种面向对象语言,它会基于OOP思想的封装、继承、多态三大特性将公共行为封装为一个类,但是当我们需要将独立的对象引入公共行为时,会发现它与OOP思想产生了一定的冲突,这时就需要运用AOP思想来解决这类问题。
3、AOP相关术语(了解)
- 横切关注点:用于一个系统的多个部分的片段功能,比如授权验证,日志记录,异常处理等;
- 通知(Advice):执行横切关注点(独立功能)的代码;
- 连接点(JoinPoint):程序执行通知的地方,比如一个类里面有10个方法,那么这10个方法在创建方法对象前,创建完成调用方法前以及调用方法后都可以看作是一个连接点;
- 切入点(PointCut):相当于AOP的“where”,它是连接点的”集合“,比如上面10个方法只想在其中几个连接点使用通知,那么这几个连接点就称为切入点;
- 切面(Aspect):切面是通知和切入点的结合;
- 引入(Introduction): 允许我们向现有的类添加新的方法或属性,就是把切面用到目标类中;
- 目标(Target): 引入中所提到的目标类,也就是要被通知的对象,也就是真正的业务逻辑;
- 代理(Porxy): 向目标对象增加通知之后创建的对象,由这个对象来访问实际的目标对象;
- 织入(Weaving): 将切面应用到目标对象来创建新的代理对象的过程;
4、.NET Core中的实现
在.NET Core中,实现AOP思想的常用对象有中间件(Middleware)、过滤器(Filter)和基于AOP思想的拦截器。其中拦截器又分为静态代理、动态代理;静态代理会在编译时静态植入,优点是效率高,缺点是缺乏灵活性;动态代理会为目标创建代理,通过代理调用实现拦截,优点是灵活性强,缺点是会影响部分效率。
上述三个对象它们对应了不同的应用场景:
- 中间件:处理的是请求管道;通常用于底层服务的通信
- 过滤器:处理的是Action方法和URL;通常用于身份验证,参数验证等
- 拦截器:处理的是对象的元数据,包括类、方法名、参数等;通常用于配合处理业务逻辑
二、AOP动态代理
通常情况下,当我们想记录项目接口的调用情况时,可以使用过滤器或者自定义一个中间件来实现,但如果想看下与数据层或逻辑层的调用情况,就比较复杂了,在这些层级中进行添加输出日志的功能显然不是一个合理的解决办法。这里我们采用动态代理的方式来解决,其核心思想就是将服务的实例交给代理类来控制,代理类可以在其内部方法中控制执行或者是添加自己的处理逻辑,下面我们来看下记录逻辑层调用信息的具体实现。
1、引入动态代理
其实反射类Reflection中已经封装了代理方法,但是需要在StartUp中的ConfigureServices方法里指明代理类与服务实例的映射关系,这就导致没有较好的方法在控制器中使用。
由于之前我们已经使用Autofac容器替换了系统容器,所以这里我们可以选择使用一款封装好了的且与Autofac配合度较高的第三方插件Castle.Core,在BlogSystem.Core层使用NuGet安装如下包,它包含了Castle.Core
2、设计拦截器
在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 });
});
}
}
}
3、注入服务和分配拦截器
动态代理代理的是服务,从我们的项目结构上看就是BLL层。这里我们在StartUp类中基于Autofac实现的方法ConfigureContainer内部进行拦截器的注册和分配操作,原先DALL和BLL写在一起了,这里需要拆开,如下:
4、运行实现效果
运行后执行两个方法,效果如下图所示。但是这里存在一个小问题,就是在用户已登录的情况下,Swagger执行无需授权的方法时是不传递jwt字段的,所以这里userId为空,暂时没有找到解决方案,有了解的朋友可在评论区告知,先在此谢过
本章完~
本人知识点有限,若文中有错误的地方请及时指正,方便大家更好的学习和交流。
本文部分内容参考了网络上的视频内容和文章,仅为学习和交流,地址如下:
老张的哲学,系列教程一目录:.netcore+vue 前后端分离
韩俊俊,什么是面向切面编程AOP